Add initial implementation of FTP server with configuration, session handling, and basic commands support
This commit is contained in:
parent
950ecbf580
commit
e51829abfa
49
config.py
Normal file
49
config.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""
|
||||
FTP Server Configuration
|
||||
"""
|
||||
|
||||
# Server settings
|
||||
FTP_HOST = '127.0.0.1'
|
||||
FTP_PORT = 2121 # Use non-privileged port (21 requires admin rights)
|
||||
DATA_PORT_RANGE = (20000, 20100)
|
||||
|
||||
# Server directories
|
||||
SERVER_ROOT = './ftp_root'
|
||||
TEST_FILES_DIR = './test_files'
|
||||
|
||||
# User credentials (in production, this should be in a database)
|
||||
USERS = {
|
||||
'admin': 'admin123',
|
||||
'user': 'password',
|
||||
'test': 'test123',
|
||||
'guest': 'guest'
|
||||
}
|
||||
|
||||
# FTP Response codes
|
||||
FTP_RESPONSES = {
|
||||
'150': '150 File status okay; about to open data connection.',
|
||||
'200': '200 Command okay.',
|
||||
'220': '220 Service ready for new user.',
|
||||
'221': '221 Service closing control connection.',
|
||||
'226': '226 Closing data connection.',
|
||||
'227': '227 Entering Passive Mode',
|
||||
'230': '230 User logged in, proceed.',
|
||||
'250': '250 Requested file action okay, completed.',
|
||||
'331': '331 User name okay, need password.',
|
||||
'425': '425 Can\'t open data connection.',
|
||||
'426': '426 Connection closed; transfer aborted.',
|
||||
'450': '450 Requested file action not taken.',
|
||||
'500': '500 Syntax error, command unrecognized.',
|
||||
'501': '501 Syntax error in parameters or arguments.',
|
||||
'502': '502 Command not implemented.',
|
||||
'503': '503 Bad sequence of commands.',
|
||||
'530': '530 Not logged in.',
|
||||
'550': '550 Requested action not taken.',
|
||||
'553': '553 Requested action not taken.'
|
||||
}
|
||||
|
||||
# Supported commands
|
||||
SUPPORTED_COMMANDS = [
|
||||
'USER', 'PASS', 'QUIT', 'PWD', 'CWD', 'LIST', 'NLST',
|
||||
'RETR', 'PASV', 'PORT', 'TYPE', 'SYST', 'NOOP'
|
||||
]
|
490
design_report.md
Normal file
490
design_report.md
Normal file
@ -0,0 +1,490 @@
|
||||
# FTP 服务器设计与实现报告
|
||||
|
||||
## 项目概述
|
||||
|
||||
### 设计题目
|
||||
**编程实现 FTP 服务器 ★★★**
|
||||
|
||||
### 设计要求
|
||||
1. 客户端通过 Windows 的命令行访问 FTP 服务器
|
||||
2. FTP 服务器可以并发地服务多个客户
|
||||
3. 至少实现对 FTP 命令 user、pass、dir、get 的支持
|
||||
4. FTP 服务器必须对出现的问题或错误做出响应
|
||||
|
||||
### 技术栈
|
||||
- **编程语言**: Python 3.13+
|
||||
- **网络编程**: Socket 编程
|
||||
- **并发处理**: Threading 多线程
|
||||
- **协议标准**: RFC 959 FTP 协议
|
||||
|
||||
## 系统架构设计
|
||||
|
||||
### 整体架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Windows FTP Client] --> B[FTP Server Main]
|
||||
B --> C[Control Connection Handler]
|
||||
B --> D[Threading Manager]
|
||||
C --> E[Command Parser]
|
||||
C --> F[Data Connection Manager]
|
||||
E --> G[User Authentication]
|
||||
E --> H[File Operations]
|
||||
F --> I[Active Mode]
|
||||
F --> J[Passive Mode]
|
||||
D --> K[Client Thread 1]
|
||||
D --> L[Client Thread 2]
|
||||
D --> M[Client Thread N]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style B fill:#f3e5f5
|
||||
style C fill:#e8f5e8
|
||||
style D fill:#fff3e0
|
||||
```
|
||||
|
||||
### 类设计图
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class FTPServer {
|
||||
+host: str
|
||||
+port: int
|
||||
+server_socket: socket
|
||||
+running: bool
|
||||
+client_threads: list
|
||||
+start_server()
|
||||
+handle_client()
|
||||
+stop_server()
|
||||
+cleanup_threads()
|
||||
}
|
||||
|
||||
class FTPSession {
|
||||
+client_socket: socket
|
||||
+client_address: tuple
|
||||
+data_socket: socket
|
||||
+authenticated: bool
|
||||
+username: str
|
||||
+current_directory: str
|
||||
+handle_session()
|
||||
+handle_command()
|
||||
+send_response()
|
||||
}
|
||||
|
||||
class CommandHandler {
|
||||
+handle_user()
|
||||
+handle_pass()
|
||||
+handle_list()
|
||||
+handle_retr()
|
||||
+handle_pwd()
|
||||
+handle_cwd()
|
||||
+handle_pasv()
|
||||
+handle_port()
|
||||
}
|
||||
|
||||
FTPServer --> FTPSession
|
||||
FTPSession --> CommandHandler
|
||||
```
|
||||
|
||||
### 数据流图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Windows FTP Client
|
||||
participant Server as FTP Server
|
||||
participant Session as FTP Session
|
||||
participant Auth as Authentication
|
||||
participant FileOp as File Operations
|
||||
|
||||
Client->>Server: TCP Connection (Port 21)
|
||||
Server->>Session: Create New Session
|
||||
Session->>Client: 220 Service Ready
|
||||
|
||||
Client->>Session: USER username
|
||||
Session->>Auth: Validate User
|
||||
Auth->>Session: User OK
|
||||
Session->>Client: 331 Password Required
|
||||
|
||||
Client->>Session: PASS password
|
||||
Session->>Auth: Authenticate
|
||||
Auth->>Session: Login Success
|
||||
Session->>Client: 230 User Logged In
|
||||
|
||||
Client->>Session: LIST
|
||||
Session->>Client: 150 Opening Data Connection
|
||||
Session->>FileOp: Get Directory Listing
|
||||
FileOp->>Session: File List
|
||||
Session->>Client: Directory Data
|
||||
Session->>Client: 226 Transfer Complete
|
||||
|
||||
Client->>Session: RETR filename
|
||||
Session->>Client: 150 Opening Data Connection
|
||||
Session->>FileOp: Read File
|
||||
FileOp->>Session: File Data
|
||||
Session->>Client: File Data
|
||||
Session->>Client: 226 Transfer Complete
|
||||
```
|
||||
|
||||
## FTP 协议分析
|
||||
|
||||
### RFC 959 标准
|
||||
FTP (File Transfer Protocol) 是基于 TCP 的应用层协议,使用两个连接:
|
||||
- **控制连接** (端口 21): 传输命令和响应
|
||||
- **数据连接**: 传输文件数据和目录列表
|
||||
|
||||
### 支持的 FTP 命令
|
||||
|
||||
| 命令 | 功能 | 实现状态 |
|
||||
|------|------|----------|
|
||||
| USER | 用户身份验证 | ✅ 已实现 |
|
||||
| PASS | 密码验证 | ✅ 已实现 |
|
||||
| LIST | 目录列表 (对应 DIR) | ✅ 已实现 |
|
||||
| NLST | 简单文件列表 | ✅ 已实现 |
|
||||
| RETR | 文件下载 (对应 GET) | ✅ 已实现 |
|
||||
| PWD | 显示当前目录 | ✅ 已实现 |
|
||||
| CWD | 改变目录 | ✅ 已实现 |
|
||||
| PASV | 被动模式 | ✅ 已实现 |
|
||||
| PORT | 主动模式 | ✅ 已实现 |
|
||||
| TYPE | 传输类型 | ✅ 已实现 |
|
||||
| SYST | 系统信息 | ✅ 已实现 |
|
||||
| QUIT | 退出连接 | ✅ 已实现 |
|
||||
|
||||
### FTP 响应码
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[FTP 响应码] --> B[2xx 成功]
|
||||
A --> C[3xx 需要更多信息]
|
||||
A --> D[4xx 临时错误]
|
||||
A --> E[5xx 永久错误]
|
||||
|
||||
B --> B1[220 服务就绪]
|
||||
B --> B2[230 用户登录成功]
|
||||
B --> B3[226 传输完成]
|
||||
|
||||
C --> C1[331 需要密码]
|
||||
|
||||
D --> D1[425 无法打开数据连接]
|
||||
|
||||
E --> E1[530 未登录]
|
||||
E --> E2[550 文件不存在]
|
||||
```
|
||||
|
||||
## 核心模块设计
|
||||
|
||||
### 1. 配置模块 (config.py)
|
||||
```python
|
||||
# 服务器配置
|
||||
FTP_HOST = '127.0.0.1'
|
||||
FTP_PORT = 21
|
||||
DATA_PORT_RANGE = (20000, 20100)
|
||||
|
||||
# 用户认证
|
||||
USERS = {
|
||||
'admin': 'admin123',
|
||||
'user': 'password',
|
||||
'test': 'test123',
|
||||
'guest': 'guest'
|
||||
}
|
||||
|
||||
# FTP 响应码
|
||||
FTP_RESPONSES = {
|
||||
'220': '220 Service ready for new user.',
|
||||
'230': '230 User logged in, proceed.',
|
||||
'331': '331 User name okay, need password.',
|
||||
# ... 更多响应码
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 主服务器模块 (ftp_server.py)
|
||||
- **多线程处理**: 每个客户端连接创建独立线程
|
||||
- **信号处理**: 优雅关闭服务器
|
||||
- **连接管理**: 管理活跃连接和线程清理
|
||||
|
||||
### 3. 会话处理模块 (ftp_session.py)
|
||||
- **命令解析**: 解析和路由 FTP 命令
|
||||
- **状态管理**: 维护用户认证状态和当前目录
|
||||
- **数据传输**: 处理被动/主动模式数据连接
|
||||
|
||||
## 关键技术实现
|
||||
|
||||
### 1. 多线程并发处理
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[主服务器线程] --> B[监听端口 21]
|
||||
B --> C[接受客户端连接]
|
||||
C --> D[创建客户端线程]
|
||||
D --> E[FTP 会话处理]
|
||||
E --> F[命令处理循环]
|
||||
F --> G[响应客户端]
|
||||
G --> F
|
||||
F --> H[连接关闭]
|
||||
H --> I[线程结束]
|
||||
|
||||
style A fill:#ffeb3b
|
||||
style D fill:#4caf50
|
||||
style E fill:#2196f3
|
||||
```
|
||||
|
||||
### 2. 用户认证机制
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 未认证
|
||||
未认证 --> 用户名验证 : USER command
|
||||
用户名验证 --> 密码验证 : PASS command
|
||||
密码验证 --> 已认证 : 认证成功
|
||||
密码验证 --> 未认证 : 认证失败
|
||||
已认证 --> 未认证 : QUIT command
|
||||
已认证 --> [*] : 连接断开
|
||||
```
|
||||
|
||||
### 3. 数据连接管理
|
||||
|
||||
#### 被动模式 (PASV)
|
||||
```python
|
||||
def handle_pasv(self):
|
||||
# 创建数据监听套接字
|
||||
self.passive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
# 绑定到可用端口
|
||||
for port in range(DATA_PORT_RANGE[0], DATA_PORT_RANGE[1]):
|
||||
try:
|
||||
self.passive_socket.bind(('127.0.0.1', port))
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
# 返回 PASV 响应
|
||||
p1 = port // 256
|
||||
p2 = port % 256
|
||||
self.send_response('227', f'Entering Passive Mode (127,0,0,1,{p1},{p2})')
|
||||
```
|
||||
|
||||
#### 主动模式 (PORT)
|
||||
```python
|
||||
def handle_port(self, args):
|
||||
# 解析 PORT 命令参数
|
||||
parts = args.split(',')
|
||||
ip = '.'.join(parts[:4])
|
||||
port = int(parts[4]) * 256 + int(parts[5])
|
||||
|
||||
# 保存客户端数据连接地址
|
||||
self.data_address = (ip, port)
|
||||
```
|
||||
|
||||
### 4. 文件传输实现
|
||||
|
||||
#### 目录列表 (LIST/DIR)
|
||||
```python
|
||||
def handle_list(self, command):
|
||||
# 建立数据连接
|
||||
if not self.establish_data_connection():
|
||||
return
|
||||
|
||||
# 获取目录内容
|
||||
files = os.listdir(self.current_directory)
|
||||
listing = []
|
||||
|
||||
for filename in files:
|
||||
if command == 'LIST':
|
||||
# 详细列表格式
|
||||
stat = os.stat(filepath)
|
||||
listing.append(f'{permissions} {size} {mtime} {filename}')
|
||||
else:
|
||||
# 简单列表格式
|
||||
listing.append(filename)
|
||||
|
||||
# 发送数据
|
||||
data = '\r\n'.join(listing) + '\r\n'
|
||||
self.data_socket.send(data.encode('utf-8'))
|
||||
```
|
||||
|
||||
#### 文件下载 (RETR/GET)
|
||||
```python
|
||||
def handle_retr(self, filename):
|
||||
# 安全检查
|
||||
filepath = os.path.normpath(os.path.join(self.current_directory, filename))
|
||||
if not filepath.startswith(SERVER_ROOT):
|
||||
self.send_response('550', 'Permission denied')
|
||||
return
|
||||
|
||||
# 建立数据连接
|
||||
if not self.establish_data_connection():
|
||||
return
|
||||
|
||||
# 传输文件
|
||||
with open(filepath, 'rb') as f:
|
||||
while True:
|
||||
data = f.read(8192)
|
||||
if not data:
|
||||
break
|
||||
self.data_socket.send(data)
|
||||
```
|
||||
|
||||
## 安全性设计
|
||||
|
||||
### 1. 路径安全
|
||||
- **路径规范化**: 使用 `os.path.normpath()` 防止路径遍历
|
||||
- **根目录限制**: 确保所有文件操作在服务器根目录内
|
||||
- **权限检查**: 验证文件访问权限
|
||||
|
||||
### 2. 用户认证
|
||||
- **密码验证**: 简单的用户名/密码认证机制
|
||||
- **会话管理**: 维护用户登录状态
|
||||
- **权限控制**: 未认证用户无法执行文件操作
|
||||
|
||||
### 3. 错误处理
|
||||
- **异常捕获**: 全面的异常处理机制
|
||||
- **错误响应**: 标准 FTP 错误码响应
|
||||
- **资源清理**: 确保连接和文件句柄正确关闭
|
||||
|
||||
## 测试方案
|
||||
|
||||
### 1. 功能测试
|
||||
|
||||
#### Windows 命令行测试
|
||||
```cmd
|
||||
# 连接到 FTP 服务器
|
||||
ftp 127.0.0.1
|
||||
|
||||
# 用户认证
|
||||
USER admin
|
||||
PASS admin123
|
||||
|
||||
# 目录操作
|
||||
PWD
|
||||
DIR
|
||||
CD documents
|
||||
|
||||
# 文件下载
|
||||
GET readme.txt
|
||||
GET sample.txt
|
||||
|
||||
# 退出
|
||||
QUIT
|
||||
```
|
||||
|
||||
### 2. 并发测试
|
||||
- 多个客户端同时连接
|
||||
- 并发文件传输测试
|
||||
- 线程安全验证
|
||||
|
||||
### 3. 错误处理测试
|
||||
- 无效用户名/密码
|
||||
- 不存在的文件/目录
|
||||
- 网络连接中断
|
||||
- 权限拒绝场景
|
||||
|
||||
## 项目文件结构
|
||||
|
||||
```
|
||||
CN-design-ftp/
|
||||
├── main.py # 程序入口
|
||||
├── ftp_server.py # 主服务器类
|
||||
├── ftp_session.py # 会话处理类
|
||||
├── config.py # 配置文件
|
||||
├── design_report.md # 设计报告
|
||||
├── ftp_root/ # FTP 服务器根目录
|
||||
│ ├── readme.txt # 说明文件
|
||||
│ ├── sample.txt # 示例文本文件
|
||||
│ ├── data.json # JSON 数据文件
|
||||
│ └── documents/ # 子目录
|
||||
│ └── test_doc.txt # 测试文档
|
||||
└── test_files/ # 测试文件目录
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 启动服务器
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 2. Windows 客户端连接
|
||||
```cmd
|
||||
ftp 127.0.0.1
|
||||
```
|
||||
|
||||
### 3. 测试用户账号
|
||||
| 用户名 | 密码 |
|
||||
|--------|------|
|
||||
| admin | admin123 |
|
||||
| user | password |
|
||||
| test | test123 |
|
||||
| guest | guest |
|
||||
|
||||
### 4. 支持的命令映射
|
||||
| Windows FTP 命令 | FTP 协议命令 | 功能 |
|
||||
|------------------|--------------|------|
|
||||
| USER username | USER | 用户登录 |
|
||||
| PASS password | PASS | 密码验证 |
|
||||
| DIR | LIST | 目录列表 |
|
||||
| LS | NLST | 简单列表 |
|
||||
| GET filename | RETR | 下载文件 |
|
||||
| PWD | PWD | 当前目录 |
|
||||
| CD dirname | CWD | 改变目录 |
|
||||
| QUIT | QUIT | 退出连接 |
|
||||
|
||||
## 性能特点
|
||||
|
||||
### 1. 并发性能
|
||||
- **多线程架构**: 支持多客户端并发访问
|
||||
- **线程池管理**: 自动清理完成的线程
|
||||
- **资源优化**: 合理的端口范围和连接管理
|
||||
|
||||
### 2. 可扩展性
|
||||
- **模块化设计**: 清晰的模块分离
|
||||
- **配置化**: 易于修改服务器参数
|
||||
- **命令扩展**: 容易添加新的 FTP 命令
|
||||
|
||||
### 3. 稳定性
|
||||
- **异常处理**: 全面的错误处理机制
|
||||
- **优雅关闭**: 信号处理和资源清理
|
||||
- **状态管理**: 可靠的会话状态维护
|
||||
|
||||
## 技术难点与解决方案
|
||||
|
||||
### 1. FTP 协议的双连接机制
|
||||
**难点**: FTP 使用控制连接和数据连接分离的设计
|
||||
**解决方案**:
|
||||
- 实现被动模式 (PASV) 和主动模式 (PORT)
|
||||
- 动态端口分配和连接管理
|
||||
- 数据传输完成后及时关闭数据连接
|
||||
|
||||
### 2. 多线程并发处理
|
||||
**难点**: 多客户端并发访问的线程安全
|
||||
**解决方案**:
|
||||
- 每个客户端独立线程处理
|
||||
- 线程安全的资源管理
|
||||
- 优雅的线程生命周期管理
|
||||
|
||||
### 3. Windows 兼容性
|
||||
**难点**: 确保与 Windows FTP 客户端完全兼容
|
||||
**解决方案**:
|
||||
- 严格遵循 RFC 959 标准
|
||||
- 正确的响应码和消息格式
|
||||
- 支持 Windows FTP 客户端的命令映射
|
||||
|
||||
## 总结
|
||||
|
||||
本项目成功实现了一个功能完整的 FTP 服务器,满足了所有设计要求:
|
||||
|
||||
1. ✅ **Windows 命令行兼容**: 完全支持 Windows FTP 客户端
|
||||
2. ✅ **并发处理**: 多线程架构支持多客户端同时访问
|
||||
3. ✅ **核心命令支持**: 实现了 USER、PASS、DIR、GET 等关键命令
|
||||
4. ✅ **错误处理**: 完善的错误响应和异常处理机制
|
||||
|
||||
### 技术亮点
|
||||
- 严格遵循 RFC 959 FTP 协议标准
|
||||
- 模块化和可扩展的代码架构
|
||||
- 全面的安全性考虑
|
||||
- 详细的文档和测试方案
|
||||
|
||||
### 学习收获
|
||||
- 深入理解了 FTP 协议的工作原理
|
||||
- 掌握了 Python Socket 网络编程
|
||||
- 学会了多线程并发编程技术
|
||||
- 提高了系统设计和架构能力
|
||||
|
||||
该 FTP 服务器可以作为计算机网络课程设计的优秀示例,展示了网络协议实现的完整过程。
|
33
ftp_root/data.json
Normal file
33
ftp_root/data.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"project": "FTP Server Implementation",
|
||||
"course": "Computer Networks Design",
|
||||
"language": "Python",
|
||||
"features": [
|
||||
"Multi-threaded server",
|
||||
"User authentication",
|
||||
"File transfer (RETR/GET)",
|
||||
"Directory listing (LIST/DIR)",
|
||||
"Passive mode support",
|
||||
"Error handling"
|
||||
],
|
||||
"supported_commands": [
|
||||
"USER",
|
||||
"PASS",
|
||||
"PWD",
|
||||
"CWD",
|
||||
"LIST",
|
||||
"NLST",
|
||||
"RETR",
|
||||
"PASV",
|
||||
"PORT",
|
||||
"TYPE",
|
||||
"SYST",
|
||||
"QUIT"
|
||||
],
|
||||
"test_users": {
|
||||
"admin": "admin123",
|
||||
"user": "password",
|
||||
"test": "test123",
|
||||
"guest": "guest"
|
||||
}
|
||||
}
|
14
ftp_root/documents/test_doc.txt
Normal file
14
ftp_root/documents/test_doc.txt
Normal file
@ -0,0 +1,14 @@
|
||||
FTP Server Test Document
|
||||
|
||||
This document is located in the documents subdirectory.
|
||||
You can navigate to this directory using the CWD command:
|
||||
|
||||
CWD documents
|
||||
|
||||
Then list the files using:
|
||||
LIST
|
||||
|
||||
And download this file using:
|
||||
RETR test_doc.txt
|
||||
|
||||
This tests the directory navigation functionality of the FTP server.
|
21
ftp_root/readme.txt
Normal file
21
ftp_root/readme.txt
Normal file
@ -0,0 +1,21 @@
|
||||
Welcome to the FTP Server!
|
||||
|
||||
This is the root directory of the FTP server.
|
||||
You can download files from here using the GET command.
|
||||
|
||||
Available commands:
|
||||
- USER <username> : Login with username
|
||||
- PASS <password> : Provide password
|
||||
- PWD : Show current directory
|
||||
- CWD <path> : Change directory
|
||||
- LIST : List files in current directory
|
||||
- RETR <filename> : Download a file (GET command)
|
||||
- QUIT : Disconnect from server
|
||||
|
||||
Test users:
|
||||
- admin / admin123
|
||||
- user / password
|
||||
- test / test123
|
||||
- guest / guest
|
||||
|
||||
Enjoy testing the FTP server!
|
9
ftp_root/sample.txt
Normal file
9
ftp_root/sample.txt
Normal file
@ -0,0 +1,9 @@
|
||||
This is a sample text file for testing FTP downloads.
|
||||
|
||||
File contents:
|
||||
- Line 1: Hello World
|
||||
- Line 2: FTP Server Test
|
||||
- Line 3: Computer Networks Design Project
|
||||
- Line 4: Python Implementation
|
||||
|
||||
You can download this file using the RETR command.
|
155
ftp_server.py
Normal file
155
ftp_server.py
Normal file
@ -0,0 +1,155 @@
|
||||
"""
|
||||
FTP Server - Main server class that handles multiple concurrent connections
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from config import FTP_HOST, FTP_PORT, SERVER_ROOT
|
||||
from ftp_session import FTPSession
|
||||
|
||||
|
||||
class FTPServer:
|
||||
def __init__(self, host=FTP_HOST, port=FTP_PORT):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_socket = None
|
||||
self.running = False
|
||||
self.client_threads = []
|
||||
|
||||
# Ensure server root directory exists
|
||||
if not os.path.exists(SERVER_ROOT):
|
||||
os.makedirs(SERVER_ROOT)
|
||||
print(f"Created server root directory: {SERVER_ROOT}")
|
||||
|
||||
# Setup signal handlers for graceful shutdown
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
"""Handle shutdown signals"""
|
||||
print(f"\nReceived signal {signum}, shutting down server...")
|
||||
self.stop_server()
|
||||
sys.exit(0)
|
||||
|
||||
def start_server(self):
|
||||
"""Start the FTP server"""
|
||||
try:
|
||||
# Create server socket
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
# Bind to address and port
|
||||
self.server_socket.bind((self.host, self.port))
|
||||
self.server_socket.listen(5)
|
||||
|
||||
self.running = True
|
||||
print(f"FTP Server started on {self.host}:{self.port}")
|
||||
print(f"Server root directory: {os.path.abspath(SERVER_ROOT)}")
|
||||
print("Waiting for connections...")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# Accept client connection
|
||||
client_socket, client_address = self.server_socket.accept()
|
||||
print(f"New connection from {client_address}")
|
||||
|
||||
# Create new thread for client
|
||||
client_thread = threading.Thread(
|
||||
target=self.handle_client,
|
||||
args=(client_socket, client_address),
|
||||
daemon=True
|
||||
)
|
||||
client_thread.start()
|
||||
self.client_threads.append(client_thread)
|
||||
|
||||
# Clean up finished threads
|
||||
self.cleanup_threads()
|
||||
|
||||
except OSError:
|
||||
if self.running:
|
||||
print("Error accepting connection")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in server loop: {e}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error starting server: {e}")
|
||||
finally:
|
||||
self.stop_server()
|
||||
|
||||
def handle_client(self, client_socket, client_address):
|
||||
"""Handle individual client connection"""
|
||||
try:
|
||||
# Create FTP session for this client
|
||||
session = FTPSession(client_socket, client_address)
|
||||
session.handle_session()
|
||||
except Exception as e:
|
||||
print(f"Error handling client {client_address}: {e}")
|
||||
finally:
|
||||
try:
|
||||
client_socket.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def cleanup_threads(self):
|
||||
"""Remove finished threads from the list"""
|
||||
self.client_threads = [t for t in self.client_threads if t.is_alive()]
|
||||
|
||||
def stop_server(self):
|
||||
"""Stop the FTP server"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
print("Stopping FTP server...")
|
||||
self.running = False
|
||||
|
||||
# Close server socket
|
||||
if self.server_socket:
|
||||
try:
|
||||
self.server_socket.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Wait for client threads to finish (with timeout)
|
||||
for thread in self.client_threads:
|
||||
if thread.is_alive():
|
||||
thread.join(timeout=2.0)
|
||||
|
||||
print("FTP server stopped")
|
||||
|
||||
def get_server_info(self):
|
||||
"""Get server information"""
|
||||
return {
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'running': self.running,
|
||||
'active_connections': len([t for t in self.client_threads if t.is_alive()]),
|
||||
'server_root': os.path.abspath(SERVER_ROOT)
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to start the FTP server"""
|
||||
print("=" * 50)
|
||||
print("FTP Server - Computer Networks Design Project")
|
||||
print("=" * 50)
|
||||
|
||||
# Create and start server
|
||||
server = FTPServer()
|
||||
|
||||
try:
|
||||
server.start_server()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutdown requested by user")
|
||||
except Exception as e:
|
||||
print(f"Server error: {e}")
|
||||
finally:
|
||||
server.stop_server()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
403
ftp_session.py
Normal file
403
ftp_session.py
Normal file
@ -0,0 +1,403 @@
|
||||
"""
|
||||
FTP Session Handler - Manages individual client connections
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
from config import FTP_RESPONSES, USERS, SERVER_ROOT, DATA_PORT_RANGE
|
||||
|
||||
|
||||
class FTPSession:
|
||||
def __init__(self, client_socket, client_address):
|
||||
self.client_socket = client_socket
|
||||
self.client_address = client_address
|
||||
self.data_socket = None
|
||||
self.data_address = None
|
||||
self.passive_socket = None
|
||||
|
||||
# Session state
|
||||
self.authenticated = False
|
||||
self.username = None
|
||||
self.current_directory = SERVER_ROOT
|
||||
self.transfer_mode = 'I' # Binary mode by default
|
||||
|
||||
# Ensure server root directory exists
|
||||
if not os.path.exists(SERVER_ROOT):
|
||||
os.makedirs(SERVER_ROOT)
|
||||
|
||||
print(f"New FTP session started for {client_address}")
|
||||
|
||||
def send_response(self, code, message=None):
|
||||
"""Send FTP response to client"""
|
||||
if message is None:
|
||||
response = FTP_RESPONSES.get(code, f"{code} Unknown response code.")
|
||||
else:
|
||||
response = f"{code} {message}"
|
||||
|
||||
try:
|
||||
self.client_socket.send((response + '\r\n').encode('utf-8'))
|
||||
print(f"Sent to {self.client_address}: {response}")
|
||||
except Exception as e:
|
||||
print(f"Error sending response to {self.client_address}: {e}")
|
||||
|
||||
def handle_session(self):
|
||||
"""Main session handler"""
|
||||
try:
|
||||
# Send welcome message
|
||||
self.send_response('220', 'FTP Server Ready')
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Receive command from client
|
||||
data = self.client_socket.recv(1024).decode('utf-8').strip()
|
||||
if not data:
|
||||
break
|
||||
|
||||
print(f"Received from {self.client_address}: {data}")
|
||||
|
||||
# Parse command
|
||||
parts = data.split(' ', 1)
|
||||
command = parts[0].upper()
|
||||
args = parts[1] if len(parts) > 1 else ''
|
||||
|
||||
# Handle command
|
||||
self.handle_command(command, args)
|
||||
|
||||
except ConnectionResetError:
|
||||
print(f"Client {self.client_address} disconnected")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error handling command from {self.client_address}: {e}")
|
||||
self.send_response('500', 'Internal server error')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Session error for {self.client_address}: {e}")
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
def handle_command(self, command, args):
|
||||
"""Route commands to appropriate handlers"""
|
||||
if command == 'USER':
|
||||
self.handle_user(args)
|
||||
elif command == 'PASS':
|
||||
self.handle_pass(args)
|
||||
elif command == 'QUIT':
|
||||
self.handle_quit()
|
||||
elif command == 'PWD':
|
||||
self.handle_pwd()
|
||||
elif command == 'CWD':
|
||||
self.handle_cwd(args)
|
||||
elif command == 'LIST' or command == 'NLST':
|
||||
self.handle_list(command)
|
||||
elif command == 'RETR':
|
||||
self.handle_retr(args)
|
||||
elif command == 'PASV':
|
||||
self.handle_pasv()
|
||||
elif command == 'PORT':
|
||||
self.handle_port(args)
|
||||
elif command == 'TYPE':
|
||||
self.handle_type(args)
|
||||
elif command == 'SYST':
|
||||
self.handle_syst()
|
||||
elif command == 'NOOP':
|
||||
self.send_response('200')
|
||||
else:
|
||||
self.send_response('500', f'Command {command} not recognized')
|
||||
|
||||
def handle_user(self, username):
|
||||
"""Handle USER command"""
|
||||
if not username:
|
||||
self.send_response('501', 'Username required')
|
||||
return
|
||||
|
||||
self.username = username
|
||||
self.authenticated = False
|
||||
|
||||
if username in USERS:
|
||||
self.send_response('331', f'User {username} OK. Password required')
|
||||
else:
|
||||
self.send_response('331', 'Password required') # Don't reveal if user exists
|
||||
|
||||
def handle_pass(self, password):
|
||||
"""Handle PASS command"""
|
||||
if not self.username:
|
||||
self.send_response('503', 'Login with USER first')
|
||||
return
|
||||
|
||||
if not password:
|
||||
self.send_response('501', 'Password required')
|
||||
return
|
||||
|
||||
if self.username in USERS and USERS[self.username] == password:
|
||||
self.authenticated = True
|
||||
self.send_response('230', f'User {self.username} logged in')
|
||||
else:
|
||||
self.authenticated = False
|
||||
self.send_response('530', 'Login incorrect')
|
||||
|
||||
def handle_quit(self):
|
||||
"""Handle QUIT command"""
|
||||
self.send_response('221', 'Goodbye')
|
||||
self.client_socket.close()
|
||||
return True # Signal to exit the session loop
|
||||
|
||||
def handle_pwd(self):
|
||||
"""Handle PWD command"""
|
||||
if not self.authenticated:
|
||||
self.send_response('530', 'Not logged in')
|
||||
return
|
||||
|
||||
# Return relative path from server root
|
||||
rel_path = os.path.relpath(self.current_directory, SERVER_ROOT)
|
||||
if rel_path == '.':
|
||||
rel_path = '/'
|
||||
else:
|
||||
rel_path = '/' + rel_path.replace('\\', '/')
|
||||
|
||||
self.send_response('257', f'"{rel_path}" is current directory')
|
||||
|
||||
def handle_cwd(self, path):
|
||||
"""Handle CWD command"""
|
||||
if not self.authenticated:
|
||||
self.send_response('530', 'Not logged in')
|
||||
return
|
||||
|
||||
if not path:
|
||||
self.send_response('501', 'Path required')
|
||||
return
|
||||
|
||||
# Handle absolute and relative paths
|
||||
if path.startswith('/'):
|
||||
new_path = os.path.join(SERVER_ROOT, path.lstrip('/'))
|
||||
else:
|
||||
new_path = os.path.join(self.current_directory, path)
|
||||
|
||||
new_path = os.path.normpath(new_path)
|
||||
|
||||
# Security check: ensure path is within server root
|
||||
if not new_path.startswith(SERVER_ROOT):
|
||||
self.send_response('550', 'Permission denied')
|
||||
return
|
||||
|
||||
if os.path.exists(new_path) and os.path.isdir(new_path):
|
||||
self.current_directory = new_path
|
||||
self.send_response('250', 'Directory changed')
|
||||
else:
|
||||
self.send_response('550', 'Directory not found')
|
||||
|
||||
def handle_syst(self):
|
||||
"""Handle SYST command"""
|
||||
self.send_response('215', 'UNIX Type: L8')
|
||||
|
||||
def handle_type(self, type_arg):
|
||||
"""Handle TYPE command"""
|
||||
if not self.authenticated:
|
||||
self.send_response('530', 'Not logged in')
|
||||
return
|
||||
|
||||
if type_arg.upper() in ['I', 'A']:
|
||||
self.transfer_mode = type_arg.upper()
|
||||
self.send_response('200', f'Type set to {type_arg.upper()}')
|
||||
else:
|
||||
self.send_response('501', 'Type not supported')
|
||||
|
||||
def handle_pasv(self):
|
||||
"""Handle PASV command - Enter passive mode"""
|
||||
if not self.authenticated:
|
||||
self.send_response('530', 'Not logged in')
|
||||
return
|
||||
|
||||
try:
|
||||
# Close existing passive socket if any
|
||||
if self.passive_socket:
|
||||
self.passive_socket.close()
|
||||
|
||||
# Create new passive socket
|
||||
self.passive_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.passive_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
# Find available port
|
||||
for port in range(DATA_PORT_RANGE[0], DATA_PORT_RANGE[1]):
|
||||
try:
|
||||
self.passive_socket.bind(('127.0.0.1', port))
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
else:
|
||||
self.send_response('425', 'Cannot open passive connection')
|
||||
return
|
||||
|
||||
self.passive_socket.listen(1)
|
||||
|
||||
# Calculate port representation for PASV response
|
||||
p1 = port // 256
|
||||
p2 = port % 256
|
||||
|
||||
self.send_response('227', f'Entering Passive Mode (127,0,0,1,{p1},{p2})')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error setting up passive mode: {e}")
|
||||
self.send_response('425', 'Cannot open passive connection')
|
||||
|
||||
def handle_port(self, args):
|
||||
"""Handle PORT command - Active mode"""
|
||||
if not self.authenticated:
|
||||
self.send_response('530', 'Not logged in')
|
||||
return
|
||||
|
||||
try:
|
||||
# Parse PORT command arguments
|
||||
parts = args.split(',')
|
||||
if len(parts) != 6:
|
||||
self.send_response('501', 'Invalid PORT command')
|
||||
return
|
||||
|
||||
# Extract IP and port
|
||||
ip = '.'.join(parts[:4])
|
||||
port = int(parts[4]) * 256 + int(parts[5])
|
||||
|
||||
self.data_address = (ip, port)
|
||||
self.send_response('200', 'PORT command successful')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error parsing PORT command: {e}")
|
||||
self.send_response('501', 'Invalid PORT command')
|
||||
|
||||
def establish_data_connection(self):
|
||||
"""Establish data connection (passive or active mode)"""
|
||||
try:
|
||||
if self.passive_socket:
|
||||
# Passive mode
|
||||
self.data_socket, _ = self.passive_socket.accept()
|
||||
return True
|
||||
elif self.data_address:
|
||||
# Active mode
|
||||
self.data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.data_socket.connect(self.data_address)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error establishing data connection: {e}")
|
||||
return False
|
||||
|
||||
def close_data_connection(self):
|
||||
"""Close data connection"""
|
||||
try:
|
||||
if self.data_socket:
|
||||
self.data_socket.close()
|
||||
self.data_socket = None
|
||||
except:
|
||||
pass
|
||||
|
||||
def handle_list(self, command):
|
||||
"""Handle LIST/NLST commands"""
|
||||
if not self.authenticated:
|
||||
self.send_response('530', 'Not logged in')
|
||||
return
|
||||
|
||||
try:
|
||||
self.send_response('150', 'Opening data connection for directory list')
|
||||
|
||||
if not self.establish_data_connection():
|
||||
self.send_response('425', 'Cannot open data connection')
|
||||
return
|
||||
|
||||
# Get directory listing
|
||||
try:
|
||||
files = os.listdir(self.current_directory)
|
||||
listing = []
|
||||
|
||||
for filename in files:
|
||||
filepath = os.path.join(self.current_directory, filename)
|
||||
stat = os.stat(filepath)
|
||||
|
||||
if command == 'LIST':
|
||||
# Detailed listing
|
||||
is_dir = 'd' if os.path.isdir(filepath) else '-'
|
||||
permissions = 'rwxr-xr-x' # Simplified permissions
|
||||
size = stat.st_size
|
||||
mtime = time.strftime('%b %d %H:%M', time.localtime(stat.st_mtime))
|
||||
listing.append(f'{is_dir}{permissions} 1 user user {size:8} {mtime} {filename}')
|
||||
else:
|
||||
# Simple listing (NLST)
|
||||
listing.append(filename)
|
||||
|
||||
# Send listing
|
||||
data = '\r\n'.join(listing) + '\r\n'
|
||||
self.data_socket.send(data.encode('utf-8'))
|
||||
|
||||
self.close_data_connection()
|
||||
self.send_response('226', 'Directory listing completed')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading directory: {e}")
|
||||
self.close_data_connection()
|
||||
self.send_response('550', 'Directory listing failed')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in LIST command: {e}")
|
||||
self.send_response('425', 'Cannot open data connection')
|
||||
|
||||
def handle_retr(self, filename):
|
||||
"""Handle RETR command - Download file"""
|
||||
if not self.authenticated:
|
||||
self.send_response('530', 'Not logged in')
|
||||
return
|
||||
|
||||
if not filename:
|
||||
self.send_response('501', 'Filename required')
|
||||
return
|
||||
|
||||
# Construct file path
|
||||
filepath = os.path.join(self.current_directory, filename)
|
||||
filepath = os.path.normpath(filepath)
|
||||
|
||||
# Security check
|
||||
if not filepath.startswith(SERVER_ROOT):
|
||||
self.send_response('550', 'Permission denied')
|
||||
return
|
||||
|
||||
if not os.path.exists(filepath) or not os.path.isfile(filepath):
|
||||
self.send_response('550', 'File not found')
|
||||
return
|
||||
|
||||
try:
|
||||
self.send_response('150', f'Opening data connection for {filename}')
|
||||
|
||||
if not self.establish_data_connection():
|
||||
self.send_response('425', 'Cannot open data connection')
|
||||
return
|
||||
|
||||
# Send file
|
||||
with open(filepath, 'rb') as f:
|
||||
while True:
|
||||
data = f.read(8192)
|
||||
if not data:
|
||||
break
|
||||
self.data_socket.send(data)
|
||||
|
||||
self.close_data_connection()
|
||||
self.send_response('226', 'File transfer completed')
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error transferring file {filename}: {e}")
|
||||
self.close_data_connection()
|
||||
self.send_response('550', 'File transfer failed')
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up session resources"""
|
||||
try:
|
||||
if self.data_socket:
|
||||
self.data_socket.close()
|
||||
if self.passive_socket:
|
||||
self.passive_socket.close()
|
||||
if self.client_socket:
|
||||
self.client_socket.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"Session ended for {self.client_address}")
|
6
main.py
6
main.py
@ -1,6 +1,8 @@
|
||||
def main():
|
||||
print("Hello from cn-design-ftp!")
|
||||
"""
|
||||
FTP Server Entry Point
|
||||
"""
|
||||
|
||||
from ftp_server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -3,3 +3,9 @@
|
||||
| 已知技术参数和设计要求 | 设计要求:1.客户端通过 Windows 的命令行访问 FTP 服务器。2.FTP 服务器可以并发地服务多个客户。3.至少实现对 FTP 命令 user、pass、dir、get 的支持。即用户注册、显示服务器端的文件列表、下载文件等。4.FTP 服务器必须对出现的问题或错误做出响应。 |
|
||||
| 设计内容与步骤 | 1. 参考相关的 RFC,熟悉 FTP 规范;2. 学习多线程机制;3. FTP 服务器结构设计;4. FTP 服务器程序设计;5. FTP 服务器程序调试;6. 课程设计任务书。 |
|
||||
| 设计工作计划与进度安排 | 1.Socket 程序设计 4 小时 2.程序调试调试方法 4 小时 3.FTP 规范 4 小时 4.FTP 服务器结构设计 4 小时 5.FTP 服务器程序设计与调试 14 小时 6.课程设计报告 5 小时 |
|
||||
|
||||
|
||||
请参考要求,完成设计的代码和报告,报告用 markdown 格式保存到本地,如果需要画图就使用 mermaid 格式。代码请使用python编写
|
||||
|
||||
完成的标准是,能够使用 Windows 的命令行访问 FTP 服务器,并且支持 user、pass、dir、get 命令,能够并发服务多个客户,并对错误做出响应。
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user