diff --git a/scripts/03_server/src_students_6/myserver/__init__.py b/scripts/03_server/src_students_6/myserver/__init__.py new file mode 100644 index 0000000..75a8973 --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/__init__.py @@ -0,0 +1,17 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) defines the directory as a Python module. +""" + + diff --git a/scripts/03_server/src_students_6/myserver/__main__.py b/scripts/03_server/src_students_6/myserver/__main__.py new file mode 100644 index 0000000..10b5f95 --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/__main__.py @@ -0,0 +1,23 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) is the entrypoint of the package. +""" + +# Plus d'infos sur __main__.py: +# https://docs.python.org/3/library/__main__.html + +from myserver.cli import main + + +main() diff --git a/scripts/03_server/src_students_6/myserver/cli.py b/scripts/03_server/src_students_6/myserver/cli.py new file mode 100644 index 0000000..bb7faa0 --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/cli.py @@ -0,0 +1,53 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) manages the command line interface to control the server. +""" + +import argparse +import sys +from myserver.server import serve + +def parse_args(argv: list[str]) -> argparse.Namespace: + """ + Parses arguments from command line. + + Parameters + ---------- + - argv: list[str] + The list of arguments to parse. + + Returns + ------- + argparse.Namespace + The list of parameters and their values. + + """ + parser = argparse.ArgumentParser( + prog="myserver", + description="My HTTP web server") + parser.add_argument('-p', '--port', + help='TCP port number to listen to', + default=8080, type=int, + required=True) + parser.add_argument('-r', '--root', + help='Root directory of the server', + type=str, required=True) + args = parser.parse_args(argv) + + return args + +def main(): + args = parse_args(sys.argv[1:]) + serve(int(args.port), args.root) + diff --git a/scripts/03_server/src_students_6/myserver/date.py b/scripts/03_server/src_students_6/myserver/date.py new file mode 100644 index 0000000..65c38ed --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/date.py @@ -0,0 +1,23 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) manages date related-utilities. +""" + +from time import gmtime, strftime + +_RFC2616_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' + +def now_rfc2616(): + return strftime(_RFC2616_DATE_FORMAT, gmtime()) + diff --git a/scripts/03_server/src_students_6/myserver/file.py b/scripts/03_server/src_students_6/myserver/file.py new file mode 100644 index 0000000..9574a93 --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/file.py @@ -0,0 +1,89 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) manages operations relative to the file system. +""" + +import os.path as path + + +def resolve_location(res:str, root: str): + """Returns the path of a resource relative to the root and its extension. + + Returns ("", "") if the concatenated path does not exist. + + "index.html" is appended to directory paths. + + Parameters + ---------- + res: str + The queried resource path. + root: str + The root directory where to look into for res. + + Returns + ------- + str + The full disk path of the resource if it exists, or "". + str + The extension of the resource if it exists, or "". + """ + + pass + + +def resolve_path(res:str, root: str): + """Returns the full disk path of a resource relative to the root. + + Beware that resources in a request start with a leading '/'. + If the request points to a directory, then append "index.html" to + the path. + + Returns "" if the concatenated path does not exist. + + Parameters + ---------- + res: str + The queried resource path. + root: str + The root directory where to look into for res. + + Returns + ------- + str + The full disk path of the resource if it exists, or "". + """ + + pass + + + +def get_resource(res_path: str): + """Returns a resource at res_path, its content type and an HTTP code. + The HTTP status code is always 200, as we have already checked the file is present. + + Parameters + ---------- + - res_path: str + Requested resource string. + + Returns + ------- + bytes + The resource content if it exists (code == 200). + int + A HTTP status code. + """ + + pass + diff --git a/scripts/03_server/src_students_6/myserver/http.py b/scripts/03_server/src_students_6/myserver/http.py new file mode 100644 index 0000000..c935ade --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/http.py @@ -0,0 +1,149 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) provides information relative to the HTTP specification. +""" + + +def get_http_code(code: int): + """Returns a dict corresponding to the HTTP status code. + + See also : https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + + Parameters + ---------- + - code: int + An HTTP code. + + Returns + ------- + dict + Information about the HTTP code, containing fileds: + - header: str + The code string to put in an HTTP reply header. + - html: str + The HTML to reply as HTTP content. + """ + + if code == 200: + return { + "header": "200 OK", + "html": "" + } + elif code == 403: + return { + "header": "403 Forbidden", + "html": """ + +

