This commit is contained in:
2026-09-24 21:30:37 +03:00
parent 0a1eb51bf8
commit 6210b46c1f
35 changed files with 4012 additions and 0 deletions
@@ -0,0 +1,10 @@
FROM python:3.12-slim
WORKDIR /http/server
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
ENTRYPOINT ["python3", "server.py"]
@@ -0,0 +1,124 @@
import dataclasses
import typing as t
@dataclasses.dataclass
class HTTPRequest:
method: str
path: str
version: str
parameters: t.Dict[str, str]
headers: t.Dict[str, str]
@staticmethod
def from_bytes(data: bytes) -> "HTTPRequest":
# data contains the request line and headers, including the final CRLF.
# Read the body separately in HTTPHandler; it may exceed available RAM.
# Query parameters are not required: parameters can be an empty dict.
# TODO: Parse the request line and headers.
pass
@dataclasses.dataclass
class HTTPResponse:
version: str
status: str
headers: t.Dict[str, str]
def to_bytes(self) -> bytes:
# Return the status line and headers, ending with an empty line.
# HTTPHandler sends the body separately.
# TODO: Serialize the response headers.
pass
# Common HTTP strings and constants
CR = b'\r'
LF = b'\n'
CRLF = CR + LF
HTTP_VERSION = "1.1"
OPTIONS = 'OPTIONS'
GET = 'GET'
HEAD = 'HEAD'
POST = 'POST'
PUT = 'PUT'
DELETE = 'DELETE'
METHODS = [
OPTIONS,
GET,
HEAD,
POST,
PUT,
DELETE,
]
# Only GET, POST, PUT and DELETE are required in this assignment.
HEADER_HOST = "Host"
HEADER_CONTENT_LENGTH = "Content-Length"
HEADER_CONTENT_TYPE = "Content-Type"
HEADER_CONTENT_ENCODING = "Content-Encoding"
HEADER_ACCEPT_ENCODING = "Accept-Encoding"
HEADER_CREATE_DIRECTORY = "Create-Directory"
HEADER_SERVER = "Server"
HEADER_REMOVE_DIRECTORY = "Remove-Directory"
GZIP = "gzip"
TEXT_PLAIN = "text/plain"
APPLICATION_OCTET_STREAM = "application/octet-stream"
APPLICATION_GZIP = "application/gzip"
OK = "200"
BAD_REQUEST = "400"
NOT_FOUND = "404"
METHOD_NOT_ALLOWED = "405"
NOT_ACCEPTABLE = "406"
CONFLICT = "409"
HTTP_REASON_BY_STATUS = {
"100": "Continue",
"101": "Switching Protocols",
"200": "OK",
"201": "Created",
"202": "Accepted",
"203": "Non-Authoritative Information",
"204": "No Content",
"205": "Reset Content",
"206": "Partial Content",
"300": "Multiple Choices",
"301": "Moved Permanently",
"302": "Found",
"303": "See Other",
"304": "Not Modified",
"305": "Use Proxy",
"307": "Temporary Redirect",
"400": "Bad Request",
"401": "Unauthorized",
"402": "Payment Required",
"403": "Forbidden",
"404": "Not Found",
"405": "Method Not Allowed",
"406": "Not Acceptable",
"407": "Proxy Authentication Required",
"408": "Request Time-out",
"409": "Conflict",
"410": "Gone",
"411": "Length Required",
"412": "Precondition Failed",
"413": "Request Entity Too Large",
"414": "Request-URI Too Large",
"415": "Unsupported Media Type",
"416": "Requested range not satisfiable",
"417": "Expectation Failed",
"500": "Internal Server Error",
"501": "Not Implemented",
"502": "Bad Gateway",
"503": "Service Unavailable",
"504": "Gateway Time-out",
"505": "HTTP Version not supported",
}
@@ -0,0 +1 @@
click==8.3.0
@@ -0,0 +1,86 @@
import logging
import pathlib
from dataclasses import dataclass
from socketserver import StreamRequestHandler
import typing as t
import click
import socket
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
@dataclass
class HTTPServer:
server_address: t.Tuple[str, int]
socket: socket.socket
server_domain: str
working_directory: pathlib.Path
class HTTPHandler(StreamRequestHandler):
server: HTTPServer
# Use self.rfile and self.wfile to interact with the client
# Access domain and working directory with self.server.{attr}
def handle(self) -> None:
first_line = self.rfile.readline()
logger.info(f"Handle connection from {self.client_address}, first_line {first_line}")
# TODO: Read the remaining headers, handle the request and send a response.
# Read the body separately using its Content-Length, not until EOF.
# Keep memory bounded when processing large files.
pass
@click.command()
@click.option("--host", envvar="SERVER_HOST", default="0.0.0.0", type=str)
@click.option("--port", envvar="SERVER_PORT", default=8080, type=int)
@click.option("--server-domain", envvar="SERVER_DOMAIN", default="localhost", type=str)
@click.option("--working-directory", envvar="SERVER_WORKING_DIRECTORY", type=str)
def main(host, port, server_domain, working_directory):
if not working_directory:
raise SystemExit(1)
working_directory_path = pathlib.Path(working_directory)
logger.info(
f"Starting server on {host}:{port}, domain {server_domain}, working directory {working_directory}"
)
# Create a server socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Set SO_REUSEADDR option
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind the socket object to the address and port
s.bind((host, port))
# Start listening for incoming connections
s.listen()
logger.info(f"Listening at {s.getsockname()}")
server = HTTPServer((host, port), s, server_domain, working_directory_path)
while True:
# Accept any new connection (request, client_address)
try:
conn, addr = s.accept()
except OSError:
break
try:
# Handle the request
HTTPHandler(conn, addr, server)
# Close the connection
conn.shutdown(socket.SHUT_WR)
conn.close()
except Exception as e:
logger.error(e)
conn.close()
if __name__ == "__main__":
main()