Python Получает HTTP-файл по ПОЧТЕ

#python #sockets #http #post

Вопрос:

Я пытаюсь создать веб-сервер Python, который может принимать файлы. Чтобы кто-то мог зайти на веб-сайт, нажмите кнопку «Загрузить» в форме, затем файл будет отправлен на сервер и сохранен локально на сервере.

Вот содержание index.html

 <form enctype="multipart/form-data" action="" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="8000000" />
    <input name="uploadedfile" type="file" /><br />
    <input type="submit" value="Upload File" />
</form>
 

Содержание Server.py

 import socket

class server():
    def __init__(self):
        self.host_ip = socket.gethostbyname(socket.gethostname())
        self.host_port = 81
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.data_recv_size = 1024

    def get_data(self, conn):
        """ gets the data from client """
        data = b""
        while b"rnrn" not in data:
            data  = conn.recv(self.data_recv_size)
        return data

    def server(self):
        """ main method starts the server """
        print(f"[ ] Server started listening on port {self.host_port}!")
        print(f"[ ] Server Ip: {self.host_ip}")
        self.s.bind((self.host_ip, self.host_port))
        self.s.listen()

        while True:
            conn, addr = self.s.accept()
            with conn:
                data = self.get_data(conn)
                
                # GET request
                if data[0:5] == b"GET /":
                    index = open("index.html", "rb").read()
                    conn.sendall(b"HTTP/1.0 200 OKnContent-Type: text/htmlnn"   index)
                    print("[ ] Responded to GET request")

                # POST request
                elif data[0:4] == b"POST":
                    with open("output.txt", "ab") as file:
                        file.write(data)
                        print(f"{len(data)} bytes received from post!")
                        conn.sendall(b"HTTP/1.0 200 OKrnContent-Type: text/html")

s = server()
s.server()
 

Часть GET сервера работает правильно, когда я посещаю веб-сайт, index.html файл отображается в моем веб-браузере, и я вижу форму загрузки файла.

РЕДАКТИРОВАТЬ: Я обновил форму до максимального размера файла 8 миллионов name="MAX_FILE_SIZE" value="8000000" , ответ на сообщение, который получает сервер, намного больше (я обновил его ниже), но по-прежнему не похоже, что он содержит содержимое файла.

 POST / HTTP/1.1
Host: 169.254.126.211:81
Connection: keep-alive
Content-Length: 2857323
Cache-Control: max-age=0
Origin: http://169.254.126.211:81
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjbf7KaGShYBQ75wT
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://169.254.126.211:81/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,ru;q=0.7

------WebKitFormBoundaryjbf7KaGShYBQ75wT
Content-Disposition: form-data; name="MAX_FILE_SIZE"

8000000
------WebKitFormBoundaryjbf7KaGShYBQ75wT
Content-Disposition: form-data; name="uploadedfile"; filename="IMG_20210131_165637.jpg"
Content-Type: image/jpeg

ÿØÿá„ÙExif  MM *         @      
°         ö       ¶       ¾POST / HTTP/1.1
Host: 169.254.126.211:81
Connection: keep-alive
Content-Length: 2857323
Cache-Control: max-age=0
Origin: http://169.254.126.211:81
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjbf7KaGShYBQ75wT
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://169.254.126.211:81/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,ru;q=0.7
 

Снимок экрана, показывающий вывод в режиме ожидания Python, когда я запускаю скрипт.

введите описание изображения здесь

Правка: В нем указано только 1024 байта, полученных от post!, поэтому похоже, что полный файл не отправляется.

Как отправить файл из веб-браузера по ПОЧТЕ и получить файл на сервере?

Комментарии:

1. Я думаю, вам нужно увеличить максимальный размер записи в вашей форме и размер data_recv_size в вашем сценарии. Длина содержимого отображается как 2804304 байта, хотя, вероятно, оно не будет сохранено из-за ограничений по размеру.

2. Где ты видишь 2804304 bytes ? Когда я запускаю сценарий, он печатает 674 bytes received from post!

3. Это в вашем ответе заголовка ( Content-Length: 2804304 ). Является ли файл, который вы пытаетесь загрузить, приблизительно 2,8 Мбайт?

