200行代码实现玩具版FTP服务

上个周,老张写了一篇文章《吃透FTP》(没看过的同学可以先点击浏览一下)。文章主要介绍了FTP的工作原理,写完之后觉得不过瘾,自己动手实现了一个玩具版的FTP服务。

 

当然,如果实现一个完整稳定的FTP服务,工作量还是相当庞大的。所以老张选择了利用Python实现一个玩具版来过过瘾,写完发现仅有200行代码。

 

所谓玩具版,就是说:

  • 用户登录。使用预制的账号root,并没有使用系统账号

  • 仅支持主动模式。

  • 仅支持Binary模式。

  • 仅支持文件的上传和下载。

  • 单线程。

 

Talk is cheap,直接看代码。

#coding=utf-8

# FtpServer.py
# 一个玩具版的Ftp服务
# by 魔笛手CTO

import socket
import os
import six


END_FLAG = "\r\n"
ASCII_MODE = "II"
BINARY_MODE = "I"


def dump(string):
    """将字符串消息dump为网络序的字节"""
    if six.PY2:
        return string
    return bytes(string, "utf-8")


def load(byte):
    """将字节消息load为字符串"""
    if six.PY2:
        return byte
    return str(byte, "utf-8")


class FtpServer():
    def __init__(self):
        self.cmd_socket = None
        self.ftp_users = {"root": "root"}  # 允许登录的ftp账号密码

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """确保服务关闭"""
        try:
            self.cmd_socket.close()
            print("socket is closed")
        except:
            pass

    def run(self):
        """启动服务,开启21端口监听"""
        print("starting server on port 21...")
        self.cmd_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
        self.cmd_socket.bind(("0.0.0.0", 21))
        self.cmd_socket.listen(1)
        while True:
            conn, addr = self.cmd_socket.accept()
            self._handle(conn, addr)

    def _close_conn(self, conn):
        """关闭指定连接"""
        conn.close()

    def _handle(self, conn, addr):
        """一旦同客户端建立连接,将有handle负责处理交互"""
        user = None
        password = None
        authed = False
        client_data_addr = None

        self._say_hello(conn)
        while True:
            req = self._read_req(conn)
            # 返回空字符串时,关闭连接,准备响应下一个连接
            if req == "":
                return self._close_conn(conn)
            # 解析并响应客户端命令
            cmd, arg = self._parse(req)
            if cmd == "USER":
                if not authed:
                    user = arg
                    resp = "331 Please specify the password"
                else:
                    resp = "500 User has authed!"
            elif cmd == "PASS":
                if user and not authed:
                    password = arg
                    if self._auth(user, password):
                        authed = True
                        resp = "230 Login successful"
                    else:
                        resp = "500 Auth error"
                else:
                    resp = "500 User is not specified or has login"
            # binary模式和ascii模式, 当前仅支持binary模式
            elif cmd == "TYPE":
                if arg == ASCII_MODE:
                    resp = "500 Only support binary mode"
                elif arg == BINARY_MODE:
                    resp = "200 Switching to binary mode"
            # 主动模式下客户端的端口号
            elif cmd == "PORT":
                if not authed:
                    resp = "530 Not login"
                else:
                    client_data_addr = self._parse_addr(arg)
                    resp = "200 PORT command successful"
            # 上传文件
            elif cmd == "STOR":
                if not authed:
                    resp = "530 Not login"
                else:
                    resp = "150 Ok to send data"
                    self._send_resp(conn, resp)
                    self._save_file(arg, client_data_addr)
                    resp = "226 Transfer complete"
            # 下载文件
            elif cmd == "RETR":
                if not authed:
                    resp = "530 Not login"
                else:
                    if not os.path.exists(arg):
                        resp = "550 File not exist"
                    else:
                        resp = "150 Ok to send data"
                        self._send_resp(conn, resp)
                        self._send_file(arg, client_data_addr)
                        resp = "226 Transfer complete"
            else:
                print("500 Unknown command")

            # 发送响应
            self._send_resp(conn, resp)

    def _create_data_conn(self, host, port):
        """主动模式下建立数据通道"""
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(("0.0.0.0", 20))
        sock.connect((host, port))
        return sock

    def _send_file(self, filename, client_data_add):
        """传输指定文件至客户端"""
        conn = self._create_data_conn(*client_data_add)
        with open(filename, "rb") as f:
            conn.sendall(f.read())
        conn.close()

    def _save_file(self, filename, client_data_addr):
        """保存客户端上传的文件"""
        conn = self._create_data_conn(*client_data_addr)

        with open(filename, "wb") as f:
            while True:
                body = conn.recv(1)
                if body == b'':
                    break
                f.write(body)

        conn.close()


    def _read_req(self, conn):
        """读取请求消息"""
        print("reading msg...")
        msg = ""
        while True:
            body = load(conn.recv(1))
            # 当客户端关闭连接时,body为空字符串
            if body == "":
                return body
            msg += body
            if msg.endswith(END_FLAG):
                break
        return msg

    def _send_resp(self, conn, msg):
        """发送命令响应"""
        print("ready to response:%s" % msg)
        if not msg.endswith(END_FLAG):
            msg += END_FLAG
        conn.sendall(dump(msg))

    def _auth(self, user, password):
        """登录用户认证"""
        if user and self.ftp_users.get(user) and self.ftp_users.get(user) == password:
            return True
        return False

    def _say_hello(self, conn):
        """发送欢迎语"""
        self._send_resp(conn, "220 Hello!")

    def _parse(self, msg):
        """解析客户端消息, 返回命令和参数"""
        print("receive msg:%s" % msg)
        msg = msg.strip()
        args = msg.split(" ")
        if len(args) == 2:
            cmd, arg = args
            return cmd, arg
        return None, None

    def _parse_addr(self, addr):
        """解析ip和端口号"""
        args = addr.strip().split(",")
        host = ".".join(args[:4])
        port = int(args[4]) * 256 + int(args[5])
        return host, port


if __name__ == "__main__":
    with FtpServer() as server:
        server.run()

 

然后为了验证程序是否能够正常工作,老张使用Python自带的ftplib来测试服务是否可用。

#coding=utf-8

import ftplib


# 登录FTP服务,使用主动模式
ftp = ftplib.FTP()
ftp.connect("127.0.0.1", 21)
ftp.login("root", "root")
ftp.set_pasv(False)

# 将本地文件上传至服务器
with open("client" , 'rb') as f:
    ftp.storbinary("STOR upload_from_client", f, 1024)

# 下载服务器文件
with open("download_from_server", "wb") as f:
    ftp.retrbinary("RETR server", f.write)

最后,如果有同学对老张的玩具版FTP感兴趣,老张把代码上传到GitHub了,可以点击https://github.com/niujiacun/FtpServer

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章