Skip to content

Commit

Permalink
Return buffered data on first EOF in tube.readline() (#2376)
Browse files Browse the repository at this point in the history
* Return buffered data on first EOF in tube.readline()

When there is still data available in the tube buffer when an EOFError occurs in
`tube.recvline()`, return that data even though it doesn't contain a newline.
The next time `tube.recvline()` is called afterwards will raise EOFError normally.

This behavior is in line with the GNU readline implementation and avoids
loss of data. It allows `tube.stream()` to print everything that's received before
the receiving end terminates.

A new warning is logged when data is returned due to an EOF informing
about the lack of the trailing newline character.

Fixes #2366

* Update CHANGELOG

* Add context.throw_eof_on_incomplete_line

Allow to control the behavior of `tube.recvline` and
be able to suppress the new warning.

* Cleanup docs
  • Loading branch information
peace-maker committed Apr 21, 2024
1 parent 7ac5a34 commit f2f55f3
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The table below shows which release corresponds to each branch, and what date th
- [#2330][2330] Change `context.newline` when setting `context.os` to `"windows"`
- [#2389][2389] Fix passing bytes to `context.log_file` and `crc.BitPolynom`
- [#2391][2391] Fix error message when passing invalid kwargs to `xor`
- [#2376][2376] Return buffered data on first EOF in tube.readline()

[2360]: https://github.com/Gallopsled/pwntools/pull/2360
[2356]: https://github.com/Gallopsled/pwntools/pull/2356
Expand All @@ -88,6 +89,7 @@ The table below shows which release corresponds to each branch, and what date th
[2330]: https://github.com/Gallopsled/pwntools/pull/2330
[2389]: https://github.com/Gallopsled/pwntools/pull/2389
[2391]: https://github.com/Gallopsled/pwntools/pull/2391
[2376]: https://github.com/Gallopsled/pwntools/pull/2376

## 4.13.0 (`beta`)

Expand Down
20 changes: 20 additions & 0 deletions pwnlib/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ class ContextType(object):
'randomize': False,
'rename_corefiles': True,
'newline': b'\n',
'throw_eof_on_incomplete_line': None,
'noptrace': False,
'os': 'linux',
'proxy': None,
Expand Down Expand Up @@ -1490,6 +1491,25 @@ def newline(self, v):
# circular imports
from pwnlib.util.packing import _need_bytes
return _need_bytes(v)

@_validator
def throw_eof_on_incomplete_line(self, v):
"""Whether to raise an :class:`EOFError` if an EOF is received before a newline in ``tube.recvline``.
Controls if an :class:`EOFError` is treated as newline in ``tube.recvline`` and similar functions
and whether a warning should be logged about it.
Possible values are:
- ``True``: Raise an :class:`EOFError` if an EOF is received before a newline.
- ``False``: Return the data received so far if an EOF is received
before a newline without logging a warning.
- ``None``: Return the data received so far if an EOF is received
before a newline and log a warning.
Default value is ``None``.
"""
return v if v is None else bool(v)


@_validator
Expand Down
43 changes: 39 additions & 4 deletions pwnlib/tubes/tube.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,19 +467,31 @@ def recvline(self, keepends=True, timeout=default):
Receive a single line from the tube.
A "line" is any sequence of bytes terminated by the byte sequence
set in :attr:`newline`, which defaults to ``'\n'``.
set in :attr:`newline`, which defaults to ``b'\n'``.
If the connection is closed (:class:`EOFError`) before a newline
is received, the buffered data is returned by default and a warning
is logged. If the buffer is empty, an :class:`EOFError` is raised.
This behavior can be changed by setting :meth:`pwnlib.context.ContextType.throw_eof_on_incomplete_line`.
If the request is not satisfied before ``timeout`` seconds pass,
all data is buffered and an empty string (``''``) is returned.
all data is buffered and an empty byte string (``b''``) is returned.
Arguments:
keepends(bool): Keep the line ending (:const:`True`).
timeout(int): Timeout
Raises:
:class:`EOFError`: The connection closed before the request
could be satisfied and the buffer is empty
Return:
All bytes received over the tube until the first
newline ``'\n'`` is received. Optionally retains
the ending.
the ending. If the connection is closed before a newline
is received, the remaining data received up to this point
is returned.
Examples:
Expand All @@ -494,8 +506,31 @@ def recvline(self, keepends=True, timeout=default):
>>> t.newline = b'\r\n'
>>> t.recvline(keepends = False)
b'Foo\nBar'
>>> t = tube()
>>> def _recv_eof(n):
... if not _recv_eof.throw:
... _recv_eof.throw = True
... return b'real line\ntrailing data'
... raise EOFError
>>> _recv_eof.throw = False
>>> t.recv_raw = _recv_eof
>>> t.recvline()
b'real line\n'
>>> t.recvline()
b'trailing data'
>>> t.recvline() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
EOFError
"""
return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
try:
return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
except EOFError:
if not context.throw_eof_on_incomplete_line and self.buffer.size > 0:
if context.throw_eof_on_incomplete_line is None:
self.warn_once('EOFError during recvline. Returning buffered data without trailing newline.')
return self.buffer.get()
raise

def recvline_pred(self, pred, keepends=False, timeout=default):
r"""recvline_pred(pred, keepends=False) -> bytes
Expand Down

0 comments on commit f2f55f3

Please sign in to comment.