支持多用戶在線的FTP程序 要求: 一、用戶加密認證 二、容許同時多用戶登陸 三、每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄 四、對用戶進行磁盤配額,每一個用戶的可用空間不一樣 五、容許用戶在ftp server上隨意切換目錄 六、容許用戶查看當前目錄下文件 七、容許上傳和下載文件,保證文件一致性 八、文件傳輸過程當中顯示進度條 九、附加功能:支持文件的斷點續傳 實現功能: 用戶加密認證 容許同時多用戶登陸 每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄 容許上傳和下載文件,保證文件一致性 文件傳輸過程當中顯示進度條
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
1 #!/usr/bin/env python 2 #_*_coding:utf-8_*_ 3 4 import os 5 import sys 6 7 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 sys.path.append(BASE_DIR) 9 10 from core import main 11 12 if __name__ == '__main__': 13 main.ArvgHandler()
conf/accounts.cfg
1 [DEFAULT] 2 3 [test001] 4 Password = 123 5 Quotation = 100 6 7 [test002] 8 Password = 123 9 Quotation = 100
conf/settings.py
1 #!/usr/bin/env python 2 #_*_coding:utf-8_*_ 3 4 import os 5 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 7 USER_HOME = "%s/home" % BASE_DIR 8 LOG_DIR = "%s/log" % BASE_DIR 9 LOG_LEVEL = "DEBUG" 10 11 ACCOUNT_FILE = "%s/conf/accounts.cfg" % BASE_DIR 12 13 HOST = "127.0.0.1" 14 PORT = 9999
core/ftp_server.py
1 #!/usr/bin/env python 2 #_*_coding:utf-8_*_ 3 4 import socketserver 5 import json 6 import configparser 7 import os 8 import hashlib 9 from conf import settings 10 11 STATUS_CODE = { 12 250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}", 13 251:"Invalid cmd", 14 252:"Invalid auth data", 15 253:"Wrong username or password", 16 254:"Passed authentication", 17 255:"filename doesn't provided", 18 256:"File doesn't exist on server", 19 257:"ready to send file", 20 258:"md5 verification", 21 } 22 23 ''' 24 250:「無效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}」, 25 251:「無效的CMD」, 26 252:「驗證數據無效」, 27 253:「錯誤的用戶名或密碼」, 28 254:「經過身份驗證」, 29 255:「文件名不提供」, 30 256:「服務器上不存在文件」, 31 257:「準備發送文件」, 32 258:「md5驗證」, 33 ''' 34 35 class FTPHandler(socketserver.BaseRequestHandler): 36 37 def handle(self): 38 '''接收客戶端消息(用戶,密碼,action)''' 39 while True: 40 self.data = self.request.recv(1024).strip() 41 print(self.client_address[0]) 42 print(self.data) 43 # self.request.sendall(self.data.upper()) 44 45 if not self.data: 46 print("client closed...") 47 break 48 data = json.loads(self.data.decode()) #接收客戶端消息 49 if data.get('action') is not None: #action不爲空 50 print("---->", hasattr(self, "_auth")) 51 if hasattr(self, "_%s" % data.get('action')): #客戶端action 符合服務端action 52 func = getattr(self, "_%s" % data.get('action')) 53 func(data) 54 else: #客戶端action 不符合服務端action 55 print("invalid cmd") 56 self.send_response(251) # 251:「無效的CMD」 57 else: #客戶端action 不正確 58 print("invalid cmd format") 59 self.send_response(250) # 250:「無效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}」 60 61 def send_response(self,status_code,data=None): 62 '''向客戶端返回數據''' 63 response = {'status_code':status_code,'status_msg':STATUS_CODE[status_code]} 64 if data: 65 response.update(data) 66 self.request.send(json.dumps(response).encode()) 67 68 def _auth(self,*args,**kwargs): 69 '''覈對服務端 發來的用戶,密碼''' 70 # print("---auth",args,kwargs) 71 data = args[0] 72 if data.get("username") is None or data.get("password") is None: #客戶端的用戶和密碼有一個爲空 則返回錯誤 73 self.send_response(252) # 252:「驗證數據無效」 74 75 user = self.authenticate(data.get("username"),data.get("password")) #把客戶端的用戶密碼進行驗證合法性 76 if user is None: #客戶端的數據爲空 則返回錯誤 77 self.send_response(253) # 253:「錯誤的用戶名或密碼」 78 else: 79 print("password authentication",user) 80 self.user = user 81 self.send_response(254) # 254:「經過身份驗證」 82 83 def authenticate(self,username,password): 84 '''驗證用戶合法性,合法就返回數據,覈對本地數據''' 85 config = configparser.ConfigParser() 86 config.read(settings.ACCOUNT_FILE) 87 if username in config.sections(): #用戶匹配成功 88 _password = config[username]["Password"] 89 if _password == password: #密碼匹配成功 90 print("pass auth..",username) 91 config[username]["Username"] = username 92 return config[username] 93 94 def _put(self,*args,**kwargs): 95 "client send file to server" 96 data = args[0] 97 base_filename = data.get('filename') 98 file_obj = open(base_filename, 'wb') 99 data = self.request.recv(4096) 100 file_obj.write(data) 101 file_obj.close() 102 103 def _get(self,*args,**kwargs): 104 '''get 下載方法''' 105 data = args[0] 106 if data.get('filename') is None: 107 self.send_response(255) # 255:「文件名不提供」, 108 user_home_dir = "%s/%s" %(settings.USER_HOME,self.user["Username"]) #當前鏈接用戶的目錄 109 file_abs_path = "%s/%s" %(user_home_dir,data.get('filename')) #客戶端發送過來的目錄文件 110 print("file abs path",file_abs_path) 111 112 if os.path.isfile(file_abs_path): #客戶端目錄文件名 存在服務端 113 file_obj = open(file_abs_path,'rb') # 用bytes模式打開文件 114 file_size = os.path.getsize(file_abs_path) #傳輸文件的大小 115 self.send_response(257,data={'file_size':file_size}) #返回即將傳輸的文件大小 和狀態碼 116 117 self.request.recv(1) #等待客戶端確認 118 119 if data.get('md5'): #有 --md5 則傳輸時加上加密 120 md5_obj = hashlib.md5() 121 for line in file_obj: 122 self.request.send(line) 123 md5_obj.update(line) 124 else: 125 file_obj.close() 126 md5_val = md5_obj.hexdigest() 127 self.send_response(258,{'md5':md5_val}) 128 print("send file done....") 129 else: #沒有 --md5 直接傳輸文件 130 for line in file_obj: 131 self.request.send(line) 132 else: 133 file_obj.close() 134 print("send file done....") 135 136 else: 137 self.send_response(256) # 256:「服務器上不存在文件」= 138 139 140 def _ls(self,*args,**kwargs): 141 pass 142 143 def _cd(self,*args,**kwargs): 144 pass 145 146 147 if __name__ == '__main__': 148 HOST, PORT = "127.0.0.1", 9999
core/main.py
1 #!/usr/bin/env python 2 #_*_coding:utf-8_*_ 3 4 import optparse 5 from core.ftp_server import FTPHandler 6 import socketserver 7 from conf import settings 8 9 class ArvgHandler(object): 10 def __init__(self): 11 self.parser = optparse.OptionParser() 12 # parser.add_option("-s","--host",dest="host",help="server binding host address") 13 # parser.add_option("-p","--port",dest="port",help="server binding port") 14 (options, args) = self.parser.parse_args() 15 # print("parser",options,args) 16 # print(options.host,options.port) 17 self.verify_args(options, args) 18 19 def verify_args(self,options,args): 20 '''校驗並調用相應功能''' 21 if hasattr(self,args[0]): 22 func = getattr(self,args[0]) 23 func() 24 else: 25 self.parser.print_help() 26 27 def start(self): 28 print('---going to start server---') 29 30 server = socketserver.ThreadingTCPServer((settings.HOST, settings.PORT), FTPHandler) 31 32 server.serve_forever()
FtpClient
ftp_client.py
1 #!/usr/bin/env python 2 #_*_coding:utf-8_*_ 3 4 import socket 5 import os 6 import sys 7 import optparse 8 import json 9 import hashlib 10 11 STATUS_CODE = { 12 250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}", 13 251:"Invalid cmd", 14 252:"Invalid auth data", 15 253:"Wrong username or password", 16 254:"Passed authentication", 17 255:"filename doesn't provided", 18 256:"File doesn't exist on server", 19 257:"ready to send file", 20 } 21 22 class FTPClient(object): 23 def __init__(self): 24 parser = optparse.OptionParser() 25 parser.add_option("-s","--server",dest="server",help="ftp server ip_addr") 26 parser.add_option("-P","--port",type="int",dest="port",help="ftp server port") 27 parser.add_option("-u","--username",dest="username",help="username") 28 parser.add_option("-p","--password",dest="password",help="password") 29 30 self.options,self.args = parser.parse_args() 31 self.verify_args(self.options,self.args) 32 self.make_connection() 33 34 def make_connection(self): 35 '''遠程鏈接''' 36 self.sock = socket.socket() 37 self.sock.connect((self.options.server,self.options.port)) 38 39 def verify_args(self,options,args): 40 '''校驗參數合法性''' 41 if options.username is not None and options.password is not None: #用戶和密碼,兩個都不爲空 42 pass 43 elif options.username is None and options.password is None: #用戶和密碼,兩個都爲空 44 pass 45 else: #用戶和密碼,有一個爲空 46 # options.username is None or options.password is None: #用戶和密碼,有一個爲空 47 exit("Err: username and password must be provided together...") 48 49 if options.server and options.port: 50 # print(options) 51 if options.port >0 and options.port <65535: 52 return True 53 else: 54 exit("Err:host port must in 0-65535") 55 56 def authenticate(self): 57 '''用戶驗證,獲取客戶端輸入信息''' 58 if self.options.username: #有輸入信息 發到遠程判斷 59 print(self.options.username,self.options.password) 60 return self.get_auth_result(self.options.username,self.options.password) 61 else: #沒有輸入信息 進入交互式接收信息 62 retry_count = 0 63 while retry_count <3: 64 username = input("username: ").strip() 65 password = input("password: ").strip() 66 return self.get_auth_result(username,password) 67 # retry_count +=1 68 69 def get_auth_result(self,user,password): 70 '''遠程服務器判斷 用戶,密碼,action ''' 71 data = {'action':'auth', 72 'username':user, 73 'password':password,} 74 75 self.sock.send(json.dumps(data).encode()) #發送 用戶,密碼,action 到遠程服務器 等待遠程服務器的返回結果 76 response = self.get_response() #獲取服務器返回碼 77 if response.get('status_code') == 254: #經過驗證的服務器返回碼 78 print("Passed authentication!") 79 self.user = user 80 return True 81 else: 82 print(response.get("status_msg")) 83 84 def get_response(self): 85 '''獲得服務器端回覆結果,公共方法''' 86 data = self.sock.recv(1024) 87 data = json.loads(data.decode()) 88 return data 89 90 def interactive(self): 91 '''交互程序''' 92 if self.authenticate(): #認證成功,開始交互 93 print("--start interactive iwth u...") 94 while True: #循環 輸入命令方法 95 choice = input("[%s]:"%self.user).strip() 96 if len(choice) == 0:continue 97 cmd_list = choice.split() 98 if hasattr(self,"_%s"%cmd_list[0]): #反射判斷 方法名存在 99 func = getattr(self,"_%s"%cmd_list[0]) #反射 方法名 100 func(cmd_list) #執行方法 101 else: 102 print("Invalid cmd.") 103 104 def _md5_required(self,cmd_list): 105 '''檢測命令是否須要進行MD5的驗證''' 106 if '--md5' in cmd_list: 107 return True 108 109 def show_progress(self,total): 110 '''進度條''' 111 received_size = 0 112 current_percent = 0 113 while received_size < total: 114 if int((received_size / total) * 100) > current_percent : 115 print("#",end="",flush=True) 116 current_percent = (received_size / total) * 100 117 new_size = yield 118 received_size += new_size 119 120 def _get(self,cmd_list): 121 ''' get 下載方法''' 122 print("get--",cmd_list) 123 if len(cmd_list) == 1: 124 print("no filename follows...") 125 return 126 #客戶端操做信息 127 data_header = { 128 'action':'get', 129 'filename':cmd_list[1], 130 } 131 132 if self._md5_required(cmd_list): #命令請求裏面有帶 --md5 133 data_header['md5'] = True #將md5加入 客戶端操做信息 134 135 self.sock.send(json.dumps(data_header).encode()) #發送客戶端的操做信息 136 response = self.get_response() #接收服務端返回的 操做信息 137 print(response) 138 139 if response["status_code"] ==257: #服務端返回的狀態碼是:傳輸中 140 self.sock.send(b'1') # send confirmation to server 141 base_filename = cmd_list[1].split('/')[-1] #取出要接收的文件名 142 received_size = 0 #本地接收總量 143 file_obj = open(base_filename,'wb') #bytes模式寫入 144 145 if self._md5_required(cmd_list): #命令請求裏有 --md5 146 md5_obj = hashlib.md5() 147 148 progress = self.show_progress(response['file_size']) 149 progress.__next__() 150 151 while received_size < response['file_size']: #當接收的量 小於 文件總量 就循環接收文件 152 data = self.sock.recv(4096) #一次接收4096 153 received_size += len(data) #本地接收總量每次遞增 154 155 try: 156 progress.send(len(data)) 157 except StopIteration as e: 158 print("100%") 159 160 file_obj.write(data) #把接收的數據 寫入文件 161 md5_obj.update(data) #把接收的數據 md5加密 162 else: 163 print("--->file rece done<---") #成功接收文件 164 file_obj.close() #關閉文件句柄 165 md5_val = md5_obj.hexdigest() 166 md5_from_server = self.get_response() #獲取服務端發送的 md5 167 if md5_from_server['status_code'] ==258: #狀態碼爲258 168 if md5_from_server['md5'] == md5_val: #兩端 md5值 對比 169 print("%s 文件一致性校驗成功!" %base_filename) 170 # print(md5_val,md5_from_server) 171 else: #沒有md5校驗 直接收文件 172 progress = self.show_progress(response['file_size']) 173 progress.__next__() 174 175 while received_size < response['file_size']: #當接收的量 小於 文件總量 就循環接收文件 176 data = self.sock.recv(4096) #一次接收4096 177 received_size += len(data) #本地接收總量每次遞增 178 file_obj.write(data) #把接收的數據 寫入文件 179 try: 180 progress.send(len(data)) 181 except StopIteration as e: 182 print("100%") 183 else: 184 print("--->file rece done<---") #成功接收文件 185 file_obj.close() #關閉文件句柄 186 187 def _put(self,cmd_list): 188 ''' put 下載方法''' 189 print("put--", cmd_list) 190 if len(cmd_list) == 1: 191 print("no filename follows...") 192 return 193 # 客戶端操做信息 194 data_header = { 195 'action': 'put', 196 'filename': cmd_list[1], 197 } 198 self.sock.send(json.dumps(data_header).encode()) # 發送客戶端的操做信息 199 self.sock.recv(1) 200 file_obj = open(cmd_list[1],'br') 201 for line in file_obj: 202 self.sock.send(line) 203 204 205 if __name__ == '__main__': 206 ftp = FTPClient() 207 ftp.interactive()