diff --git a/src/server/server.py b/src/server/server.py new file mode 100644 index 0000000..2c97ba3 --- /dev/null +++ b/src/server/server.py @@ -0,0 +1,203 @@ + +# TODO 1. Finalize the `Request` Class +# - Parse the request-line to extract `method`, `url`, and `protocol`. +# - Validate the protocol is `HTCPCP/1.0`. +# - Parse headers into a dictionary and validate required ones. +# - Handle payload extraction based on `Content-Length` header. +# +# TODO 2. Validate Requests +# - Ensure URL uses a valid scheme (`coffee://`, `caf%C3%E8://`, etc.). +# - Check method validity against `ALLOWED_METHODS`. +# - Validate the path and extract query parameters like `additions`. +# +# TODO 3. Handle URL Parameters +# - Parse query parameters from the URL. +# - Validate `additions` against a list of allowed values (e.g., cream, sugar). +# - Respond with 400 Bad Request for invalid additions. +# +# TODO 4. Implement Proper HTCPCP Responses +# - Support dynamic 200 OK responses for valid requests. +# - Add error handling for 400, 405, 418, and 500 responses. +# +# TODO 5. Expand `RESPONSES` Dictionary +# - Add entries dynamically for specific paths and parameters (e.g., `/coffee?additions=cream`). +# +# TODO 6. Create a Simple Client +# - Use Python's `socket` library to send manual HTCPCP requests. +# - Include options to customize headers, methods, and payloads. +# +# TODO 7. Add Compliance Checks +# - Verify mandatory headers like `Accept` and `Content-Type`. +# - Handle `Content-Length` correctly. +# - Validate supported `Content-Type` values. +# +# TODO 8. Improve `parse_request` Function +# - Refactor it to call `Request` methods for parsing and validation. +# +# TODO 9. Support POST Requests +# - Parse and validate the payload (e.g., JSON format). +# - Add logic to return appropriate responses for POST requests. +# +# TODO 10. Testing & Error Handling +# - Write `pytest` cases to automate server testing. +# - Ensure all exceptions are handled gracefully. +# +# TODO 11. Add Logging (Bonus) +# - Log incoming requests, responses, and errors for debugging. +import re +import socket +from typing import Dict, Optional +from datetime import datetime, timezone + +# Constants +PROTOCOL = "HTCPCP/1.0" +ALLOWED_METHODS = ["GET", "BREW", "POST"] +NAME = "CoffeeMaker" +HOST = '127.0.0.1' +PORT = 6969 + +COFFEE_SCHEMES = [ + "coffee", "koffie", "%D9%82%D9%87%D9%88%D8%A9", "caf%C3%E8", "%E5%92%96%E5%95%A1", + "k%C3%A1va", "kaffe", "kafo", "kohv", "kahvi", "%CE%BA%CE%B1%CF%86%CE%AD", + "%E0%A4%95%E0%A5%8C%E0%A4%AB%E0%A5%80", "%EC%BB%A4%ED%94%BC", "%D0%BA%D0%BE%D1%84%D0%B5" +] + +RESPONSES: Dict[int, str] = { + 200: "OK", + 400: "BAD REQUEST" +} + +ERRORS = { + "SCHEME_MISMATCH": "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nInvalid scheme. Use coffee://.", + "NOT_FOUND": "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\nCommand not recognized." +} + + +# region Utils + +def get_http_date() -> str: + """ + Returns the current date and time in HTTP-date format (RFC 7231). + Example: Thu, 19 Dec 2024 06:56:48 GMT + """ + now = datetime.now(timezone.utc) + return now.strftime("%a, %d %b %Y %H:%M:%S GMT") + +# endregion + + +class HtpcpError(Exception): + def __init__(self, message, status): + super().__init__(message) + self.status = status + + +class Request: + def __init__(self, raw_request: str): + self.method = "" + self.url = "" + self.protocol = "" + self.headers: Dict[str, str] = {} + self.payload: Optional[str] = None + + self._parse_request(raw_request) + + def _parse_request(self, raw_request: str): + lines = raw_request.split("\n") + + if len(lines) < 1: + raise HtpcpError("Invalid request: Request is empty.", 400) + + request_line = lines[0].strip() + self._parse_request_line(request_line) + + header_and_body = raw_request.split("\n\n", 1) + self._parse_headers(header_and_body[0]) + + if len(header_and_body) == 2: + self.payload = header_and_body[1].strip() + + def _parse_request_line(self, request_line: str): + parts = request_line.split(" ") + + if len(parts) != 3: + raise HtpcpError("Invalid request line format.", 400) + + self.method, self.url, self.protocol = parts + + if self.protocol != PROTOCOL: + raise HtpcpError(f"Invalid protocol: {self.protocol}", 400) + + def _parse_headers(self, header_block: str): + header_lines = header_block.split("\n")[1:] + + for line in header_lines: + if ":" in line: + key, value = map(str.strip, line.split(":", 1)) + self.headers[key.lower()] = value.lower() + + def validate(self): + if not any(self.url.startswith(f"{scheme}://") for scheme in COFFEE_SCHEMES): + raise HtpcpError("Invalid URL scheme.", 400) + + if self.method not in ALLOWED_METHODS: + raise HtpcpError(f"Unsupported method: {self.method}", 400) + + +class Response(): + status: int = 200 + + body: str = "" + + def __str__(self): + status_line = f"{PROTOCOL} {self.status} {RESPONSES[self.status]}" + date_line = get_http_date() + server_line = f"Server: {NAME}" + + return f"{status_line}\r\nDate: {date_line}\r\n{server_line}" + + +class Server(): + def __init__(self, host: str, port: int): + self.host = host + self.port = port + self.queue = [] + + def parse_request(self, raw_request: str) -> Response: + """ + Parse the raw request and validate the COFFEE scheme. + Return a proper response or error. + """ + response = Response() + + try: + request = Request(raw_request) + except HtpcpError as e: + print(e) + response.status = e.status + + + return response + + def start_server(self): + """Start the TCP server to handle COFFEE protocol requests.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_server: + socket_server.bind((self.host, self.port)) + socket_server.listen() + print(f"Server started at {HOST}:{PORT}") + + while True: + conn, addr = socket_server.accept() + with conn: + print(f"Connected by {addr}") + request = conn.recv(1024).decode("utf-8") + print(f"Request:\n{request}") + + # Process the request and send the response + response = self.parse_request(request) + conn.sendall(str(response).encode("utf-8")) + + +if __name__ == "__main__": + server = Server(HOST, PORT) + server.start_server()