-
-
Notifications
You must be signed in to change notification settings - Fork 5
[STAGE 2] FTP payload. #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThis PR introduces a comprehensive FTP server payload implementation with control and data channel handling for file operations, updates logging in a bootstrap module, and modifies .gitignore entries to exclude a persistent directory while removing an obsolete ignore pattern. Changes
Sequence DiagramsequenceDiagram
participant Client
participant FTPServer
participant CtrlConn as Control Channel
participant DataConn as Data Channel
participant FileSystem
Client->>FTPServer: Connect
FTPServer->>CtrlConn: Accept connection
CtrlConn->>Client: 220 (Ready)
Client->>CtrlConn: USER command
CtrlConn->>FTPServer: handle_USER()
FTPServer->>Client: 331 (User OK)
Client->>CtrlConn: PASS command
CtrlConn->>FTPServer: handle_PASS()
FTPServer->>Client: 230 (Login OK)
Client->>CtrlConn: PASV command
CtrlConn->>FTPServer: handle_PASV()
FTPServer->>DataConn: Create listening socket
FTPServer->>Client: 227 (Passive mode ready)
Client->>DataConn: Connect (data channel)
Client->>CtrlConn: LIST command
CtrlConn->>FTPServer: handle_LIST()
FTPServer->>FileSystem: Read directory
FileSystem-->>FTPServer: Directory entries
FTPServer->>DataConn: Send listing
FTPServer->>Client: 226 (Transfer complete)
Client->>CtrlConn: QUIT command
CtrlConn->>FTPServer: Close connection
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.gitignore(1 hunks)payloads/ftp_server.py(1 hunks)stage2.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
stage2.py (1)
src/utils/rp.py (1)
log(45-52)
payloads/ftp_server.py (3)
src/structure.py (3)
Structure(9-46)create(23-25)get_field_raw(90-93)payloads/updater_for_up_to_2.x.x.py (1)
sc(16-34)src/utils/rp.py (1)
log(45-52)
🪛 Ruff (0.14.4)
payloads/ftp_server.py
32-32: Comment contains ambiguous – (EN DASH). Did you mean - (HYPHEN-MINUS)?
(RUF003)
129-129: Multiple statements on one line (colon)
(E701)
130-130: Multiple statements on one line (colon)
(E701)
131-131: Multiple statements on one line (colon)
(E701)
132-132: Multiple statements on one line (colon)
(E701)
133-133: Multiple statements on one line (colon)
(E701)
134-134: Multiple statements on one line (colon)
(E701)
135-135: Multiple statements on one line (colon)
(E701)
136-136: Multiple statements on one line (colon)
(E701)
202-205: Create your own exception
(TRY002)
230-233: Create your own exception
(TRY002)
237-240: Create your own exception
(TRY002)
272-272: Do not catch blind exception: Exception
(BLE001)
287-287: Do not catch blind exception: Exception
(BLE001)
299-299: Do not catch blind exception: Exception
(BLE001)
304-304: Undefined name unicode
(F821)
308-308: Undefined name unicode
(F821)
309-309: Do not catch blind exception: Exception
(BLE001)
310-310: Undefined name unichr
(F821)
314-314: Do not catch blind exception: Exception
(BLE001)
315-315: Undefined name unichr
(F821)
318-318: Undefined name unicode
(F821)
366-367: try-except-pass detected, consider logging the exception
(S110)
366-366: Do not catch blind exception: Exception
(BLE001)
503-503: Do not catch blind exception: Exception
(BLE001)
556-557: try-except-pass detected, consider logging the exception
(S110)
556-556: Do not catch blind exception: Exception
(BLE001)
569-570: try-except-pass detected, consider logging the exception
(S110)
569-569: Do not catch blind exception: Exception
(BLE001)
826-826: Do not catch blind exception: Exception
(BLE001)
960-961: try-except-pass detected, consider logging the exception
(S110)
960-960: Do not catch blind exception: Exception
(BLE001)
970-970: Do not catch blind exception: Exception
(BLE001)
1051-1051: Do not catch blind exception: Exception
(BLE001)
1051-1051: Local variable e is assigned to but never used
Remove assignment to unused variable e
(F841)
1060-1061: try-except-pass detected, consider logging the exception
(S110)
1060-1060: Do not catch blind exception: Exception
(BLE001)
1065-1066: try-except-pass detected, consider logging the exception
(S110)
1065-1065: Do not catch blind exception: Exception
(BLE001)
1075-1078: Create your own exception
(TRY002)
1100-1103: Create your own exception
(TRY002)
1107-1110: Create your own exception
(TRY002)
1112-1112: Possible binding to all interfaces
(S104)
1118-1119: try-except-pass detected, consider logging the exception
(S110)
1118-1118: Do not catch blind exception: Exception
(BLE001)
1139-1140: try-except-pass detected, consider logging the exception
(S110)
1139-1139: Do not catch blind exception: Exception
(BLE001)
| arg = line[4:].strip() if len(line) > 4 else None | ||
| if not arg: | ||
| self.ftp_send_ctrl( | ||
| "500 Syntax error, command unrecognized.\r\n" | ||
| ) | ||
| return | ||
|
|
||
| if arg == "/": | ||
| self.state.cur_path = "/" | ||
| self.ftp_send_ctrl( | ||
| "250 Requested file action okay, completed.\r\n" | ||
| ) | ||
| return | ||
|
|
||
| if arg == "..": | ||
| self.state.cur_path = dir_up(self.state.cur_path) | ||
| if self.state.cur_path != "/": | ||
| self.state.cur_path = "/" | ||
| self.ftp_send_ctrl( | ||
| "250 Requested file action okay, completed.\r\n" | ||
| ) | ||
| return |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix CWD .. handling
Walking up from /a/b/c should land at /a/b, but the current branch forces everything above the root back to /. Drop the extra reset so CWD .. truly moves one level up.
- if arg == "..":
- self.state.cur_path = dir_up(self.state.cur_path)
- if self.state.cur_path != "/":
- self.state.cur_path = "/"
+ if arg == "..":
+ self.state.cur_path = dir_up(self.state.cur_path)
self.ftp_send_ctrl(
"250 Requested file action okay, completed.\r\n"
)
return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| arg = line[4:].strip() if len(line) > 4 else None | |
| if not arg: | |
| self.ftp_send_ctrl( | |
| "500 Syntax error, command unrecognized.\r\n" | |
| ) | |
| return | |
| if arg == "/": | |
| self.state.cur_path = "/" | |
| self.ftp_send_ctrl( | |
| "250 Requested file action okay, completed.\r\n" | |
| ) | |
| return | |
| if arg == "..": | |
| self.state.cur_path = dir_up(self.state.cur_path) | |
| if self.state.cur_path != "/": | |
| self.state.cur_path = "/" | |
| self.ftp_send_ctrl( | |
| "250 Requested file action okay, completed.\r\n" | |
| ) | |
| return | |
| arg = line[4:].strip() if len(line) > 4 else None | |
| if not arg: | |
| self.ftp_send_ctrl( | |
| "500 Syntax error, command unrecognized.\r\n" | |
| ) | |
| return | |
| if arg == "/": | |
| self.state.cur_path = "/" | |
| self.ftp_send_ctrl( | |
| "250 Requested file action okay, completed.\r\n" | |
| ) | |
| return | |
| if arg == "..": | |
| self.state.cur_path = dir_up(self.state.cur_path) | |
| self.ftp_send_ctrl( | |
| "250 Requested file action okay, completed.\r\n" | |
| ) | |
| return |
🤖 Prompt for AI Agents
In payloads/ftp_server.py around lines 435 to 456, the CWD ".." branch currently
sets self.state.cur_path = dir_up(self.state.cur_path) but then incorrectly
forces any non-root result back to "/"—remove that extra reset so the result of
dir_up is preserved; simply assign the parent path returned by dir_up and keep
the existing response send, no additional conditional overwrite is needed.
| fd = self._open_for_send(full_path) | ||
| if fd < 0: | ||
| self.ftp_send_ctrl("550 File not found\r\n") | ||
| return | ||
|
|
||
| if self.state.restore_point and self.state.restore_point >= 0: | ||
| sc.syscalls.lseek(fd, self.state.restore_point, 0) # SEEK_SET=0 | ||
|
|
||
| chunk = bytearray(8192) | ||
| self.ftp_send_ctrl( | ||
| "150 Opening Image mode data transfer\r\n" | ||
| ) | ||
| if not self.ftp_open_data(): | ||
| sc.syscalls.close(fd) | ||
| return | ||
|
|
||
| while True: | ||
| n = u64_to_i64(sc.syscalls.read(fd, chunk, len(chunk))) | ||
| if n <= 0: | ||
| break | ||
| self.ftp_send_data(chunk[:n]) | ||
|
|
||
| self.ftp_close_data() | ||
| sc.syscalls.close(fd) | ||
| self.ftp_send_ctrl("226 Transfer completed\r\n") | ||
|
|
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear REST offset after RETR transfers
self.state.restore_point survives every RETR, so a later download without a new REST command skips the first chunk of data. REST markers are single-use and must be reset (effectively REST 0) once the transfer finishes or aborts.(rfc-editor.org) Make sure all exit paths restore self.state.restore_point = -1.
if fd < 0:
self.ftp_send_ctrl("550 File not found\r\n")
+ self.state.restore_point = -1
return
@@
if not self.ftp_open_data():
+ self.state.restore_point = -1
sc.syscalls.close(fd)
return
@@
self.ftp_send_ctrl("226 Transfer completed\r\n")
+ self.state.restore_point = -1📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fd = self._open_for_send(full_path) | |
| if fd < 0: | |
| self.ftp_send_ctrl("550 File not found\r\n") | |
| return | |
| if self.state.restore_point and self.state.restore_point >= 0: | |
| sc.syscalls.lseek(fd, self.state.restore_point, 0) # SEEK_SET=0 | |
| chunk = bytearray(8192) | |
| self.ftp_send_ctrl( | |
| "150 Opening Image mode data transfer\r\n" | |
| ) | |
| if not self.ftp_open_data(): | |
| sc.syscalls.close(fd) | |
| return | |
| while True: | |
| n = u64_to_i64(sc.syscalls.read(fd, chunk, len(chunk))) | |
| if n <= 0: | |
| break | |
| self.ftp_send_data(chunk[:n]) | |
| self.ftp_close_data() | |
| sc.syscalls.close(fd) | |
| self.ftp_send_ctrl("226 Transfer completed\r\n") | |
| fd = self._open_for_send(full_path) | |
| if fd < 0: | |
| self.ftp_send_ctrl("550 File not found\r\n") | |
| self.state.restore_point = -1 | |
| return | |
| if self.state.restore_point and self.state.restore_point >= 0: | |
| sc.syscalls.lseek(fd, self.state.restore_point, 0) # SEEK_SET=0 | |
| chunk = bytearray(8192) | |
| self.ftp_send_ctrl( | |
| "150 Opening Image mode data transfer\r\n" | |
| ) | |
| if not self.ftp_open_data(): | |
| self.state.restore_point = -1 | |
| sc.syscalls.close(fd) | |
| return | |
| while True: | |
| n = u64_to_i64(sc.syscalls.read(fd, chunk, len(chunk))) | |
| if n <= 0: | |
| break | |
| self.ftp_send_data(chunk[:n]) | |
| self.ftp_close_data() | |
| sc.syscalls.close(fd) | |
| self.ftp_send_ctrl("226 Transfer completed\r\n") | |
| self.state.restore_point = -1 |
🤖 Prompt for AI Agents
In payloads/ftp_server.py around lines 729 to 754, the REST offset
(self.state.restore_point) is left set after RETR transfers and on error paths;
REST markers must be single-use and reset to -1 once a transfer finishes or
aborts. Wrap the data-transfer sequence in a try/finally (or ensure every early
return path) and set self.state.restore_point = -1 in the finally block (or
immediately before each return on errors) so that after successful completion,
failure to open data, file-not-found, or any other exit the restore_point is
cleared.
| def _recv_file(self, full_path, append=False): | ||
| fd = self._open_for_recv(full_path, append=append) | ||
| if fd < 0: | ||
| self.ftp_send_ctrl("500 Error opening file\r\n") | ||
| return | ||
|
|
||
| self.ftp_send_ctrl( | ||
| "150 Opening Image mode data transfer\r\n" | ||
| ) | ||
| if not self.ftp_open_data(): | ||
| sc.syscalls.close(fd) | ||
| return | ||
|
|
||
| chunk = bytearray(8192) | ||
| while True: | ||
| # read from data connection into chunk | ||
| if self.state.conn_type == "active": | ||
| n = u64_to_i64( | ||
| sc.syscalls.read( | ||
| self.state.data_sock, | ||
| chunk, | ||
| len(chunk), | ||
| ) | ||
| ) | ||
| else: | ||
| n = u64_to_i64( | ||
| sc.syscalls.read( | ||
| self.state.pasv_accepted_sock, | ||
| chunk, | ||
| len(chunk), | ||
| ) | ||
| ) | ||
| if n <= 0: | ||
| break | ||
|
|
||
| w = u64_to_i64( | ||
| sc.syscalls.write(fd, chunk[:n], n) | ||
| ) | ||
| if w < n: | ||
| self.ftp_send_ctrl("550 File write error\r\n") | ||
| break | ||
|
|
||
| self.ftp_close_data() | ||
| sc.syscalls.close(fd) | ||
| self.ftp_send_ctrl("226 Transfer completed\r\n") |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset REST state after STOR/APPE
When a client resumes an upload with REST, the server must treat the restart marker as one-shot; leaving restore_point untouched means the next store starts mid-file even without REST. Clear the marker on every return path in _recv_file to honour the protocol.(rfc-editor.org)
fd = self._open_for_recv(full_path, append=append)
if fd < 0:
self.ftp_send_ctrl("500 Error opening file\r\n")
+ self.state.restore_point = -1
return
@@
if not self.ftp_open_data():
sc.syscalls.close(fd)
+ self.state.restore_point = -1
return
@@
self.ftp_close_data()
sc.syscalls.close(fd)
self.ftp_send_ctrl("226 Transfer completed\r\n")
+ self.state.restore_point = -1🤖 Prompt for AI Agents
In payloads/ftp_server.py around lines 755 to 799, _recv_file currently leaves
self.state.restore_point set when returning, which causes subsequent STOR/APPE
to resume incorrectly; clear the restart marker on every exit path by setting
self.state.restore_point = None (or the equivalent cleared value used elsewhere)
immediately before each return and also after the normal completion path so the
restart marker is one-shot per RFC3659.
| def handle_RNTO(self, line): | ||
| arg = line[5:].strip() | ||
| if not self.state.rname: | ||
| self.ftp_send_ctrl( | ||
| "503 Bad sequence of commands\r\n" | ||
| ) | ||
| return | ||
|
|
||
| old_full = sanitize_path(self.state.cur_path, self.state.rname) | ||
| new_full = sanitize_path(self.state.cur_path, arg) | ||
| ret = u64_to_i64( | ||
| sc.syscalls.rename( | ||
| old_full.encode("utf-8") + b"\0", | ||
| new_full.encode("utf-8") + b"\0", | ||
| ) | ||
| ) | ||
| if ret < 0: | ||
| self.ftp_send_ctrl( | ||
| "550 Error renaming file\r\n" | ||
| ) | ||
| else: | ||
| self.ftp_send_ctrl("226 Renamed file\r\n") |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear remembered RNFR name after RNTO
After any RNTO attempt you must require a fresh RNFR; otherwise a client can keep issuing RNTO with a stale self.state.rname, violating the RNFR/RNTO sequence defined in RFC 959.(w3.org) Reset self.state.rname = None on both success and failure.
if ret < 0:
self.ftp_send_ctrl(
"550 Error renaming file\r\n"
)
+ self.state.rname = None
else:
self.ftp_send_ctrl("226 Renamed file\r\n")
+ self.state.rname = None🤖 Prompt for AI Agents
In payloads/ftp_server.py around lines 928 to 949, after handling RNTO you must
clear the remembered RNFR name so subsequent RNTOs require a fresh RNFR; modify
the RNTO handler to set self.state.rname = None in both the error and success
paths (i.e., after determining ret < 0 and also after the else branch) so the
RNFR/RNTO sequence is enforced per RFC959.
| def serve_one(self, ctrl_sock): | ||
| self.state.ctrl_sock = ctrl_sock | ||
| self.state.is_connected = True | ||
| self.ftp_send_ctrl("220 RLL FTP Server\r\n") | ||
|
|
||
| handlers = { | ||
| "USER": self.handle_USER, | ||
| "PASS": self.handle_PASS, | ||
| "NOOP": self.handle_NOOP, | ||
| "PWD": self.handle_PWD, | ||
| "TYPE": self.handle_TYPE, | ||
| "PASV": self.handle_PASV, | ||
| "LIST": self.handle_LIST, | ||
| "CWD": self.handle_CWD, | ||
| "CDUP": self.handle_CDUP, | ||
| "PORT": self.handle_PORT, | ||
| "SYST": self.handle_SYST, | ||
| "FEAT": self.handle_FEAT, | ||
| "MKD": self.handle_MKD, | ||
| "RMD": self.handle_RMD, | ||
| "SIZE": self.handle_SIZE, | ||
| "DELE": self.handle_DELE, | ||
| "RNFR": self.handle_RNFR, | ||
| "RNTO": self.handle_RNTO, | ||
| "RETR": self.handle_RETR, | ||
| "STOR": self.handle_STOR, | ||
| "APPE": self.handle_APPE, | ||
| "REST": self.handle_REST, | ||
| "SITE": self.handle_SITE, | ||
| } | ||
|
|
||
| while True: | ||
| line = self.recv_line(ctrl_sock) | ||
| if line is None: | ||
| break | ||
| line = line.strip() | ||
| if not line: | ||
| continue | ||
|
|
||
| cmd = line.split()[0].upper() | ||
| if cmd == "QUIT": | ||
| self.ftp_send_ctrl("221 Goodbye\r\n") | ||
| break | ||
|
|
||
| handler = handlers.get(cmd) | ||
| if handler is None: | ||
| self.ftp_send_ctrl( | ||
| "500 Syntax error, command unrecognized.\r\n" | ||
| ) | ||
| continue | ||
|
|
||
| try: | ||
| handler(line) | ||
| except Exception as e: | ||
| # Best-effort robustness | ||
| self.ftp_send_ctrl( | ||
| "550 Internal server error\r\n" | ||
| ) | ||
|
|
||
| # cleanup |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset FTP session state per connection
FTPServer keeps a single FTPState object across sessions, so a new client inherits the previous client’s working directory, pending rename, REST offset, etc. That breaks FTP semantics and can expose one user’s state to the next. Re-initialize self.state when a control connection is accepted, copying only the configured root so each session starts clean.
- self.state.ctrl_sock = ctrl_sock
- self.state.is_connected = True
+ previous_root = self.state.root_path if self.state else "/"
+ self.state = FTPState()
+ self.state.root_path = previous_root
+ self.state.cur_path = previous_root
+ self.state.ctrl_sock = ctrl_sock
+ self.state.is_connected = True🧰 Tools
🪛 Ruff (0.14.4)
1051-1051: Do not catch blind exception: Exception
(BLE001)
1051-1051: Local variable e is assigned to but never used
Remove assignment to unused variable e
(F841)
🤖 Prompt for AI Agents
In payloads/ftp_server.py around lines 998 to 1057, the server reuses a single
FTPState across connections which leaks per-session state (cwd, RNFR, REST
offset, etc.) to new clients; reinitialize self.state at the start of serve_one
so each control connection gets a fresh FTPState that preserves only the
configured root (or equivalent configured session defaults). Create a new
FTPState instance (or reset all per-session fields) using the server's
configured root before sending the welcome and handling commands so every
session starts with a clean state.
| from utils.rp import log | ||
| from constants import SYSCALL | ||
|
|
||
| DEBUG = True |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct? Setting debug as true?
Helloyunho
left a comment
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check and fix reviews from coderabbit, and then it will be all good
Just an FTP payload stage 2 already tested on my PS5 in the latest fw 12.20 👍
Summary by CodeRabbit
New Features
Chores