# Minimal HTTP server # # Usage: # # from httpserver import HTTPServer, sendfile, CONNECTION_CLOSE # # app = HTTPServer() # @app.route("GET", "/") # def root(conn, request): # response = HTTPResponse(200, "text/html", close=True) # response.send(conn) # sendfile(conn, "index.html") # # app.start() # # Handlers for the (method, path) combinations must be decorated with @route, # and declared before the server is started (via a call to start). # Every handler receives the connection socket and an object with all the # details from the request (see url.py for exact content). The handler must # construct and send a correct HTTP response. To avoid typos use the # HTTPResponse component from response.py. # When leaving the handler the connection will be closed, unless the return # code of the handler is CONNECTION_KEEP_ALIVE. # Any (method, path) combination which has not been declared using @route # will, when received by the server, result in a 404 HTTP error. # The server cannot be stopped unless an alert is raised. A KeyboardInterrupt # will cause a controlled exit. # # Copyright 2021 (c) Erik de Lange # Released under MIT license import errno import socket from micropython import const from .response import HTTPResponse from .url import HTTPRequest, InvalidRequest CONNECTION_CLOSE = const(0) CONNECTION_KEEP_ALIVE = const(1) class HTTPServerError(Exception): pass class HTTPServer: def __init__(self, host="0.0.0.0", port=80, backlog=5, timeout=30): self.host = host self.port = port self.backlog = backlog self.timeout = timeout self._routes = dict() # stores link between (method, path) and function to execute def route(self, method="GET", path="/"): """ Decorator which connects method and path to the decorated function. """ if (method, path) in self._routes: raise HTTPServerError(f"route{(method, path)} already registered") def wrapper(function): self._routes[(method, path)] = function return wrapper def start(self): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((self.host, self.port)) server.listen(self.backlog) print(f"HTTP server started on {self.host}:{self.port}") while True: try: conn, addr = server.accept() conn.settimeout(self.timeout) request_line = conn.readline() if request_line is None: raise OSError(errno.ETIMEDOUT) if request_line in [b"", b"\r\n"]: print(f"empty request line from {addr[0]}") conn.close() continue print(f"request line {request_line} from {addr[0]}") try: request = HTTPRequest(request_line) except InvalidRequest as e: while True: # read and discard header fields line = conn.readline() if line is None: raise OSError(errno.ETIMEDOUT) if line in [b"", b"\r\n"]: break HTTPResponse(conn, 400, "HTML", close=True).send_file() conn.write(repr(e).encode("utf-8")) conn.close() continue while True: # read header fields and add name / value to dict 'header' line = conn.readline() if line is None: raise OSError(errno.ETIMEDOUT) if line in [b"", b"\r\n"]: break else: if line.find(b":") != 1: name, value = line.split(b':', 1) request.header[name] = value.strip() # search function which is connected to (method, path) func = self._routes.get((request.method, request.path)) if func: if func(conn, request) != CONNECTION_KEEP_ALIVE: # close connection unless explicitly kept alive conn.close() else: # no function found for (method, path) combination response = HTTPResponse(conn, 404).send_file() conn.close() except KeyboardInterrupt: # will stop the server conn.close() break except Exception as e: conn.close() if type(e) is OSError and e.errno == errno.ETIMEDOUT: # communication timeout pass elif type(e) is OSError and e.errno == errno.ECONNRESET: # client reset the connection pass else: server.close() raise e server.close() print("HTTP server stopped")