Skip to content

Commit 249bccc

Browse files
authored
fix(ftp): reject a status code that is not three digits (#2416) (#2419)
1 parent 27c797d commit 249bccc

2 files changed

Lines changed: 81 additions & 6 deletions

File tree

‎pcapkit/protocols/application/ftp.py‎

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_FTP':
173173

174174
def make(self,
175175
cmmd: 'Optional[Enum_Command | str | bytes | bytearray]' = None,
176-
code: 'Optional[Enum_ReturnCode | int | str | bytes]' = None,
176+
code: 'Optional[Enum_ReturnCode | int | str | bytes | bytearray]' = None,
177177
args: 'Optional[str | bytes | bytearray]' = None,
178178
more: 'bool' = False,
179179
charset: 'Optional[str]' = None,
@@ -195,7 +195,10 @@ def make(self,
195195
cmmd: FTP command, a :class:`str` (an
196196
:class:`~pcapkit.const.ftp.command.Command` member is one),
197197
:class:`bytes` or :class:`bytearray`.
198-
code: FTP status code.
198+
code: FTP status code, an :class:`int` in ``0``--``999`` (an
199+
:class:`~pcapkit.const.ftp.return_code.ReturnCode` member is
200+
one) or a numeric :class:`str`, :class:`bytes` or
201+
:class:`bytearray` holding one.
199202
args: Optional FTP command arguments and/or status messages, a
200203
:class:`str`, :class:`bytes` or :class:`bytearray`.
201204
more: More status messages to follow for response packets.
@@ -213,8 +216,11 @@ def make(self,
213216
ProtocolError: If neither or both of ``cmmd`` and ``code`` are
214217
given, if ``cmmd`` or ``args`` is not a :class:`str`,
215218
:class:`bytes` or :class:`bytearray` (a :obj:`bool` included),
216-
or if ``lines`` are given other than as the rest of a
217-
multi-line reply.
219+
if ``code`` is not an :class:`int` in ``0``--``999`` (a
220+
:obj:`bool` included) nor a numeric :class:`str`,
221+
:class:`bytes` or :class:`bytearray` holding one, or if
222+
``lines`` are given other than as the rest of a multi-line
223+
reply.
218224
219225
"""
220226
if cmmd is not None and code is None:
@@ -231,8 +237,7 @@ def make(self,
231237

232238
mf = b''
233239
elif cmmd is None and code is not None:
234-
code_val = int(code)
235-
prefix = b'%03d' % code_val # reply codes are three digits, so keep a leading zero
240+
prefix = self._make_code(code)
236241

237242
mf = b'-' if more else b''
238243
else:
@@ -286,6 +291,48 @@ def _make_data(cls, data: 'Data_FTP') -> 'dict[str, Any]': # type: ignore[overr
286291
'lines': getattr(data, 'lines', ()),
287292
}
288293

294+
@staticmethod
295+
def _make_code(code: 'Enum_ReturnCode | int | str | bytes | bytearray') -> 'bytes':
296+
"""Render a reply code as the three digits a reply line opens with.
297+
298+
The three digits hold ``0``--``999``, which is what the ``[0-9]{3}``
299+
of ``FTP_RESPONSE`` reads back, so that is the range accepted rather
300+
than the 1xx--5xx of :rfc:`959#section-4.2` alone -- 6xx is
301+
:rfc:`2228#section-5`, and the read path accepts it.
302+
303+
Args:
304+
code: Reply code, an :class:`int` (a
305+
:class:`~pcapkit.const.ftp.return_code.ReturnCode` member is
306+
one) or a numeric :class:`str`, :class:`bytes` or
307+
:class:`bytearray` holding one.
308+
309+
Returns:
310+
The code as three digits, a leading zero kept.
311+
312+
Raises:
313+
ProtocolError: If ``code`` is of any other type, a :obj:`bool`
314+
included, or does not fit in three digits.
315+
316+
"""
317+
# NOTE: ``ReturnCode`` is an ``IntEnum``, so a member takes the ``int``
318+
# branch. ``bool`` is an ``int`` subclass, so without the explicit check
319+
# ``True`` would write code 001, and ``int()`` alone would raise a bare
320+
# ValueError or TypeError rather than a ProtocolError (#2416).
321+
value = None # type: Optional[int]
322+
if isinstance(code, bool):
323+
pass
324+
elif isinstance(code, int):
325+
value = code
326+
elif isinstance(code, (str, bytes, bytearray)):
327+
try:
328+
value = int(code)
329+
except ValueError:
330+
pass
331+
if value is None or not 0 <= value <= 999:
332+
raise ProtocolError('FTP: invalid status code: must be an int in 0-999 or a '
333+
f'numeric str, bytes or bytearray, not {code!r}')
334+
return b'%03d' % value
335+
289336
@staticmethod
290337
def _test_lines(code: 'bytes', lines: 'tuple[bytes, ...]') -> 'bool':
291338
"""Whether ``lines`` are the rest of a multi-line reply with ``code``.

‎tests/protocols/application/test_ftp_unit.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,34 @@ def test_ftp_make_rejects_arguments_that_are_not_str_bytes_or_bytearray(self) ->
236236
self.assertEqual(ftp.make(code=230, args=bytearray(b'Logged in')).data,
237237
b'230 Logged in\r\n')
238238

239+
def test_ftp_make_rejects_a_status_code_that_is_not_three_digits(self) -> None:
240+
"""A code of any other type, or out of ``0``--``999``, raises (#2416)."""
241+
from pcapkit.const.ftp.return_code import ReturnCode
242+
from pcapkit.protocols.application.ftp import FTP
243+
from pcapkit.utilities.exceptions import ProtocolError
244+
245+
ftp = object.__new__(FTP)
246+
247+
for code in (True, False, 3.7, [220], memoryview(b'230'),
248+
SimpleNamespace(value=220), 'abc', b'abc', 1000, -5, '1000', b'-5'):
249+
with self.subTest(code=code):
250+
with self.assertRaises(ProtocolError) as context:
251+
ftp.make(code=code, args='Ready')
252+
self.assertEqual(
253+
str(context.exception),
254+
'FTP: invalid status code: must be an int in 0-999 or a numeric '
255+
f'str, bytes or bytearray, not {code!r}')
256+
257+
# an int, a member and a numeric str, bytes or bytearray all still build
258+
for code in (230, ReturnCode.CODE_230, '230', b'230', bytearray(b'230')):
259+
with self.subTest(code=code):
260+
self.assertEqual(ftp.make(code=code, args='Logged in').data,
261+
b'230 Logged in\r\n')
262+
263+
# the three digits keep their leading zeroes at either end of the range
264+
self.assertEqual(ftp.make(code=0).data, b'000\r\n')
265+
self.assertEqual(ftp.make(code=999).data, b'999\r\n')
266+
239267
def test_ftp_make_data_preserves_request_and_response_fields(self) -> None:
240268
from pcapkit.const.ftp.command import Command
241269
from pcapkit.const.ftp.return_code import ReturnCode

0 commit comments

Comments
 (0)