[main] Added server file

This commit is contained in:
Thastertyn 2024-12-20 12:24:26 +01:00
parent 066fdd2dc4
commit 64e003f673

203
src/server/server.py Normal file
View File

@ -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()