2

I'm trying to create a Python web server that can receive files. So someone can vist the website, click the upload button on the form, then the file will be sent to the server and stored locally on the server.

Here is the contents of 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>

Contents of 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"\r\n\r\n" 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 OK\nContent-Type: text/html\n\n" + 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 OK\r\nContent-Type: text/html")

s = server()
s.server()

The GET part of the server works correctly, when I visit the website the index.html file is display in my web browser and I can see the file upload form.

EDIT: I updated the form to max file size 8 million name="MAX_FILE_SIZE" value="8000000", The POST response the server receives is much bigger (I updated it below), but it still doesn't look like it contains the file contents.

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

Screenshot showing the output in Python IDLE when I run the script.

enter image description here

Edit: It only says 1024 bytes received from post!, so it looks like the full file isn't being sent.

How do I sent a file from a web browser via POST, and receive the file on the server?

7
  • I think you need to increase the max post size on your form and data_recv_size on your script. The content length shows as 2804304 bytes, although probably won't save it because of the size limitations. Commented Jul 24, 2021 at 20:18
  • Where do you see 2804304 bytes? When I run the script it prints 674 bytes received from post! Commented Jul 24, 2021 at 20:22
  • It's in your header response (Content-Length: 2804304). Is the file you are attempting to upload approximately 2.8 MB? Commented Jul 24, 2021 at 20:26
  • Yes, im trying to upload a 2.8MB photo to test if server.py works. Commented Jul 24, 2021 at 20:28
  • Try increasing the limits that are set within your script and upload form. Commented Jul 24, 2021 at 20:28

2 Answers 2

1

In get_data() you check while b"\r\n\r\n" not in data: so you read only head with headers but not body with posted file.

You have to get value from header Content-Length and use it to read rest of data - body.

But your recv(1024) may already read some part of body and it can make problem. You should read byte-after-byte (recv(1)) until you get b"\r\n\r\n" and later use Content-Length to read rest of data.


Minimal working code with HTML in code so everyone can simply copy and run it.

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"\r\n\r\n"):
            data += conn.recv(1)
        return data

    def get_body(self, conn, size):
        """ gets the data from client """
        data = b""
        while b"\r\n\r\n" 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 OK\nContent-Type: text/html\n\n" + 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 OK\r\nContent-Type: text/html\r\n\r\n" + html.encode())
                        print("[+] Responded to POST request")
    
        except KeyboardInterrupt:
            print("[+] Stopped by Ctrl+C")
        finally:
            self.s.close()

# --- main ---

s = Server()
s.run()

BTW:

I display http://0.0.0.0:8000 in console because in some consoles you can click on URL to open it in browser.

I use universal address 0.0.0.0 so it can recive connections from all NIC (Network Internet Controller/Card?) which means LAN cable, WiFi and other connections at the same time.

PEP 8 -- Style Guide for Python Code


EDIT:

It can be much simpler with Flask

I use form for uploading 3 files at once.

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)
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for your help, I was having issues with your code as it still wasn't fully receiving the entire file. But I managed to fix it by combing the get_head and get_body methods, then use an IF statement to check the length of a chunk. I put the completed code in a separate answer.
My code receive all request but to save file separatelly it still need to parse data from get_head. It only shows that socket need a lot of code and it can be simpler to write it other modules - i.e. http or Flask
I added example in Flask to show that it can be much simpler with web frameworks like Flask, Bottle, etc.
0

With the help of furas answer, trial an error, and lots of research online. I was able to create a web server that works. I'm going to post the completed script here, as it will be of use to other people in the future who stumbles across this question, and are also trying to create a file upload server as well.

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 OK\nContent-Type: text/html\n\n"+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 OK\r\nContent-Type: text/html\r\n\r\n"+ok_reponse)
                print(f"[+] {len(packet)} bytes received from POST!")


s = Server()
s.run()

Here is a screenshot showing the script running

enter image description here

Here is a screenshot of the directory with the image file saved, after it was uploaded via the server.

enter image description here

So it all seems to be working OK now.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.