Files
STM32F4-Base/test/ftp_capture.py
2026-07-22 01:46:29 +08:00

257 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
FTP 协议抓取工具 — 模拟 FTP Server打印客户端每一条命令和交互细节
用法python ftp_capture.py [--port 21]
需要管理员权限Linux 无需Windows 需要管理员权限绑定 21 端口)
"""
import socket
import threading
import sys
import os
import time
import struct
import re
HOST = "0.0.0.0"
PORT = 21
DATA_DIR = "ftp_capture_data"
def log(fmt, *args):
ts = time.strftime("%H:%M:%S", time.localtime())
msg = fmt % args if args else fmt
print(f"[{ts}] {msg}")
def recv_line(sock):
data = b""
while True:
ch = sock.recv(1)
if not ch:
return None
data += ch
if data.endswith(b"\r\n"):
return data
def send_line(sock, line):
sock.sendall((line + "\r\n").encode())
log(">>> %s", line)
class FtpHandler(threading.Thread):
def __init__(self, ctrl_sock, addr):
super().__init__(daemon=True)
self.ctrl = ctrl_sock
self.addr = addr
self.cwd = "/"
self.data_listener = None
self.data_port = 0
self.pasv_socket = None
def run(self):
log("=== New connection from %s:%d ===", *self.addr)
send_line(self.ctrl, "220 Service ready for new user.")
while True:
raw = recv_line(self.ctrl)
if raw is None:
log("=== Connection closed ===")
break
line = raw.decode(errors="replace").rstrip()
log("<<< %s", line)
parts = line.split()
cmd = parts[0].upper()
arg = parts[1] if len(parts) > 1 else None
try:
self.handle(cmd, arg)
except Exception as e:
log("!!! ERROR: %s", e)
send_line(self.ctrl, "550 Internal error.")
self.cleanup()
def handle(self, cmd, arg):
handler = getattr(self, f"cmd_{cmd}", None)
if handler:
handler(arg)
else:
send_line(self.ctrl, f"502 Command not implemented.")
def cmd_USER(self, arg):
send_line(self.ctrl, "230 User logged in, proceed.")
def cmd_PASS(self, arg):
send_line(self.ctrl, "230 User logged in, proceed.")
def cmd_FEAT(self, arg):
send_line(self.ctrl, "211-System status, or system help reply.")
send_line(self.ctrl, " EPSV")
send_line(self.ctrl, " PASV")
send_line(self.ctrl, " SIZE")
send_line(self.ctrl, " NLST")
send_line(self.ctrl, "211 System status, or system help reply.")
def cmd_PWD(self, arg):
send_line(self.ctrl, f'257 "{self.cwd}"')
def cmd_CWD(self, arg):
if arg == "." or arg == "/":
self.cwd = "/"
send_line(self.ctrl, "250 Requested file action okay, completed.")
else:
send_line(self.ctrl, "550 Requested action not taken.")
def cmd_TYPE(self, arg):
send_line(self.ctrl, "200 Command okay.")
def cmd_SYST(self, arg):
send_line(self.ctrl, "215 UNIX Type: L8")
def cmd_NOOP(self, arg):
send_line(self.ctrl, "200 Command okay.")
def cmd_QUIT(self, arg):
send_line(self.ctrl, "221 Service closing control connection.")
self.ctrl.close()
def cmd_SIZE(self, arg):
send_line(self.ctrl, "550 File unavailable.")
def cmd_PASV(self, arg):
"""创建 PASV 数据监听"""
self.cleanup_data()
pasv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
pasv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
pasv.bind(("0.0.0.0", 0))
pasv.listen(1)
self.pasv_socket = pasv
port = pasv.getsockname()[1]
p1, p2 = port >> 8, port & 0xFF
# 使用 127.0.0.1 本地环回避免防火墙问题
send_line(self.ctrl,
f"227 Entering Passive Mode (127,0,0,1,{p1},{p2}).")
log(" PASV listener on port %d (127.0.0.1:port)", port)
def cmd_EPSV(self, arg):
self.cleanup_data()
pasv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
pasv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
pasv.bind(("0.0.0.0", 0))
pasv.listen(1)
self.pasv_socket = pasv
port = pasv.getsockname()[1]
send_line(self.ctrl, f"229 Entering Extended Passive Mode (|||{port}|)")
log(" EPSV listener on port %d", port)
def cmd_LIST(self, arg):
send_line(self.ctrl, "150 File status okay; about to open data connection.")
conn = self.accept_data()
if conn:
log(" LIST: sending directory listing (empty)")
conn.sendall(b"-rw-rw-rw- 1 owner group 123 Jan 01 1970 test.txt\r\n")
conn.close()
log(" LIST: data closed")
send_line(self.ctrl, "226 Closing data connection.")
def cmd_NLST(self, arg):
self.cmd_LIST(arg)
def cmd_STOR(self, arg):
"""接收文件上传"""
size = 0
send_line(self.ctrl, "150 File status okay; about to open data connection.")
conn = self.accept_data()
if conn:
log(" STOR: receiving file '%s'...", arg)
data = b""
while True:
try:
chunk = conn.recv(4096)
if not chunk:
break
data += chunk
size += len(chunk)
except socket.timeout:
break
conn.close()
log(" STOR: received %d bytes, data closed", size)
else:
log(" STOR: data connection failed!")
send_line(self.ctrl, f"226 Closing data connection.")
def cmd_RETR(self, arg):
send_line(self.ctrl, "550 File unavailable.")
def cmd_DELE(self, arg):
send_line(self.ctrl, "250 Requested file action okay, completed.")
def accept_data(self):
"""接受数据连接5秒超时"""
if self.pasv_socket is None:
log(" ERROR: no PASV listener!")
return None
try:
self.pasv_socket.settimeout(5)
conn, addr = self.pasv_socket.accept()
log(" Data connection from %s:%d", *addr)
return conn
except socket.timeout:
log(" Data accept TIMEOUT (5s)")
return None
except Exception as e:
log(" Data accept ERROR: %s", e)
return None
def cleanup_data(self):
if self.pasv_socket:
try:
self.pasv_socket.close()
except:
pass
self.pasv_socket = None
def cleanup(self):
self.cleanup_data()
try:
self.ctrl.close()
except:
pass
def main():
import argparse
parser = argparse.ArgumentParser(description="FTP Protocol Capture")
parser.add_argument("--port", type=int, default=PORT,
help=f"FTP port (default {PORT}, may need admin)")
args = parser.parse_args()
# Windows 下低端口需要管理员权限
if sys.platform == "win32" and args.port < 1024:
import ctypes
if not ctypes.windll.shell32.IsUserAnAdmin():
log("WARNING: Port %d may need admin rights on Windows", args.port)
log(" Run as Administrator or use --port 2121")
os.makedirs(DATA_DIR, exist_ok=True)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, args.port))
server.listen(5)
log("FTP capture server on port %d", args.port)
log("Connect with MobaXterm/FileZilla and try operations")
log("=" * 50)
try:
while True:
conn, addr = server.accept()
FtpHandler(conn, addr).start()
except KeyboardInterrupt:
log("Stopped")
finally:
server.close()
if __name__ == "__main__":
main()