[main] Added server file
This commit is contained in:
parent
066fdd2dc4
commit
64e003f673
203
src/server/server.py
Normal file
203
src/server/server.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user