Erreur 403 : Interdit

+

Une porte fermée se tient devant vous ; et vous n'avez pas la clé.

+ + +""" + } + elif code == 404: + return { + "header": "404 Not Found", + "html": """ + +

Erreur 404

+

Vous avez traversé les limites du Web. Où que vous soyez, ce n'est sur aucune carte.

+ + +""" + } + elif code == 501: + return { + "header": "501 Not implemented", + "html": """ + +

Erreur 501 : Non implémenté

+

Ce que vous demandez est acceptable, mais on ne fait pas ça chez nous.

+ + +""" + } + else: # 500 + return { + "header": "500 Internal Server Error", + "html": """ + +

Erreur 500 : InTERNal SRveR ER0ooOR

+

Erreur serveur inconnue.

+ + +""" + } + +# From: https://source.chromium.org/chromium/chromium/src/+/main:net/base/mime_util.cc;l=147 +# The Chromium authors, 2012, BSD Licence +file_extension_to_content_type = { + "webm": "video/webm", + "mp3": "audio/mpeg", + "wasm": "application/wasm", + "crx": "application/x-chrome-extension", + "xhtml": "application/xhtml+xml", + "xht": "application/xhtml+xml", + "xhtm": "application/xhtml+xml", + "flac": "audio/flac", + "ogg": "audio/ogg", + "oga": "audio/ogg", + "opus": "audio/ogg", + "wav": "audio/wav", + "m4a": "audio/x-m4a", + "avif": "image/avif", + "gif": "image/gif", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "png": "image/png", + "apng": "image/apng", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "webp": "image/webp", + "mht": "multipart/related", + "mhtml": "multipart/related", + "css": "text/css", + "html": "text/html", + "htm": "text/html", + "shtml": "text/html", + "shtm": "text/html", + "js": "text/javascript", + "mjs": "text/javascript", + "xml": "text/xml", + "mp4": "video/mp4", + "m4v": "video/mp4", + "ogv": "video/ogg", + "ogm": "video/ogg", + "csv": "text/csv", + "ico": "image/vnd.microsoft.icon" +} + +def get_http_content_type(extension: str): + """Returns the HTTP Content-Type corresponding to a file extension. + + Returns "application/octet-stream" when the extension is unknown. + + Parameters + ---------- + - extension: str + A file extension. + + Returns + ------- + str + An HTTP Content-Type + """ + + if file_extension_to_content_type.get(extension) is None: + return "application/octet-stream" + return file_extension_to_content_type[extension] diff --git a/scripts/03_server/src_students_6/myserver/http_request.py b/scripts/03_server/src_students_6/myserver/http_request.py new file mode 100644 index 0000000..2c58d1d --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/http_request.py @@ -0,0 +1,144 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) manages the parsing of HTTP requests. +""" + + +def parse_request(buf: bytes) -> dict[str, dict]: + """Parses a full HTTP request bytes buffer into a dict. + + The parsed request dict contains two keys: + - head: dict[str, str] + Information on the HTTP request header (i.e. the first request line); + output of `parse_request_head`. + - params: dict[str, str] + List of the HTTP parameters (i.e. the following lines); + output of `parse_request_params`. + + An example of return: + ``` + { + 'head': { 'verb': 'GET', 'resource': '//index.html'}, + 'params': { + 'Host': 'localhost:8000', + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-GB,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br', + [SNIP] + } + } + ``` + + Parameters + ---------- + - buf: bytes + The HTTP request buffer. + + Returns + ------- + dict[str, dict] + The parsed content of the HTTP request. + + Raises + ------ + ValueError + The request is not valid HTTP. + """ + if buf == b'': + raise ValueError("Received empty request") + lines = buf.decode('utf-8').strip().splitlines() + + req_head = parse_request_head(lines[0]) + req_params = dict() + if len(lines) > 1: + req_params = parse_request_params(lines[1:]) + + return dict( + head=req_head, + params=req_params + ) + +def parse_request_head(line: str) -> dict[str, str]: + """Parses a HTTP request header string (its first line) into a dict. + + The parsed request dict contains two keys: + - verb: str + The _uppercase_ verb of the request, i.e. the first word of the line; + for example: "GET". + - resource: str + The requested resource, i.e. the second "word" of the line; + for example: "/index.html". + + Parameters + ---------- + - line: str + The HTTP request header (the first line of a full HTTP request). + + Returns + ------- + dict[str, str] + The parsed content of the HTTP request header. + + Raises + ------ + ValueError + The request header is not valid HTTP. + """ + fields = line.split(' ') + if len(fields) != 3: + raise ValueError(f"Request header is invalid: {line}") + + return dict( + verb=fields[0].upper(), + resource=fields[1] + ) + +def parse_request_params(lines: list[str]) -> dict[str, str]: + """Parses HTTP request parameters (a list of lines) into a dict. + + The parsed request dict contains one key/value pair per line, with the + dict key being the left part of the line (the parameter key), and the + dict value being the right part of the line (the parameter value). + + The function strips leading and trailing spaces: " Host: a.org " becomes + `{"Host": "a.org"}`. + + Parameters + ---------- + - lines: list[str] + HTTP parameters (one list item per line) + + Returns + ------- + dict[str, str] + Dictionary of the parameters + + Raises + ------ + ValueError + The provided lines are not valid HTTP. + """ + params = dict() + for l in lines: + kv = l.strip().split(': ') + + if len(kv) != 2 or len(kv[0]) == 0 or len(kv[1]) == 0: + raise ValueError(f"Request line is not a valid key/value pair: {l}") + + params[kv[0]] = kv[1] + + return params + + diff --git a/scripts/03_server/src_students_6/myserver/log.py b/scripts/03_server/src_students_6/myserver/log.py new file mode 100644 index 0000000..428c107 --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/log.py @@ -0,0 +1,97 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This module (file) manages the HTTP logging messages. +""" + +from myserver.date import now_rfc2616 + + +def log(msg: str): + """ + Logs a message to stdout, with a timestamp. + + Output is: `timestamp - message`. + + Parameters + ---------- + - msg : str + The message string to print. + """ + + pass + + +def log_address(addr: tuple[str, int], msg: str): + """ + Logs a message to stdout, with a timestamp and an address (host:port). + + Output is: `timestamp - host:port - message`. + + Parameters + ---------- + - addr: tuple[str, int] + The address to print, as a tuple (host, port) + - msg: str + The message string to print. + """ + + pass + + +def log_request(addr: tuple[str, int], req: dict[str, dict]): + """ + Logs a request message to stdout, with a timestamp and an address (host:port). + If the User-Agent header is passed, its value is appended at the end. + + Output is: `timestamp - host:port - verb resource`. + + Output with User-Agent is: `timestamp - host:port - verb resource - user_agent`. + + Parameters + ---------- + - addr: tuple[str, int] + The address to print, as a tuple (host, port) + - req: dict[str, dict] + The request to print. + """ + + msg = None + + log_address(addr, msg) + + +def log_reply(addr: tuple[str, int], req: dict[str, dict], code: int): + """ + Logs an HTTP reply to stdout, with timestamp, address (host:port), code. + If the User-Agent header is passed, its value is appended at the end. + + Output is: `timestamp - host:port - HTTP-verb HTTP-resource - code`. + + Output with User-Agent is: `timestamp - host:port - HTTP-verb HTTP-resource - code - user_agent`. + + Parameters + ---------- + - addr: tuple[str, int] + The address to print, as a tuple (host, port) + - req: dict[str, dict] + The request to print. + - code: int + The replied code to print. + """ + + msg = None + + log_address(addr, msg) + + diff --git a/scripts/03_server/src_students_6/myserver/server.py b/scripts/03_server/src_students_6/myserver/server.py new file mode 100644 index 0000000..8334719 --- /dev/null +++ b/scripts/03_server/src_students_6/myserver/server.py @@ -0,0 +1,191 @@ +###################################################################### +# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +###################################################################### + +""" +A package for learning network programming in Python. + +This part manages the socket connections and multi-threading of clients. +""" + +import socket +from myserver.log import log, log_reply +from myserver.http_request import parse_request +from myserver.file import resolve_location, get_resource +from myserver.date import now_rfc2616 +from myserver.http import get_http_code, get_http_content_type + +_BUF_SIZE = 4096 +_SERVER_ADDR = "0.0.0.0" + +def serve(port: int, root: str): + """ + Serves http request connections for clients. + + This function creates the network socket, listens, and as soon as it receives + a request (connection), calls the :func:`~myserver.handle_client()`. + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # Allows reusing a socket right after it got closed (after ctrl-c) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # Attach the socket to the port and interface provided. + s.bind((_SERVER_ADDR, port)) + + ## Start listening on the socket. + s.listen() + + log(f"Server started at {_SERVER_ADDR}:{port}.") + + # Try / catch KeyboardInterrupt to close server socket properly if we hit + # the Control-C sequence to interrupt the server. + try: + while True: + c, addr = s.accept() + # Try / catch KeyboardInterrupt to properly close the client socket if + # we hit the Control-C sequence to interrupt the server. + try: + handle_client(c, addr, root) + # If the KeyboardInterrupt is raised, we pass it to the outer loop + # to also close the server socket. + except KeyboardInterrupt as e: + c.close() + raise e + except KeyboardInterrupt: + log("Received KeyboardInterrupt. Closing...") + s.close() + + +def handle_client(c: socket.socket, addr: tuple[str, int], root:str): + """ + Manages a single connection from a client. + + In details, we: + * read data from the socket provided, + * parse this data to build the request and headers, + * call the prepare_resource() or prepare_reply() function accordingly, + * send the reply back. + * optionally write something in the log, + * close the connection. + + Parameters + ---------- + - c: socket.socket + The socket to communicate with the client. + - addr: tuple[str, int] + The IP address and port of the client, as returned by the accept command. + - root: str + The path to the local directory to serve. + + """ + + # Read the socket. + buf = c.recv(_BUF_SIZE) + print(buf) + + # Parse the request to get the headers - call parse_request(). + + # Prepare our reply. + # If we get a GET verb from the request header, then call prepare_resource(). + # Otherwise, prepare a reply with a "Non Implemented" status code. + + # Send the reply back. + reply = """HTTP/1.0 200 OK +Server: RegardeMamanJeFaisUnServeurWeb/0.1 +Date: Wed, 20 Mar 2024 16:36:42 GMT +Content-type: text/html +Content-Length: 14 + +Hello, world!""" + c.send(reply) + + # Trace the action in the logs. + + # Close the connection. + c.close() + + +def prepare_resource(root: str, req: dict): + """ + Retrieves the content of the resource and sets the status code. + + Parameters + ---------- + - root: str + The path to the local directory to serve. + - req: dict[str, dict] + The request to proceed. + + Returns + ------- + tuple + The reply for the request, including the data and status code. + - data: str + The data (header + content) to reply on the socket. + - code: int + The status code for the reply. + """ + code = 200 + content = b"Hello World!" + content_type = "text/html" + + # Call resolve_location(). + # If an empty string is returned, the resource is not found. + # If a path is returned, get the content_type, the resource and the status code. + + # Then call prepare_reply to build the final reply. + return prepare_reply(content, content_type, code) + + +def prepare_reply(content: bytes, content_type: str, code: int): + """ + Generates the proper answer, including the HTTP headers and content of the + webpage, and the status code. + + Headers will look like that: + ``` + HTTP/1.0 200 OK + Content-Type: text/html + Date: Thu, 07 Mar 2024 08:29:45 GMT + Content-Length: 152 + Server: RegardeMamanJeFaisUnServeurWeb/0.1 + ``` + + For more information about: + * Content type, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types + * Status code, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + + Parameters + ---------- + - content: bytes + The raw data for the resource. + - content_type: str + The content type for the resource. + - code: int + The status code. + + Returns + ------- + tuple + The reply for the request, including the data and status code. + - data: str + The data (header + content) to reply on the socket. + - code: int + The status code for the reply. + """ + # Prepare status code + code = 1234 + + # Prepare headers, including content-type, date, content-length, server. + header = ''.encode() + + return header + content, code + +