Skip to content

Instantly share code, notes, and snippets.

@fabiand
Last active May 31, 2024 10:18
Show Gist options
  • Save fabiand/5628006 to your computer and use it in GitHub Desktop.
Save fabiand/5628006 to your computer and use it in GitHub Desktop.
A simple HTTP Server supporting put (python3)
# python3 -m SimpleHTTPPutServer 8080
from http.server import HTTPServer, SimpleHTTPRequestHandler
class PutHTTPRequestHandler(SimpleHTTPRequestHandler):
def do_PUT(self):
print(self.headers)
length = int(self.headers["Content-Length"])
path = self.translate_path(self.path)
with open(path, "wb") as dst:
dst.write(self.rfile.read(length))
self.send_response(200)
self.end_headers()
def run(server_class=HTTPServer, handler_class=PutHTTPRequestHandler):
server_address = ('', 8000)
httpd = server_class(server_address, handler_class)
httpd.serve_forever()
if __name__ == '__main__':
run()
@EricBuist
Copy link

Python 2 is end of life. Why not align to Python 3? If there is something built into Python 3 that could be used instead, would be worth mentioning this here.

@fabiand
Copy link
Author

fabiand commented May 25, 2022

Yes this would be great!

@orgads
Copy link

orgads commented Aug 4, 2022

Here, based on https://gist.github.com/mdonkers/63e115cc0c79b4f6b8b3a6b797e485c7

DISCLAIMER: I added protection for access outside the running directory, but I don't guarantee it cannot be bypassed.

#!/usr/bin/env python3
"""
Very simple HTTP file server in python
Usage::
    ./server.py [<port>]
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import mimetypes
import os
import pathlib

cwd = os.path.normcase(os.getcwd())

class S(BaseHTTPRequestHandler):
    def __init__(self, *args, directory=None, **kwargs):
        self.protocol_version = 'HTTP/1.1'
        super().__init__(*args, **kwargs)

    def _send_response(self, code = 200, msg='Done', content_type='text/plain'):
        body = msg.encode('utf-8')
        self.send_response(code)
        self.send_header('Content-type', content_type)
        self.send_header('Content-Length', len(body))
        self.end_headers()
        self.wfile.write(body)

    def req_path(self):
        fname = self.path[1:] # Strip leading slash
        path = os.path.normcase(os.path.dirname(os.path.realpath(fname)))
        if os.path.commonpath((path, cwd)) == cwd:
            return fname
        raise Exception("Access denied")

    def do_GET(self):
        try:
            with open(self.req_path(), "rb") as src:
                self._send_response(200, src.read(), mimetypes.guess_type(self.req_path()))
        except:
            self._send_response(404, '404 Not Found\r\n')

    def do_PUT(self):
        try:
            path = self.req_path()
            pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True)
            with open(path, "wb") as dst:
                content_length = int(self.headers['Content-Length'])
                dst.write(self.rfile.read(content_length))
            self._send_response(200, 'Done\r\n')
        except Exception as ex:
            print(ex)
            self._send_response(500, '500 Access Denied\r\n')

    def do_POST(self):
        return self.do_PUT()

def run(port=8000):
    server_address = ('', port)
    httpd = HTTPServer(server_address, S)
    print(f'Listening on port {port}')
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()

if __name__ == '__main__':
    from sys import argv

    if len(argv) == 2:
        run(port=int(argv[1]))
    else:
        run()

@janumejia
Copy link

Thanks @orgads. That is what I was looking for. It works fine for me.

@stefanstruik
Copy link

hello i have a question how can i make simpleHTTPServer work it give error when i use it give it print self.headers
^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
i use prython 3

@janumejia
Copy link

janumejia commented Nov 7, 2022

Hello @stefanstruik. Check the code that @orgads shared here. If you want to run the first code (of @fabiand ) you need to install python 2.7

@stefanstruik
Copy link

I can do you ok in a zip existence for me I still have it for my education

@stefanstruik
Copy link

het works thanks jou wel

@fabiand
Copy link
Author

fabiand commented Nov 8, 2022

Crazy how old this snippet is by now.

@stefanstruik
Copy link

stefanstruik commented Nov 22, 2022

help i have a question how can i make simpleHTTPServer work it give error when i use it give python: can't open file SimpleHTTPPutServer': [Errno 2] No such file or directory i use Python 3.10.8
gr stefan

@TijlE-1951707
Copy link

TijlE-1951707 commented Mar 27, 2024

I made a few changes to @orgads' implementation --- thank you! --- that suit my needs. The GET method didn't work, because the file content was already binary while _send_response expected the message to be still a string. If you want to read it binary for optimal efficiency, just edit _send_response instead.

Also, I used it in a web-browser, where multiple files where being retrieved. It made me wait for 2 minutes before axios fetched it and I never saw it in the log. This is because the non-threaded version of the previous authors will simply not see the request coming in and ignore it, until axios tries another time (apparently, that is after 2 minutes). I solved it by using a threaded server from the same http.server library.

The disclaimer still holds:

DISCLAIMER: I added protection for access outside the running directory, but I don't guarantee it cannot be bypassed.

I recommend looking at the script --- it's not hard. But for those who don't have the time, the directory being served is expected to be the current working directory of the script (cwd = os.path.normcase(os.getcwd())), so either move to the directory and execute the script from there, or configure it so you can pass a parameter and share your updated version here ;-) I did the former with one of my scripts ("serve": "cd data; python3 ./server.py 8756")

#!/usr/bin/env python3
"""
Very simple HTTP file server in python
Usage::
    ./server.py [<port>]
