180 lines
5.4 KiB
Python
180 lines
5.4 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.
|
|
|
|
"""
|
|
|
|
# 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.
|
|
|
|
# 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
|
|
|
|
|