一、程序介绍:
需求:
支持多用户在线的FTP程序
要求:
1、用户加密认证
2、允许同时多用户登录
3、每个用户有自己的家目录 ,且只能访问自己的家目录
4、对用户进行磁盘配额,每个用户的可用空间不同
5、允许用户在ftp server上随意切换目录
6、允许用户查看当前目录下文件
7、允许上传和下载文件,保证文件一致性
8、文件传输过程中显示进度条
9、附加功能:支持文件的断点续传
实现功能:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
程序结构:
FTP服务端
FtpServer #服务端主目录
├── bin #启动目录
│ └── ftp_server.py #启动文件
├── conf #配置文件目录
│ ├── accounts.cfg #用户存储
│ └── settings.py #配置文件
├── core #程序主逻辑目录
│ ├── ftp_server.py #功能文件
│ └── main.py #主逻辑文件
├── home #用户家目录
│ ├── test001 #用户目录
│ └── test002 #用户目录
└── log #日志目录
FTP客户端
FtpClient #客户端主目录
└── ftp_client.py #客户端执行文件
二、流程图
三、代码
#FtpServer代码
bin/ftp_server.py
#!/usr/bin/env python
#_*_coding:utf-8_*_
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
from core import main
if __name__ == '__main__':
main.ArvgHandler()
conf/accounts.cfg
[DEFAULT]
[test001]
Password = 123
Quotation = 100
[test002]
Password = 123
Quotation = 100
conf/settings.py
#!/usr/bin/env python
#_*_coding:utf-8_*_
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
USER_HOME = "%s/home" % BASE_DIR
LOG_DIR = "%s/log" % BASE_DIR
LOG_LEVEL = "DEBUG"
ACCOUNT_FILE = "%s/conf/accounts.cfg" % BASE_DIR
HOST = "127.0.0.1"
PORT = 9999
core/ftp_server.py
#!/usr/bin/env python
#_*_coding:utf-8_*_
import socketserver
import json
import configparser
import os
import hashlib
from conf import settings
STATUS_CODE = {
250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}",
251:"Invalid cmd",
252:"Invalid auth data",
253:"Wrong username or password",
254:"Passed authentication",
255:"filename doesn't provided",
256:"File doesn't exist on server",
257:"ready to send file",
258:"md5 verification",
}
'''
250:“无效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}”,
251:“无效的CMD”,
252:“验证数据无效”,
253:“错误的用户名或密码”,
254:“通过身份验证”,
255:“文件名不提供”,
256:“服务器上不存在文件”,
257:“准备发送文件”,
258:“md5验证”,
'''
class FTPHandler(socketserver.BaseRequestHandler):
def handle(self):
'''接收客户端消息(用户,密码,action)'''
while True:
self.data = self.request.recv(1024).strip()
print(self.client_address[0])
print(self.data)
# self.request.sendall(self.data.upper())
if not self.data:
print("client closed...")
break
data = json.loads(self.data.decode()) #接收客户端消息
if data.get('action') is not None: #action不为空
print("---->", hasattr(self, "_auth"))
if hasattr(self, "_%s" % data.get('action')): #客户端action 符合服务端action
func = getattr(self, "_%s" % data.get('action'))
func(data)
else: #客户端action 不符合服务端action
print("invalid cmd")
self.send_response(251) # 251:“无效的CMD”
else: #客户端action 不正确
print("invalid cmd format")
self.send_response(250) # 250:“无效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}”
def send_response(self,status_code,data=None):
'''向客户端返回数据'''
response = {'status_code':status_code,'status_msg':STATUS_CODE[status_code]}
if data:
response.update(data)
self.request.send(json.dumps(response).encode())
def _auth(self,*args,**kwargs):
'''核对服务端 发来的用户,密码'''
# print("---auth",args,kwargs)
data = args[0]
if data.get("username") is None or data.get("password") is None: #客户端的用户和密码有一个为空 则返回错误
self.send_response(252) # 252:“验证数据无效”
user = self.authenticate(data.get("username"),data.get("password")) #把客户端的用户密码进行验证合法性
if user is None: #客户端的数据为空 则返回错误
self.send_response(253) # 253:“错误的用户名或密码”
else:
print("password authentication",user)
self.user = user
self.send_response(254) # 254:“通过身份验证”
def authenticate(self,username,password):
'''验证用户合法性,合法就返回数据,核对本地数据'''
config = configparser.ConfigParser()
config.read(settings.ACCOUNT_FILE)
if username in config.sections(): #用户匹配成功
_password = config[username]["Password"]
if _password == password: #密码匹配成功
print("pass auth..",username)
config[username]["Username"] = username
return config[username]
def _put(self,*args,**kwargs):
"client send file to server"
data = args[0]
base_filename = data.get('filename')
file_obj = open(base_filename, 'wb')
data = self.request.recv(4096)
file_obj.write(data)
file_obj.close()
def _get(self,*args,**kwargs):
'''get 下载方法'''
data = args[0]
if data.get('filename') is None:
self.send_response(255) # 255:“文件名不提供”,
user_home_dir = "%s/%s" %(settings.USER_HOME,self.user["Username"]) #当前连接用户的目录
file_abs_path = "%s/%s" %(user_home_dir,data.get('filename')) #客户端发送过来的目录文件
print("file abs path",file_abs_path)
if os.path.isfile(file_abs_path): #客户端目录文件名 存在服务端
file_obj = open(file_abs_path,'rb') # 用bytes模式打开文件
file_size = os.path.getsize(file_abs_path) #传输文件的大小
self.send_response(257,data={'file_size':file_size}) #返回即将传输的文件大小 和状态码
self.request.recv(1) #等待客户端确认
if data.get('md5'): #有 --md5 则传输时加上加密
md5_obj = hashlib.md5()
for line in file_obj:
self.request.send(line)
md5_obj.update(line)
else:
file_obj.close()
md5_val = md5_obj.hexdigest()
self.send_response(258,{'md5':md5_val})
print("send file done....")
else: #没有 --md5 直接传输文件
for line in file_obj:
self.request.send(line)
else:
file_obj.close()
print("send file done....")
else:
self.send_response(256) # 256:“服务器上不存在文件”=
def _ls(self,*args,**kwargs):
pass
def _cd(self,*args,**kwargs):
pass
if __name__ == '__main__':
HOST, PORT = "127.0.0.1", 9999
core/main.py
#!/usr/bin/env python
#_*_coding:utf-8_*_
import optparse
from core.ftp_server import FTPHandler
import socketserver
from conf import settings
class ArvgHandler(object):
def __init__(self):
self.parser = optparse.OptionParser()
# parser.add_option("-s","--host",dest="host",help="server binding host address")
# parser.add_option("-p","--port",dest="port",help="server binding port")
(options, args) = self.parser.parse_args()
# print("parser",options,args)
# print(options.host,options.port)
self.verify_args(options, args)
def verify_args(self,options,args):
'''校验并调用相应功能'''
if hasattr(self,args[0]):
func = getattr(self,args[0])
func()
else:
self.parser.print_help()
def start(self):
print('---going to start server---')
server = socketserver.ThreadingTCPServer((settings.HOST, settings.PORT), FTPHandler)
server.serve_forever()
#FtpClient代码
ftp_client.py
#!/usr/bin/env python
#_*_coding:utf-8_*_
import socket
import os
import sys
import optparse
import json
import hashlib
STATUS_CODE = {
250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}",
251:"Invalid cmd",
252:"Invalid auth data",
253:"Wrong username or password",
254:"Passed authentication",
255:"filename doesn't provided",
256:"File doesn't exist on server",
257:"ready to send file",
}
class FTPClient(object):
def __init__(self):
parser = optparse.OptionParser()
parser.add_option("-s","--server",dest="server",help="ftp server ip_addr")
parser.add_option("-P","--port",type="int",dest="port",help="ftp server port")
parser.add_option("-u","--username",dest="username",help="username")
parser.add_option("-p","--password",dest="password",help="password")
self.options,self.args = parser.parse_args()
self.verify_args(self.options,self.args)
self.make_connection()
def make_connection(self):
'''远程连接'''
self.sock = socket.socket()
self.sock.connect((self.options.server,self.options.port))
def verify_args(self,options,args):
'''校验参数合法性'''
if options.username is not None and options.password is not None: #用户和密码,两个都不为空
pass
elif options.username is None and options.password is None: #用户和密码,两个都为空
pass
else: #用户和密码,有一个为空
# options.username is None or options.password is None: #用户和密码,有一个为空
exit("Err: username and password must be provided together...")
if options.server and options.port:
# print(options)
if options.port >0 and options.port <65535:
return True
else:
exit("Err:host port must in 0-65535")
def authenticate(self):
'''用户验证,获取客户端输入信息'''
if self.options.username: #有输入信息 发到远程判断
print(self.options.username,self.options.password)
return self.get_auth_result(self.options.username,self.options.password)
else: #没有输入信息 进入交互式接收信息
retry_count = 0
while retry_count <3:
username = input("username: ").strip()
password = input("password: ").strip()
return self.get_auth_result(username,password)
# retry_count +=1
def get_auth_result(self,user,password):
'''远程服务器判断 用户,密码,action '''
data = {'action':'auth',
'username':user,
'password':password,}
self.sock.send(json.dumps(data).encode()) #发送 用户,密码,action 到远程服务器 等待远程服务器的返回结果
response = self.get_response() #获取服务器返回码
if response.get('status_code') == 254: #通过验证的服务器返回码
print("Passed authentication!")
self.user = user
return True
else:
print(response.get("status_msg"))
def get_response(self):
'''得到服务器端回复结果,公共方法'''
data = self.sock.recv(1024)
data = json.loads(data.decode())
return data
def interactive(self):
'''交互程序'''
if self.authenticate(): #认证成功,开始交互
print("--start interactive iwth u...")
while True: #循环 输入命令方法
choice = input("[%s]:"%self.user).strip()
if len(choice) == 0:continue
cmd_list = choice.split()
if hasattr(self,"_%s"%cmd_list[0]): #反射判断 方法名存在
func = getattr(self,"_%s"%cmd_list[0]) #反射 方法名
func(cmd_list) #执行方法
else:
print("Invalid cmd.")
def _md5_required(self,cmd_list):
'''检测命令是否需要进行MD5的验证'''
if '--md5' in cmd_list:
return True
def show_progress(self,total):
'''进度条'''
received_size = 0
current_percent = 0
while received_size < total:
if int((received_size / total) * 100) > current_percent :
print("#",end="",flush=True)
current_percent = (received_size / total) * 100
new_size = yield
received_size += new_size
def _get(self,cmd_list):
''' get 下载方法'''
print("get--",cmd_list)
if len(cmd_list) == 1:
print("no filename follows...")
return
#客户端操作信息
data_header = {
'action':'get',
'filename':cmd_list[1],
}
if self._md5_required(cmd_list): #命令请求里面有带 --md5
data_header['md5'] = True #将md5加入 客户端操作信息
self.sock.send(json.dumps(data_header).encode()) #发送客户端的操作信息
response = self.get_response() #接收服务端返回的 操作信息
print(response)
if response["status_code"] ==257: #服务端返回的状态码是:传输中
self.sock.send(b'1') # send confirmation to server
base_filename = cmd_list[1].split('/')[-1] #取出要接收的文件名
received_size = 0 #本地接收总量
file_obj = open(base_filename,'wb') #bytes模式写入
if self._md5_required(cmd_list): #命令请求里有 --md5
md5_obj = hashlib.md5()
progress = self.show_progress(response['file_size'])
progress.__next__()
while received_size < response['file_size']: #当接收的量 小于 文件总量 就循环接收文件
data = self.sock.recv(4096) #一次接收4096
received_size += len(data) #本地接收总量每次递增
try:
progress.send(len(data))
except StopIteration as e:
print("100%")
file_obj.write(data) #把接收的数据 写入文件
md5_obj.update(data) #把接收的数据 md5加密
else:
print("--->file rece done
file_obj.close() #关闭文件句柄
md5_val = md5_obj.hexdigest()
md5_from_server = self.get_response() #获取服务端发送的 md5
if md5_from_server['status_code'] ==258: #状态码为258
if md5_from_server['md5'] == md5_val: #两端 md5值 对比
print("%s 文件一致性校验成功!" %base_filename)
# print(md5_val,md5_from_server)
else: #没有md5校验 直接收文件
progress = self.show_progress(response['file_size'])
progress.__next__()
while received_size < response['file_size']: #当接收的量 小于 文件总量 就循环接收文件
data = self.sock.recv(4096) #一次接收4096
received_size += len(data) #本地接收总量每次递增
file_obj.write(data) #把接收的数据 写入文件
try:
progress.send(len(data))
except StopIteration as e:
print("100%")
else:
print("--->file rece done
file_obj.close() #关闭文件句柄
def _put(self,cmd_list):
''' put 下载方法'''
print("put--", cmd_list)
if len(cmd_list) == 1:
print("no filename follows...")
return
# 客户端操作信息
data_header = {
'action': 'put',
'filename': cmd_list[1],
}
self.sock.send(json.dumps(data_header).encode()) # 发送客户端的操作信息
self.sock.recv(1)
file_obj = open(cmd_list[1],'br')
for line in file_obj:
self.sock.send(line)
if __name__ == '__main__':
ftp = FTPClient()
ftp.interactive()