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

 

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