diff --git a/README.md b/README.md index e69de29..42ecf03 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,238 @@ +# FTP 服务器实现 - 计算机网络课程设计 + +一个基于 Python 的多线程 FTP 服务器实现,支持 Windows 命令行客户端访问。 + +## 项目概述 + +本项目实现了一个功能完整的 FTP 服务器,严格遵循 RFC 959 标准,支持: + +- ✅ **多线程并发**: 支持多个客户端同时连接 +- ✅ **用户认证**: USER/PASS 命令实现用户登录 +- ✅ **目录操作**: LIST/DIR 命令显示文件列表 +- ✅ **文件下载**: RETR/GET 命令下载文件 +- ✅ **路径导航**: PWD/CWD 命令目录切换 +- ✅ **错误处理**: 完善的错误响应机制 +- ✅ **Windows 兼容**: 完全支持 Windows FTP 客户端 + +## 快速开始 + +### 1. 启动 FTP 服务器 + +```bash +python main.py +``` + +服务器将在 `127.0.0.1:2121` 启动(使用非特权端口避免需要管理员权限)。 + +### 2. 使用 Windows FTP 客户端连接 + +打开 Windows 命令提示符: + +```cmd +ftp 127.0.0.1 2121 +``` + +### 3. 登录测试 + +使用以下测试账户: + +| 用户名 | 密码 | +|--------|------| +| admin | admin123 | +| user | password | +| test | test123 | +| guest | guest | + +### 4. 基本操作示例 + +```cmd +# 连接服务器 +ftp 127.0.0.1 2121 + +# 登录 +User (127.0.0.1:(none)): admin +Password: admin123 + +# 查看当前目录 +ftp> pwd + +# 列出文件 +ftp> dir + +# 下载文件 +ftp> get readme.txt + +# 切换目录 +ftp> cd documents + +# 退出 +ftp> quit +``` + +## 支持的 FTP 命令 + +| FTP 命令 | Windows 命令 | 功能描述 | +|----------|--------------|----------| +| USER | USER | 用户身份验证 | +| PASS | PASS | 密码验证 | +| LIST | DIR | 详细文件列表 | +| NLST | LS | 简单文件列表 | +| RETR | GET | 文件下载 | +| PWD | PWD | 显示当前目录 | +| CWD | CD | 改变目录 | +| PASV | - | 被动模式 | +| PORT | - | 主动模式 | +| TYPE | - | 传输类型设置 | +| SYST | - | 系统信息 | +| QUIT | QUIT | 退出连接 | + +## 项目结构 + +``` +CN-design-ftp/ +├── main.py # 程序入口点 +├── ftp_server.py # 主服务器类 +├── ftp_session.py # 客户端会话处理 +├── config.py # 服务器配置 +├── test_ftp_client.py # 测试客户端 +├── design_report.md # 详细设计报告 +├── README.md # 项目说明 +├── ftp_root/ # FTP 服务器根目录 +│ ├── readme.txt # 服务器说明文件 +│ ├── sample.txt # 示例文本文件 +│ ├── data.json # JSON 数据文件 +│ └── documents/ # 子目录示例 +│ └── test_doc.txt # 测试文档 +└── test_files/ # 测试文件目录 +``` + +## 技术特性 + +### 架构设计 + +- **多线程服务器**: 每个客户端连接独立线程处理 +- **模块化设计**: 清晰的代码结构和职责分离 +- **配置化管理**: 易于修改服务器参数 + +### 安全特性 + +- **路径安全**: 防止目录遍历攻击 +- **用户认证**: 基于用户名/密码的访问控制 +- **错误处理**: 全面的异常处理和错误响应 + +### 协议兼容性 + +- **RFC 959 标准**: 严格遵循 FTP 协议规范 +- **双连接模式**: 支持控制连接和数据连接 +- **传输模式**: 支持被动模式和主动模式 + +## 测试 + +### 自动化测试 + +运行内置测试脚本: + +```bash +# 启动服务器(在一个终端) +python main.py + +# 运行测试(在另一个终端) +python test_ftp_client.py +``` + +### 手动测试 + +1. **基本连接测试** + ```bash + telnet 127.0.0.1 2121 + ``` + +2. **FTP 客户端测试** + ```cmd + ftp 127.0.0.1 2121 + ``` + +3. **并发测试** + - 同时打开多个 FTP 客户端连接 + - 验证多用户并发访问 + +## 配置说明 + +编辑 `config.py` 文件可以修改服务器配置: + +```python +# 服务器设置 +FTP_HOST = '127.0.0.1' # 服务器地址 +FTP_PORT = 2121 # 服务器端口 +DATA_PORT_RANGE = (20000, 20100) # 数据连接端口范围 + +# 服务器目录 +SERVER_ROOT = './ftp_root' # FTP 根目录 + +# 用户账户 +USERS = { + 'admin': 'admin123', + 'user': 'password', + 'test': 'test123', + 'guest': 'guest' +} +``` + +## 故障排除 + +### 常见问题 + +1. **端口被占用** + - 修改 `config.py` 中的 `FTP_PORT` 为其他端口 + - 确保防火墙允许该端口通信 + +2. **权限错误** + - 确保 `ftp_root` 目录有读写权限 + - 在 macOS/Linux 上可能需要 `chmod 755 ftp_root` + +3. **连接被拒绝** + - 检查服务器是否正常启动 + - 验证客户端连接的 IP 和端口是否正确 + +4. **文件传输失败** + - 检查被动模式端口范围是否被防火墙阻止 + - 确保数据连接端口 (20000-20100) 可用 + +### 调试模式 + +服务器会在控制台输出详细的连接和命令日志,便于调试: + +``` +FTP Server started on 127.0.0.1:2121 +New connection from ('127.0.0.1', 54321) +Received from ('127.0.0.1', 54321): USER admin +Sent to ('127.0.0.1', 54321): 331 User admin OK. Password required +``` + +## 开发说明 + +### 扩展功能 + +要添加新的 FTP 命令: + +1. 在 `ftp_session.py` 的 `handle_command` 方法中添加命令路由 +2. 实现对应的 `handle_xxx` 方法 +3. 在 `config.py` 中添加相应的响应码 + +### 代码结构 + +- `FTPServer`: 主服务器类,处理客户端连接 +- `FTPSession`: 单个客户端会话处理 +- `config.py`: 集中配置管理 + +## 许可证 + +本项目仅用于教育和学习目的。 + +## 作者 + +计算机网络课程设计项目 + +--- + +更多详细信息请参阅 [设计报告](design_report.md)。 diff --git a/design_report.md b/design_report.md index c8a3ece..ffe0d6c 100644 --- a/design_report.md +++ b/design_report.md @@ -403,9 +403,11 @@ python main.py ### 2. Windows 客户端连接 ```cmd -ftp 127.0.0.1 +ftp 127.0.0.1 2121 ``` +注意:由于使用非标准端口 2121(避免需要管理员权限),需要指定端口号。 + ### 3. 测试用户账号 | 用户名 | 密码 | |--------|------| diff --git a/ftp_root/text1.txt b/ftp_root/text1.txt new file mode 100644 index 0000000..117fb47 --- /dev/null +++ b/ftp_root/text1.txt @@ -0,0 +1,8 @@ +This is a test file for FTP download. + +Content: +- Line 1: Hello World +- Line 2: FTP Server Test +- Line 3: File download working correctly + +You can download this file using the GET command in FTP client. diff --git a/ftp_session.py b/ftp_session.py index 30604f7..45aa5e3 100644 --- a/ftp_session.py +++ b/ftp_session.py @@ -64,7 +64,9 @@ class FTPSession: args = parts[1] if len(parts) > 1 else '' # Handle command - self.handle_command(command, args) + should_quit = self.handle_command(command, args) + if should_quit: + break except ConnectionResetError: print(f"Client {self.client_address} disconnected") @@ -85,7 +87,7 @@ class FTPSession: elif command == 'PASS': self.handle_pass(args) elif command == 'QUIT': - self.handle_quit() + return self.handle_quit() elif command == 'PWD': self.handle_pwd() elif command == 'CWD': @@ -106,6 +108,7 @@ class FTPSession: self.send_response('200') else: self.send_response('500', f'Command {command} not recognized') + return False def handle_user(self, username): """Handle USER command""" @@ -178,7 +181,9 @@ class FTPSession: new_path = os.path.normpath(new_path) # Security check: ensure path is within server root - if not new_path.startswith(SERVER_ROOT): + server_root_abs = os.path.abspath(SERVER_ROOT) + new_path_abs = os.path.abspath(new_path) + if not new_path_abs.startswith(server_root_abs): self.send_response('550', 'Permission denied') return @@ -356,8 +361,10 @@ class FTPSession: filepath = os.path.join(self.current_directory, filename) filepath = os.path.normpath(filepath) - # Security check - if not filepath.startswith(SERVER_ROOT): + # Security check: ensure path is within server root + server_root_abs = os.path.abspath(SERVER_ROOT) + filepath_abs = os.path.abspath(filepath) + if not filepath_abs.startswith(server_root_abs): self.send_response('550', 'Permission denied') return diff --git a/test_ftp_client.py b/test_ftp_client.py new file mode 100644 index 0000000..4a9f45d --- /dev/null +++ b/test_ftp_client.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +Simple FTP Client Test Script +Tests the basic functionality of our FTP server +""" + +import socket +import time + +def test_ftp_server(): + """Test basic FTP server functionality""" + print("Testing FTP Server...") + + try: + # Connect to FTP server + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('127.0.0.1', 2121)) + + # Read welcome message + response = sock.recv(1024).decode('utf-8') + print(f"Server: {response.strip()}") + + # Test USER command + sock.send(b'USER admin\r\n') + response = sock.recv(1024).decode('utf-8') + print(f"Server: {response.strip()}") + + # Test PASS command + sock.send(b'PASS admin123\r\n') + response = sock.recv(1024).decode('utf-8') + print(f"Server: {response.strip()}") + + # Test PWD command + sock.send(b'PWD\r\n') + response = sock.recv(1024).decode('utf-8') + print(f"Server: {response.strip()}") + + # Test SYST command + sock.send(b'SYST\r\n') + response = sock.recv(1024).decode('utf-8') + print(f"Server: {response.strip()}") + + # Test QUIT command + sock.send(b'QUIT\r\n') + response = sock.recv(1024).decode('utf-8') + print(f"Server: {response.strip()}") + + sock.close() + print("✅ Basic FTP commands test passed!") + + except Exception as e: + print(f"❌ Test failed: {e}") + +if __name__ == "__main__": + test_ftp_server()