基础知识

FTP只通过TCP连接,FTP不同于其他服务的是它使用了两个端口, 一个数据端口和一个命令端口(或称为控制端口)。

通常21端口是命令端口,20端口是数据端口。当混入主动/被动模式的概念时,数据端口就有可能不是20了

FTP主动模式

在主动模式下,FTP客户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,

然后开放N+1号端口进行监听,并向服务器发出PORT N+1命令。

服务器接收到命令后,会用其本地的FTP数据端口(通常是20)来连接客户端指定的端口N+1,进行数据传输。

  • FTP服务器命令(21)端口接受客户端任意端口(客户端初始连接)
  • FTP服务器命令(21)端口到客户端端口(>1023)(服务器响应客户端命令)
  • FTP服务器数据(20)端口到客户端端口(>1023)(服务器初始化数据连接到客户端数据端口)
  • FTP服务器数据(20)端口接受客户端端口(>1023)(客户端发送ACK包到服务器的数据端口)

主动模式的优点:

服务端配置简单,利于服务器安全管理,服务器只需要开放21端口

主动模式的缺点:

如果客户端开启了防火墙,或客户端处于内网(NAT网关之后), 那么服务器对客户端端口发起的连接可能会失败

FTP被动模式

在被动模式下,FTP库户端随机开启一个大于1024的端口N向服务器的21号端口发起连接,同时会开启N+1号端口。

然后向服务器发送PASV命令,通知服务器自己处于被动模式。

服务器收到命令后,会开放一个大于1024的端口P进行监听,然后用PORT P命令通知客户端,自己的数据端口是P。

客户端收到命令后,会通过N+1号端口连接服务器的端口P,然后在两个端口之间进行数据传输

  • FTP服务器命令(21)端口接受客户端任意端口(客户端初始连接)
  • FTP服务器命令(21)端口到客户端端口(>1023)(服务器响应客户端命令)
  • FTP服务器数据端口(>1023)接受客户端端口(>1023)(客户端初始化数据连接到服务器指定的任意端口)
  • FTP服务器数据端口(>1023)到客户端端口(>1023)(服务器发送ACK响应和数据到客户端的数据端口)

被动模式缺点:

服务器配置管理稍显复杂,不利于安全,服务器需要开放随机高位端口以便客户端可以连接,因此大多数FTP服务软件都可以手动配置被动端口的范围

被动模式的优点:

对客户端网络环境没有要求

使用python来实现FTP服务

安装模块 pyftpdlib

pip3 install pyftpdlib

Tutorial — pyftpdlib 1.5.4 documentation教程

源码

GitHub - giampaolo/pyftpdlib: Extremely fast and scalable Python FTP server library

使用:

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
 
# 新建一个用户组
authorizer = DummyAuthorizer()
# 将用户名,密码,指定目录,权限 添加到里面
authorizer.add_user("fan", "root", "E:/", perm="elr")  # adfmw
# 这个是添加匿名用户,任何人都可以访问,如果去掉的话,需要输入用户名和密码,可以自己尝试
authorizer.add_anonymous("E:/")
 
handler = FTPHandler
handler.authorizer = authorizer
# 开启服务器
server = FTPServer(("127.0.0.1", 21), handler)
server.serve_forever()

然后将程序运行起来,接下来看一下效果,在浏览器上ftp://localhost/

用户权限

读取权限:

"e" =更改目录(CWD,CDUP命令)

"l" =列表文件(LIST,NLST,STAT,MLSD,MLST,SIZE命令)

"r" =从服务器检索文件(RETR命令)

写入权限:

"a" =将数据追加到现有文件(APPE命令)

"d" =删除文件或目录(DELE,RMD命令)

"f" =重命名文件或目录(RNFR,RNTO命令)

"m" =创建目录(MKD命令)

"w" =将文件存储到服务器(STOR,STOU命令)

"M"=更改文件模式/权限(SITE CHMOD命令)

"T"=更改文件修改时间(SITE MFMT命令)

开启被动端口模式

#添加被动端口范围
handler.passive_ports = range(8300, 8500)
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer

