ue_pe_web/scripts/03_server/src_complete/myserver/server.py
2024-03-21 21:42:50 +01:00

201 lines
5.9 KiB
Python

######################################################################
# 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)
# Parse the request to get the headers - call parse_request().
req = parse_request(buf)
# Prepare our reply.
if req['head']['verb'] == 'GET':
reply, code = prepare_resource(root, req)
else:
# Not implemented: we treat only GET calls for now.
reply, code = prepare_reply(b"", "", 501)
# Send the reply back.
c.send(reply)
# Trace the action in the logs.
log_reply(addr, req, code)
# 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""
content_type = ""
res_path, res_extension = resolve_location(req['head']['resource'], root)
if res_path == "":
code = 404
else:
content_type = get_http_content_type(res_extension)
content, code = get_resource(res_path)
# 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
http_code_dict = get_http_code(code)
if code != 200:
content = http_code_dict['html'].encode()
content_type = get_http_content_type('html')+"; charset=utf-8"
# Prepare headers, including content-type, date, content-length, server.
header = f"""HTTP/1.0 {http_code_dict['header']}
Content-Type: {content_type}
Date: {now_rfc2616()}
Content-Length: {len(content)}
Server: RegardeMamanJeFaisUnServeurWeb/0.1
""".encode()
return header + content, code