Qasim Ali   About  Archives  Links

Apr 23, 2019.

DIY image hosting

Please don’t use this for a real site. This script doesn’t do any sort of file validation. It’s possible for someone to rename a .exe to .jpg, upload it, and then use the site for malware distribution.

I spent a couple of hours throwing together a quick Python HTTP server that I can use to run image hosting on this machine. It’s super ugly and not at all responsive to errors, so I have systemd just restart the script on failure. On the plus side, deployment is just a git clone. The script is running behind Nginx so I don’t have to worry about SSL here. I’m using basic auth to prevent everyone else from uploading images. Any non-image requests get proxied to the script, while image requests get a cache header added and are served normally:

server {
	listen 443 ssl http2;
	listen [::]:443 http2;
	server_name i.qasima.li;
	root /var/www/i.qasima.li;

	ssl_certificate     /etc/letsencrypt/live/i.qasima.li/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/i.qasima.li/privkey.pem;

	client_max_body_size 50m;

	location / {
		auth_basic "Image upload";
		auth_basic_user_file /etc/nginx/i.qasima.li.htpasswd;

		proxy_pass http://127.0.0.1:8000;
	}

	location ~* .*\.(jpe?g|png|gif|tiff?|webp)$ {
		access_log off;
		add_header Cache-Control "public, max-age=86400";
	}
}

Since nginx is the one serving files, the script responds to all GETs with an upload page. On POST, we read out the uploaded image’s mime type so we know what to save it as. Then the script just copies the image to the resulting directory.

#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
from io import BytesIO


class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    homepage = b"""
	<!DOCTYPE html>
	<html><head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
        <body style="padding: 10px;">
            <form action="/" enctype="multipart/form-data" method="post">
                <div style="margin:10px auto"><input type="file" accept="image/*" name="i" style="font-size: 16px;"></div>
                <div><input type="submit" value="Upload" style="width: 100%;height: 60px;font-size: 23px;"></div>
            </form>
        
    </body></html>"""

    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(self.homepage)

    def do_POST(self):
        content_type = self.headers["Content-Type"]
        if not content_type.startswith("multipart/form-data"):
            self.send_response(400)
            self.end_headers()
            response = BytesIO()

            response.write(
                bytes("Wrong content type " + content_type, encoding="utf-8")
            )
            self.wfile.write(response.getvalue())
            return

        content_length = int(self.headers["Content-Length"])
        body = self.rfile.read(content_length)
        lines = body.split(b"\r\n")

        boundary = lines[0]
        file_content_type = lines[2]
        lead_up = "Content-Type: image/"
        ftype = str(file_content_type[len(lead_up) :], encoding="utf-8")

        import random, string

        target_filename = (
            "".join(
                random.choice(string.ascii_lowercase + string.digits) for _ in range(5)
            )
            + "."
            + ftype
        )

        end_boundary_start = body.rfind(boundary) - len(b"\r\n")
        file_start = len(b"\r\n".join(lines[0:3])) + 2 * len(b"\r\n")

        with open("/var/www/i.qasima.li/" + target_filename, "wb") as output:
            output.write(body[file_start:end_boundary_start])

        self.send_response(301)
        self.send_header('Location', '/' + target_filename)

        self.end_headers()
        self.wfile.write(b'')


if __name__ == "__main__":
    httpd = HTTPServer(("localhost", 8000), SimpleHTTPRequestHandler)
    httpd.serve_forever()

The systemd unit file is as barebones as can be:

[Unit]
Description=Runs /var/www/img/main.py for i.qasima.li
After=network.target

[Service]
Type=simple
User=www-data
ExecStart=/usr/bin/python3 /var/www/img/main.py
Restart=on-failure

[Install]
WantedBy=multi-user.target

It’s super janky, but I think it’s all I’ll need for now. Cleanup can be done in shell:

#!/usr/bin/env sh

# delete >= 30d old files
find /var/www/i.qasima.li/ -mtime +30 -exec rm {} \;