# 新建一个用户组
authorizer = DummyAuthorizer()
# 将用户名,密码,指定目录,权限 添加到里面
authorizer.add_user("test", "1234", "E:/", perm="elradfmw")  # adfmw
# 这个是添加匿名用户,任何人都可以访问,如果去掉的话,需要输入用户名和密码,可以自己尝试
authorizer.add_anonymous("E:/")

handler = FTPHandler
handler.authorizer = authorizer

#添加被动端口范围
handler.passive_ports = range(8300, 8500)

# 开启服务器
server = FTPServer(("127.0.0.1", 2121), handler)
server.serve_forever()

-----------输出-----------------

[I 2018-08-06 15:37:16] >>> starting FTP server on 127.0.0.1:2121, pid=8564 <<<
[I 2018-08-06 15:37:16] concurrency model: async
[I 2018-08-06 15:37:16] masquerade (NAT) address: None
[I 2018-08-06 15:37:16] passive ports: 8300->8499
[I 2018-08-06 15:37:32] 127.0.0.1:54898-[] FTP session opened (connect)

读取用户列表,建立FTP

#-----------user.ini------

[alex]
password=123
perm=elradfmwM
home=D:/

[egon]
password=123456
perm=elradfmwM
home=D:/


#------------ftpdemo

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler, ThrottledDTPHandler
from pyftpdlib.servers import FTPServer
import configparser
import logging

IP = '127.0.0.1'

PORT = '2121'

# 上传速度  100kb/s
MAX_UPLOAD = 100 * 1024

# 下载速度 100kb/s
MAX_DOWNLOAD = 100 * 1024

# 最大连接数
MAX_CONS = 100

# 最多IP数
MAX_PER_IP = 10

# 被动端口范围,注意被动端口数量要比最大IP数多,否则可能出现无法连接的情况
PASSIVE_PORTS = (8300, 8500)

# 是否开启匿名访问 on|off
ENABLE_ANONYMOUS = 'off'

# 匿名用户目录
ANONYMOUS_PATH = 'E:/DEVTOOL/'


# 日志文件
LOGING_NAME = 'pyftp.log'

# 欢迎信息
WELCOME_MSG = 'Welcome to my ftp'

# 新建一个用户组
authorizer = DummyAuthorizer()

# 读取用户配置
config = configparser.ConfigParser()
config.read('user.ini')
user_list = config.sections()
for user in user_list:
    passwd = config[user]["password"]
    perm = config[user]["perm"]
    home_dir = config[user]["home"]
    # 将用户名,密码,指定目录,权限 添加到里面
    authorizer.add_user(user, passwd, homedir=home_dir, perm=perm)

# 添加匿名用户 只需要路径
if ENABLE_ANONYMOUS == 'on':
    authorizer.add_anonymous(ANONYMOUS_PATH)

# 下载上传速度设置
dtp_handler = ThrottledDTPHandler
dtp_handler.read_limit = MAX_DOWNLOAD
dtp_handler.write_limit = MAX_UPLOAD

# 初始化ftp句柄
handler = FTPHandler
handler.authorizer = authorizer

# 添加被动端口范围
handler.passive_ports = range(PASSIVE_PORTS[0], PASSIVE_PORTS[1])


# 欢迎信息
handler.banner = WELCOME_MSG

# 监听ip 和 端口
server = FTPServer((IP, PORT), handler)

# 最大连接数
server.max_cons = MAX_CONS
server.max_cons_per_ip = MAX_PER_IP

# 开始服务
print('FTP开始服务 ', (IP, PORT))
server.serve_forever()

基于twisted

#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
  
"""a very simple ftp server by twisted  
"""  
  
__author__ = "Bobning(nb5550606@gmail.com)"  
  
  
import sys, os  
sys.path.append('../../..')  
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'  
  
from twisted.protocols import ftp  
from twisted.cred import portal, checkers, credentials, error as credError  
from twisted.internet import reactor, defer  
from zope.interface import implements  
from twisted.python import filepath  
from django.contrib.auth import authenticate  
from django.contrib.auth.models import User  
  
  
class UserChecker:  
    implements(checkers.ICredentialsChecker)  
    credentialInterfaces = (credentials.IUsernamePassword,)  
      
    def __init__(self):  
        "passwords: a dict-like object mapping usernames to passwords"  
  
      
    def requestAvatarId(self, credentials):  
        user = authenticate(username=credentials.username, password=credentials.password)  
        if user is not None:  
            if user.is_active:  
                return defer.succeed(credentials.username)  
            else:  
                return defer.fail(credError.UnauthorizedLogin("access denied"))  
        else:  
            return defer.fail(credError.UnauthorizedLogin("No such user"))  
  
  