4. Да, я пытаюсь загрузить фотографию размером 2,8 МБ, чтобы проверить, если server.py работает.

5. Попробуйте увеличить ограничения, установленные в вашем скрипте и форме загрузки.

Ответ №1:

С помощью ответа furas, пробной ошибки и множества исследований в Интернете. Мне удалось создать веб-сервер, который работает. Я собираюсь опубликовать завершенный сценарий здесь, так как в будущем он будет полезен другим людям, которые столкнутся с этим вопросом, а также попытаются создать сервер загрузки файлов.

 import socket, re

class Server():
    def __init__(self):
        self.host_ip = "localhost"
        self.host_port = 81
        self.s = socket.socket()
        self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.data_recv_size = 1024
        self.form = b"""<form enctype="multipart/form-data" action="" method="POST">
                <input type="hidden" name="MAX_FILE_SIZE" value="8000000" />
                <input name="uploadedfile" type="file" /><br />
                <input type="submit" value="Upload File" />
            </form>"""

    def get_data(self, conn):
        data = b""
        while True:
            chunk = conn.recv(self.data_recv_size)
            if len(chunk) < self.data_recv_size:
                return data   chunk
            else:
                data  = chunk

    def save_file(self, packet):
        name = re.compile(b'name="uploadedfile"; filename="(. )"').search(packet).group(1)
        data = re.compile(b"WebKitFormBoundary((n|.)*)Content-Type. n. ?n((n|.)*)([-] WebKitFormBoundary)?")
        with open(name, "wb") as file:
            file.write(data.search(packet).group(3))

    def run(self):
        print(f"[ ] Server: http://{self.host_ip}:{self.host_port}")
        self.s.bind((self.host_ip, self.host_port))
        self.s.listen()

        while True:
            conn,addr = self.s.accept()
            request = self.get_data(conn)

            # GET request
            if request[0:5] == b"GET /":
                conn.sendall(b"HTTP/1.0 200 OKnContent-Type: text/htmlnn" self.form)
                print("[ ] Responded to GET request!")

            # POST request
            elif request[0:4] == b"POST":
                packet = self.get_data(conn)
                self.save_file(packet)
                ok_reponse = b"Successfully upload %d bytes to the server!" % len(packet)
                conn.sendall(b"HTTP/1.0 200 OKrnContent-Type: text/htmlrnrn" ok_reponse)
                print(f"[ ] {len(packet)} bytes received from POST!")


s = Server()
s.run()
 

Вот скриншот, показывающий запуск сценария

введите описание изображения здесь

Вот скриншот каталога с сохраненным файлом изображения, после того как он был загружен через сервер.

введите описание изображения здесь

Так что теперь все, кажется, работает нормально.

Ответ №2:

В get_data() вы проверяете while b"rnrn" not in data: , чтобы вы читали только head с headers body опубликованным файлом, но не с ним.

Вы должны получить значение из заголовка Content-Length и использовать его для чтения остальных данных — body .

Но вы recv(1024) , возможно, уже прочитали какую-то часть body , и это может создать проблемы. Вы должны читать байт за байтом ( recv(1) ), пока не получите b"rnrn" , а затем использовать Content-Length для чтения остальных данных.


Минимальный рабочий код с HTML в коде, чтобы каждый мог просто скопировать и запустить его.

 import socket

