#!/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()