class MyFTPRealm:  
    implements(portal.IRealm)  
      
    def __init__(self, anonymousRoot):  
        self.anonymousRoot = filepath.FilePath(anonymousRoot)  
      
    def requestAvatar(self, avatarId, mind, *interfaces):  
        for iface in interfaces:  
            if iface is ftp.IFTPShell:  
                if avatarId is checkers.ANONYMOUS:  
                    avatar = ftp.FTPAnonymousShell(self.anonymousRoot)  
                else:  
                    try:  
                        user = User.objects.get(username=avatarId)  
                        ftpdir = user.ftp.all()[0].ftpdir  
                    except:  
                        raise "没有该用户"  
                    avatar = ftp.FTPShell(filepath.FilePath(ftpdir.encode("utf-8")))  
                return ftp.IFTPShell, avatar, getattr(avatar, "logout", lambda:None)  
        raise NotImplementedError("only IFTPShell interface is supported by this realm")  
  
  
class MyFtpServer(ftp.FTP):  
    def __init__(self, *args, **kw):  
        super(ftp.FTP, self).__init__(*args, **kw)  
  
    def dataReceived(self, data):  
        self.transport.write(data)  
  
  
if __name__ == "__main__":  
    p = portal.Portal(MyFTPRealm(""))  
    p.registerChecker(UserChecker())  
  
    factory = ftp.FTPFactory(MyFtpServer())  
    factory.portal = p  
    reactor.listenTCP(2121, factory)  
    reactor.run()  

客户端实现

import ftplib
import sys
 
#获取服务器的ip地址(如192.168.1.107),使用sys.argv可以从命令行参数里面获取
if len(sys.argv) < 2:
    tmp = input("please input server address:")
    sys.argv.append(tmp)
server_address = sys.argv[1]
#创建FTP实例,并显示欢迎界面
ftp = ftplib.FTP(server_address)
print(ftp.getwelcome())
#登录,输入服务器里添加过的用户名和口令
ftp.login('user', 'pass')
 
#文件上传
def upload(fname):
    fd = open(fname, 'rb')
    new_name = input("input new name:")
    #以二进制的形式上传
    ftp.storbinary("STOR %s" % new_name, fd)
    fd.close()
    print("upload finished")
	
#文件下载
def download(fname):
    #构建文件的存储路径,这里用的是D盘,可以自行设置
    new_path = "D:\\FTPdownload\\" + fname
    fd = open(new_path, 'wb')
    #以二进制形式下载,注意第二个参数是fd.write,上传时是fd
    ftp.retrbinary("RETR %s" % fname, fd.write)
    fd.close()
    print("download finished")
 
def main():
    #选择操作,上传、下载、退出
    op = input("what do you want?(u/d/q)")
    if op == "u":
        #输入文件完整路径,必要时可以用绝对路径
        fname = input("input the file of path:")
        upload(fname)
    elif op == "d":
        fname = input("input the file name:")
        download(fname)
    else:
        print("quit now!")
    ftp.quit()
 
if __name__ == '__main__':
    main()

附带一些ftplib库的相关操作:

ftp.cwd(pathname)           # 设置FTP当前操作的路径
ftp.dir()                   # 显示目录下所有目录的信息
ftp.nlst()                  # 获取目录下的文件
ftp.mkd(pathname)           # 新建远程目录
ftp.rmd(dirname)            # 删除远程目录
ftp.pwd()                   # 返回当前所在位置
ftp.delete(filename)        # 删除远程文件
ftp.rename(old_name, new_name)    #将fromname改为toname
ftp.storbinary('STOR filename.txt',file_handel,[bufsize])  # 上传目标文件,最后一个参数可以不填
ftp。retrbinary('RETR filename.txt',file_handel,[bufsize])  # 下载FTP文件

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