Skip to content

Commit f2f55f3

Browse files
authored
Return buffered data on first EOF in tube.readline() (Gallopsled#2376)
* 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 Gallopsled#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
1 parent 7ac5a34 commit f2f55f3

File tree

3 files changed

+61
-4
lines changed

3 files changed

+61
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ The table below shows which release corresponds to each branch, and what date th
7979
- [#2330][2330] Change `context.newline` when setting `context.os` to `"windows"`
8080
- [#2389][2389] Fix passing bytes to `context.log_file` and `crc.BitPolynom`
8181
- [#2391][2391] Fix error message when passing invalid kwargs to `xor`
82+
- [#2376][2376] Return buffered data on first EOF in tube.readline()
8283

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

9294
## 4.13.0 (`beta`)
9395

pwnlib/context/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ class ContextType(object):
367367
'randomize': False,
368368
'rename_corefiles': True,
369369
'newline': b'\n',
370+
'throw_eof_on_incomplete_line': None,
370371
'noptrace': False,
371372
'os': 'linux',
372373
'proxy': None,
@@ -1490,6 +1491,25 @@ def newline(self, v):
14901491
# circular imports
14911492
from pwnlib.util.packing import _need_bytes
14921493
return _need_bytes(v)
1494+
1495+
@_validator
1496+
def throw_eof_on_incomplete_line(self, v):
1497+
"""Whether to raise an :class:`EOFError` if an EOF is received before a newline in ``tube.recvline``.
1498+
1499+
Controls if an :class:`EOFError` is treated as newline in ``tube.recvline`` and similar functions
1500+
and whether a warning should be logged about it.
1501+
1502+
Possible values are:
1503+
1504+
- ``True``: Raise an :class:`EOFError` if an EOF is received before a newline.
1505+
- ``False``: Return the data received so far if an EOF is received
1506+
before a newline without logging a warning.
1507+
- ``None``: Return the data received so far if an EOF is received
1508+
before a newline and log a warning.
1509+
1510+
Default value is ``None``.
1511+
"""
1512+
return v if v is None else bool(v)
14931513

14941514

14951515
@_validator

pwnlib/tubes/tube.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,19 +467,31 @@ def recvline(self, keepends=True, timeout=default):
467467
Receive a single line from the tube.
468468
469469
A "line" is any sequence of bytes terminated by the byte sequence
470-
set in :attr:`newline`, which defaults to ``'\n'``.
470+
set in :attr:`newline`, which defaults to ``b'\n'``.
471+
472+
If the connection is closed (:class:`EOFError`) before a newline
473+
is received, the buffered data is returned by default and a warning
474+
is logged. If the buffer is empty, an :class:`EOFError` is raised.
475+
This behavior can be changed by setting :meth:`pwnlib.context.ContextType.throw_eof_on_incomplete_line`.
471476
472477
If the request is not satisfied before ``timeout`` seconds pass,
473-
all data is buffered and an empty string (``''``) is returned.
478+
all data is buffered and an empty byte string (``b''``) is returned.
474479
475480
Arguments:
476481
keepends(bool): Keep the line ending (:const:`True`).
477482
timeout(int): Timeout
478483
484+
Raises:
485+
:class:`EOFError`: The connection closed before the request
486+
could be satisfied and the buffer is empty
487+
479488
Return:
480489
All bytes received over the tube until the first
481490
newline ``'\n'`` is received. Optionally retains
482-
the ending.
491+
the ending. If the connection is closed before a newline
492+
is received, the remaining data received up to this point
493+
is returned.
494+
483495
484496
Examples:
485497
@@ -494,8 +506,31 @@ def recvline(self, keepends=True, timeout=default):
494506
>>> t.newline = b'\r\n'
495507
>>> t.recvline(keepends = False)
496508
b'Foo\nBar'
509+
>>> t = tube()
510+
>>> def _recv_eof(n):
511+
... if not _recv_eof.throw:
512+
... _recv_eof.throw = True
513+
... return b'real line\ntrailing data'
514+
... raise EOFError
515+
>>> _recv_eof.throw = False
516+
>>> t.recv_raw = _recv_eof
517+
>>> t.recvline()
518+
b'real line\n'
519+
>>> t.recvline()
520+
b'trailing data'
521+
>>> t.recvline() # doctest: +ELLIPSIS
522+
Traceback (most recent call last):
523+
...
524+
EOFError
497525
"""
498-
return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
526+
try:
527+
return self.recvuntil(self.newline, drop = not keepends, timeout = timeout)
528+
except EOFError:
529+
if not context.throw_eof_on_incomplete_line and self.buffer.size > 0:
530+
if context.throw_eof_on_incomplete_line is None:
531+
self.warn_once('EOFError during recvline. Returning buffered data without trailing newline.')
532+
return self.buffer.get()
533+
raise
499534

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

0 commit comments

Comments
 (0)