class Server():  # PEP8: `CamelCaseName` for classes
    
    def __init__(self):
        #self.host_ip = socket.gethostbyname(socket.gethostname())
        self.host_ip = '0.0.0.0'  # universal IP for server - to connect from other computers
        self.host_port = 8000  # 81 was restricted on my computer

        #self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s = socket.socket()  # default values are `socket.AF_INET, socket.SOCK_STREAM`
        self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # solution for '[Error 89] Address already in use'. Use before bind()

        self.data_recv_size = 1024

    def get_head(self, conn):
        """ gets headers from client """
        data = b""
        while not data.endswith(b"rnrn"):
            data  = conn.recv(1)
        return data

    def get_body(self, conn, size):
        """ gets the data from client """
        data = b""
        while b"rnrn" not in data:
            data  = conn.recv(self.data_recv_size)
        return data

    def run(self):
        """ main method starts the server """
        print(f"[ ] Server: http://{self.host_ip}:{self.host_port}")
        self.s.bind((self.host_ip, self.host_port))
        self.s.listen()
        try:
            while True:
                conn, addr = self.s.accept()
                with conn:
                    head = self.get_head(conn)
                    
                    # todo: parse headers
                    lines = head.strip().splitlines()
                    request = lines[0]
                    headers = lines[1:]
                    headers = list(line.split(b': ') for line in headers)
                    #print(headers)
                    headers = dict(headers)
                    for key, value in headers.items():
                        print(f'{key.decode()}: {value.decode()}')
                    
                    # GET request
                    if request[0:5] == b"GET /":
                        #html = open("index.html", "rb").read()
                        
                        html = '''<form enctype="multipart/form-data" action="" method="POST">
                                    <input type="hidden" name="MAX_FILE_SIZE" value="8000000" />
                                    <input name="uploadedfile" type="file" /><br />
                                    <input type="submit" value="Upload File" />
                                </form>'''

                        conn.sendall(b"HTTP/1.0 200 OKnContent-Type: text/htmlnn"   html.encode())
                        print("[ ] Responded to GET request")
    
                    # POST request
                    elif request[0:4] == b"POST":
                        size = int(headers[b'Content-Length'].decode())
                        body = self.get_body(conn, size)
                        with open("output.txt", "ab") as file:
                            file.write(head)
                            file.write(body)
                        total_size = len(head) len(body)    
                        print(f"{total_size} bytes received from POST")
                        html = f"OK: {total_size} bytes"
                        conn.sendall(b"HTTP/1.0 200 OKrnContent-Type: text/htmlrnrn"   html.encode())
                        print("[ ] Responded to POST request")
    
        except KeyboardInterrupt:
            print("[ ] Stopped by Ctrl C")
        finally:
            self.s.close()

# --- main ---

s = Server()
s.run()
 

кстати:

Я отображаюсь http://0.0.0.0:8000 в консоли, потому что в некоторых консолях вы можете нажать на URL-адрес, чтобы открыть его в браузере.

Я использую универсальный адрес 0.0.0.0 , чтобы он мог получать соединения от всех NIC ( Network Internet Controller/Card ?), что означает LAN кабель WiFi и другие соединения одновременно.

PEP 8 — Руководство по стилю для кода на Python


Редактировать:

Это может быть намного проще с Flask

Я использую форму для загрузки сразу 3 файлов.

 import os
from flask import Flask, request

# create folder for uploaded data
FOLDER = 'uploaded'
os.makedirs(FOLDER, exist_ok=True)

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():

    if request.method == 'GET':
        return '''<form enctype="multipart/form-data" action="" method="POST">
    <input type="hidden" name="MAX_FILE_SIZE" value="8000000" />
    <input name="uploadedfile1" type="file" /><br />
    <input name="uploadedfile2" type="file" /><br />
    <input name="uploadedfile3" type="file" /><br />
    <input type="submit" value="Upload File" />
</form>'''
    
    if request.method == 'POST':
        for field, data in request.files.items():
            print('field:', field)
            print('filename:', data.filename)
            if data.filename:
                data.save(os.path.join(FOLDER, data.filename))
        return "OK"

if __name__ == '__main__':
    app.run(port=80)
 

Комментарии:

1. Спасибо за вашу помощь, у меня возникли проблемы с вашим кодом, так как он все еще не полностью получал весь файл. Но мне удалось исправить это, вычесав методы get_head и get_body, а затем используя оператор IF для проверки длины фрагмента. Я поместил заполненный код в отдельный ответ.

2. Мой код получает все запросы, но для отдельного сохранения файла ему все равно нужно проанализировать данные get_head . Это только показывает, что socket нужно много кода, и может быть проще написать его другими модулями — т. е. http или Flask

3. Я добавил пример, Flask чтобы показать , что это может быть намного проще с веб-фреймворками, такими как Flask , Bottle и т. Д.