FTP实验报告 

注:本文为计算机网络原理课程实验的实验报告,实验内容为实现一个简单的FTP服务器,程序已开源:https://github.com/jzh14/ftpd

  1. 概述

    本程序实现了一个符合RFC 959的FTP服务器和配套的客户端,其中FTP服务器支持主动和被动模式,支持多用户同时上传/下载,可以和主流FTP客户端(如Filezilla)正常交互(上传、下载、重命名、删除、创建目录、删除目录、切换目录等),支持使用配置文件对用户名、密码、路径、欢迎信息进行设置,并在安全性上进行了比较充分的测试和针对性的加固。

    FTP客户端仿照Windows内置的ftp命令实现,提供了和FTP服务器正常交互所需的功能(上传、下载、重命名、删除、创建目录、删除目录、切换目录等)。

    FTP服务器支持的FTP命令:APPE CWD DELE HELP LIST MKD NLST NOOP OPTS PASS PASV PORT PWD QUIT RNTR RMD RNFR RNTO STOR SYST TYPE USER XPWD。

    FTP客户端支持的FTP命令:USER PASS HELP QUIT CWD PWD PUT PORT STOR RETR LIST MKD RMD DELE,并可使用LITERAL命令发送任意FTP命令到FTP服务器。

    以下主要针对服务器部分加以说明,因为服务器技术上的难度明显高于客户端,结构复杂得多,而且也有更多需要注意的细节。客户端功能仿照Windows内置ftp命令,实现细节可参看源码。

    本程序的全部源码可在此处下载:

    https://jinzihao.me/wp-content/uploads/2016/12/ftpd.tar.gz

  1. 技术架构

    FTP服务器主体为一个循环,使用fd_set维护当前活跃的socket集合,使用select()函数查找需要处理的socket。对于需要处理的socket分三种情况:客户端建立命令连接、客户端建立数据连接、客户端发来命令,以下分别讨论:

    1. 客户端建立命令连接

      只需接受连接并将socket加入命令socket对应的集合。

    2. 客户端建立数据连接

      又分为主动模式等待发送、主动模式等待接收、被动模式等待发送、被动模式等待接收四种情况:

      对于两种等待接收的情况,根据之前接收STOR命令时记录下的上传文件名,打开对应的文件,开始从网络上读取并写入到文件。

      主动模式等待发送不需要特殊的处理,因为服务器在和一个主动模式的客户端交互时一旦收到了RETR命令,就会立即和客户端建立连接,并开始传输。

      被动模式等待发送需要一些特殊处理,因为服务器端收到RETR命令后,不能立即发送数据,而是要等到客户端建立了数据连接之后才能发送,因此需要将待发送的文件缓存下来,等到进入了被动模式等待发送这个状态,再将缓存的数据发送给客户端。

    3. 客户端发来命令

      由于TCP是流不是包,不能保证每次收到的恰好是一条命令(有可能是半条、两条、三条或是更多条)。将用户发来的命令在换行符处切开,将已经完整接收到的命令加入队列。接下来处理队列中待处理的命令,共23种情况,以下分别介绍:

      1. OPTS

        不起实际作用,仅为应对部分FTP客户端建立连接后发送的OPTS UTF8命令。

      2. USER

        如果收到了用户名,则将该客户端对应的状态设为等待密码状态。

      3. PASS

        如果收到了密码,则和启动时读入到内存中的用户名密码对应表进行比对,如果匹配则将该客户端对应的状态设为已连接状态。特别地,anonymous用户的密码可以为任意值,是否允许anonymous用户登录在配置文件中指定。

      4. QUIT

        向客户端发送goodbye信息,断开连接。

      5. PORT

        如果客户端发来的IP地址和端口有效,则和客户端发来的IP地址和端口建立连接,并将该连接加入对应的fd_set。

      6. PASV

        创建一个新的监听socket,等待客户端连接,并向客户端发送服务器IP地址和新建的监听socket的端口号。

      7. XPWD

        发送该客户端的当前路径。

      8. PWD

        发送该客户端的当前路径。

      9. NLST

        如果客户端指定了路径,则列出以该用户的根目录为基准的,该路径下的文件。如果客户端没有指定参数,则发送该客户端当前路径下的文件列表,以换行符分隔(类似ls)。

      10. LIST

        类似NLST,但发送的内容还包括文件大小、修改时间、权限等信息(类似ls –l)。

      11. CWD

        如果客户端指定了路径,则切换到以用户的根目录为基准的该路径。

      12. HELP

        列出服务器支持的所有FTP命令。

      13. NOOP

        空指令,仅发送200 NOOP ok.

      14. SYST

        发送 215 UNIX Type: L8

      15. TYPE

        对TYPE I和TYPE A做出相应,但不起实际作用,实际使用的永远是二进制模式(即TYPE I)。

      16. RETR

        如果客户端已经进入了PORT或PASV模式,而且客户端指定了要下载的文件名,而且文件存在,则分主动模式和被动模式两种情况分别处理:

        如果是主动模式,则建立一个新进程,在新进程中立刻开始发送数据。

        如果是被动模式,需要分客户端是否已经建立了数据连接两种情况分别处理:如果客户端已经建立了数据连接,则直接建立新进程并开始发送;如果客户端尚未建立数据连接,则将待发送的文件缓存起来,等待客户端建立连接时再发送,其后的过程在前文”客户端建立数据连接”小节中有所介绍。

      17. STOR

        如果客户端已经进入了PORT或PASV模式,而且客户端指定了要上传的文件名,则将客户端请求上传的文件打开,并将对应的ofstream保存起来,等待客户端实际建立了数据连接之后再开始写入。

      18. APPE

        类似STOR,只不过打开文件时需要加入ios::app参数指定append模式。

      19. DELE

        如果客户端指定了要删除的文件名,且该文件存在,则调用remove函数删除文件并发回删除成功消息;如果客户端未指明要删除的文件名,或该文件不存在,则发挥删除失败消息。

      20. MKD

        如果客户端指定了要建立的文件夹名,则调用mkdir函数建立文件夹,如果建立成功则发回成功消息;如果未指明文件夹名或建立文件夹失败,则发回失败消息。

      21. RMD

        如果客户端指定了要删除的文件夹名,则调用rmdir函数删除文件夹,如果删除成功则发回成功消息;如果未指明文件夹名或删除文件夹失败,则发回失败消息。

      22. RNFR

        如果客户端指定了重命名的源文件,且源文件存在,则记录在该客户端对应的状态信息里,否则发送失败消息。

      23. RNTO

        如果客户端指定了重命名的目标文件,且之前已经成功执行过RNFR命令,则调用rename函数执行重命名,如果成功则发送成功消息,否则发送失败消息;如果之前未执行过RNFR命令则发送对应的失败消息。

  2. 一些细节
    1. 在utils.h中实现了一组通用的操作文件路径的函数,核心功能是给定根路径、当前路径和新路径的情况下进行路径切换的函数,可以对越界访问(超出用户的根路径)的情况进行比较好的限制。
    2. lib_ini.cpp为一个开源的INI Parser,利用它实现了配置文件的读取,从而避免了对配置信息硬编码。
    3. 在对用户命令的处理中,每一条命令(除去USER、PASS、OPTS、SYST等少数几条)都先判断了用户是否登录,从而避免了用户认证绕过。
  1. 思考题
    1. 在FTP中,为什么要建立两个TCP连接来分别传送命令和数据。

      如果只使用一个连接,发送的数据里可能混有命令(例如PWD、CWD),使得区分命令和数据变得困难。尽管可以使用转义序列的方式对数据中出现的命令加以转义,但也会造成效率降低和额外的带宽开销。而且FTP实现多种多样,对于什么需要转义这个问题,不同的服务器和客户端理解会有不一致的地方,一旦出现理解不一致就会导致传输过程中发生意外,从而大大降低了FTP协议的兼容性和实现的简便性。将命令和数据分为两个连接,大大简化了实现,避免了很多特殊判断,可以说是简洁优雅的设计。

    2. 主动方式和被动方式的主要区别是什么,为何要设计这两种方式。

      主动方式是服务器向客户端发起数据连接,而被动模式是客户端向服务器发起数据连接。这两种方式主要是应对服务器或客户端(实际上主要是客户端)可能在防火墙内,可以连接外网但无法接受外网连接这种情况。对于客户端处在内网的情况(例如通过使用NAT的路由器联网),被动方式是使用FTP唯一可行的选项。

    3. 当使用FTP下载大量小文件的时候,速度会很慢,这是什么缘故,可以怎样改进。

      通过FTP下载一个文件的开销比较大,需要先发送一个PORT或PASV命令,再发送一个RETR命令,再建立一个连接,然后才能开始下载。对于大量小文件的情况,开始下载时需要的这些操作耗时相当可观,实际传输耗时反倒占比较小。如果改进的话,可以新增一个批量传输命令,服务器端将大量小文件打包发送,客户端收到后再解包。(实际使用FTP时,对于大量小文件,确实往往是先手动压缩成zip或tar.gz再上传或下载,然后再手动解压缩。)

  1. 一点感想

    首先要说这个实验设计的相当好,能比较完整地体验一个真实的服务器是如何开发的,难易适中,而且还有很多可以拓展的地方。而且最后实现了一个实用的软件,可以和现有的FTP客户端兼容,而且这套框架也很容易套用到其他C/S架构的应用程序,这一点也是相当实用的。参照RFC实现一个协议,也是很值得体验的过程,在以后的工作中也是非常实用的技能。总之,这是一个相当实用的作业,在这个实验上花些时间非常值得。

    另外要感谢帮忙测试的肖元安、王瑞康两位学弟和黄昱恺学长,通过黑盒测试一共发现了FTP服务器2处安全漏洞和2处性能上的瓶颈,也提出了相当有帮助的建议,可以说在网络安全方面给我上了一课,非常感谢~也要感谢助教们在最后一周加班加点检查同学们的大实验,还能保持相当的耐心,还能给我足够的时间演示实现效果,为助教们点个赞~