Authors::
    fabiand, orgads, TijlE-1951707 (https://gist.github.com/fabiand/5628006)
"""
from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer
import mimetypes
import os
import pathlib

# # https://stackoverflow.com/questions/62599036/python-requests-is-slow-and-takes-very-long-to-complete-http-or-https-request
# import logging
# import http.server
# BaseHTTPRequestHandler=http.server.BaseHTTPRequestHandler
# HTTPServer=http.server.HTTPServer
# http.server.BaseHTTPRequestHandler.debuglevel = 1
# http.server.HTTPServer.debuglevel = 1
# logging.basicConfig()
# logging.getLogger().setLevel(logging.DEBUG)

cwd = os.path.normcase(os.getcwd())
print(cwd)

class S(BaseHTTPRequestHandler):
    def __init__(self, *args, directory=None, **kwargs):
        self.protocol_version = 'HTTP/1.1'
        super().__init__(*args, **kwargs)

    def _send_response(self, code = 200, msg='Done', content_type='text/plain'):
        body = msg.encode('utf-8')
        self.send_response(code)
        self.send_header('Content-type', content_type)
        self.send_header('Content-Length', len(body))
        # self.send_header('Keep-Alive', "timeout=5")
        # self.send_header('Connection', "keep-alive")
        self.send_header('Cache-Control', "no-store") # seemed to be necessary to GET through axios, and added in GET through Insomnia
        # self.send_header('Cache-Control', "no-cache, no-store, must-revalidate") # is all that Insomnia added to custom requests
        self.end_headers()
        self.wfile.write(body)

    def req_path(self):
        fname = self.path[1:] # Strip leading slash
        path = os.path.normcase(os.path.dirname(os.path.realpath(fname)))
        if os.path.commonpath((path, cwd)) == cwd:
            return fname.replace("%20", " ")
        raise Exception("Access denied")

    def do_GET(self):
        try:
            path = self.req_path()
            mime_type = mimetypes.guess_type(self.req_path())
            content_type = ('text/plain' if (t:= mime_type[0]) == None else t) + ('' if (e := mime_type[1]) == None else f"; charset={e}")
            with open(path, "r") as src:
                self._send_response(200, src.read(), content_type)
        except FileNotFoundError as ex:
            self._send_response(404, '404 Not Found\r\n')
        except BrokenPipeError as ex:
            print("client disconnected")
            print(ex)
        except Exception as ex:
            print(ex, type(ex))
            self._send_response(500, 'Internal server error\r\n')

    def do_PUT(self):
        try:
            path = self.req_path()
            pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True)
            with open(path, "wb") as dst:
                content_length = int(self.headers['Content-Length'])
                dst.write(self.rfile.read(content_length))
            self._send_response(200, 'Done\r\n')
        except Exception as ex:
            print(ex)
            self._send_response(403, '403 Access Denied\r\n')

    # def do_POST(self):
    #     print("infinite loop?")
    #     return self.do_PUT()

def run(port=8000):
    server_address = ('', port)
    # httpd = HTTPServer(server_address, S) # WARNING: if the server is busy, it will completely miss incoming requests!
    httpd = ThreadingHTTPServer(server_address, S)
    print(f'Listening on port {port}')
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()

if __name__ == '__main__':
    from sys import argv

    if len(argv) == 2:
        run(port=int(argv[1]))
    else:
        run()

@fabiand
Copy link
Author

fabiand commented May 31, 2024

I've updated the gist to stay minimal, but be python3 based.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment