Add HW 3
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user