src.myserver.server

A package for learning network programming in Python.

This part manages the socket connections and multi-threading of clients.

  1######################################################################
  2# Copyright (c) Adrien Luxey-Bitri, Boris Baldassari
  3#
  4# This program and the accompanying materials are made
  5# available under the terms of the Eclipse Public License 2.0
  6# which is available at https://www.eclipse.org/legal/epl-2.0/
  7#
  8# SPDX-License-Identifier: EPL-2.0
  9######################################################################
 10
 11"""
 12A package for learning network programming in Python.
 13
 14This part manages the socket connections and multi-threading of clients.
 15"""
 16
 17import socket 
 18from myserver.log import log, log_reply
 19from myserver.http_request import parse_request
 20from myserver.file import resolve_location, get_resource
 21from myserver.date import now_rfc2616 
 22from myserver.http import get_http_code, get_http_content_type
 23
 24_BUF_SIZE = 4096
 25_SERVER_ADDR = "0.0.0.0"
 26
 27def serve(port: int, root: str):
 28    """
 29    Serves http request connections for clients.
 30    
 31    This function creates the network socket, listens, and as soon as it receives
 32    a request (connection), calls the :func:`~myserver.handle_client()`.
 33    """
 34    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 35
 36    # Allows reusing a socket right after it got closed (after ctrl-c)
 37    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 38
 39    # Attach the socket to the port and interface provided.
 40    s.bind((_SERVER_ADDR, port))
 41
 42    ## Start listening on the socket.
 43    s.listen()
 44
 45    log(f"Server started at {_SERVER_ADDR}:{port}.")
 46
 47    # Try / catch KeyboardInterrupt to close server socket properly if we hit
 48    # the Control-C sequence to interrupt the server.
 49    try: 
 50        while True:
 51            c, addr = s.accept()
 52            # Try / catch KeyboardInterrupt to properly close the client socket if
 53            # we hit the Control-C sequence to interrupt the server.
 54            try: 
 55                handle_client(c, addr, root)
 56            # If the KeyboardInterrupt is raised, we pass it to the outer loop
 57            # to also close the server socket.
 58            except KeyboardInterrupt as e:
 59                c.close()
 60                raise e
 61    except KeyboardInterrupt:
 62        log("Received KeyboardInterrupt. Closing...")
 63        s.close()
 64
 65        
 66def handle_client(c: socket.socket, addr: tuple[str, int], root:str):
 67    """
 68    Manages a single connection from a client.
 69
 70    In details, we:
 71    * read data from the socket provided,
 72    * parse this data to build the request and headers,
 73    * call the prepare_resource() or prepare_reply() function accordingly,
 74    * send the reply back.
 75    * optionally write something in the log,
 76    * close the connection.
 77
 78    Parameters
 79    ----------
 80    - c: socket.socket
 81        The socket to communicate with the client.
 82    - addr: tuple[str, int]
 83        The IP address and port of the client, as returned by the accept command.
 84    - root: str
 85        The path to the local directory to serve.
 86
 87    """
 88
 89    buf = c.recv(_BUF_SIZE)
 90    req = parse_request(buf)
 91
 92    # Prepare our reply.
 93    if req['head']['verb'] == 'GET':
 94        reply, code = prepare_resource(root, req)
 95    else:
 96        # Not implemented: we treat only GET calls for now.
 97        reply, code = prepare_reply(b"", "", 501)
 98
 99    # Send the reply back.
100    c.send(reply)
101
102    # Trace the action in the logs.
103    log_reply(addr, req, code)
104
105    # Close the connection.
106    c.close()
107    
108
109def prepare_resource(root: str, req: dict):
110    """
111    Retrieves the content of the resource and sets the status code.
112
113    Parameters
114    ----------
115    - root: str
116        The path to the local directory to serve.
117    - req: dict[str, dict]
118        The request to proceed.
119
120    Returns
121    -------
122    tuple 
123        The reply for the request, including the data and status code.
124        - data: str
125            The data (header + content) to reply on the socket.
126        - code: int
127            The status code for the reply.
128    """
129    code = 200
130    content = b""
131    content_type = ""
132
133    res_path, res_extension = resolve_location(req['head']['resource'], root)
134    if res_path == "":
135        code = 404
136    else:
137        content_type = get_http_content_type(res_extension)
138        content, code = get_resource(res_path)
139
140    return prepare_reply(content, content_type, code)
141
142
143def prepare_reply(content: bytes, content_type: str, code: int):
144    """
145    Generates the proper answer, including the HTTP headers and content of the
146    webpage, and the status code.
147
148    Headers will look like that:
149    ```
150    HTTP/1.0 200 OK
151    Content-Type: text/html
152    Date: Thu, 07 Mar 2024 08:29:45 GMT
153    Content-Length: 152
154    Server: RegardeMamanJeFaisUnServeurWeb/0.1
155    ```
156
157    For more information about:
158    * Content type, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
159    * Status code, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
160
161    Parameters
162    ----------
163    - content: bytes
164        The raw data for the resource.
165    - content_type: str
166        The content type for the resource.
167    - code: int
168        The status code.
169
170    Returns
171    -------
172    tuple 
173        The reply for the request, including the data and status code.
174        - data: str
175            The data (header + content) to reply on the socket.
176        - code: int
177            The status code for the reply.
178    """
179    # Prepare status code
180    http_code_dict = get_http_code(code)
181    if code != 200:
182        content = http_code_dict['html'].encode()
183        content_type = get_http_content_type('html')+"; charset=utf-8"
184
185    # Prepare headers, including content-type, date, content-length, server.
186    header = f"""HTTP/1.0 {http_code_dict['header']}
187Content-Type: {content_type}
188Date: {now_rfc2616()}
189Content-Length: {len(content)}
190Server: RegardeMamanJeFaisUnServeurWeb/0.1
191
192""".encode()
193
194    return header + content, code 
def serve(port: int, root: str):
28def serve(port: int, root: str):
29    """
30    Serves http request connections for clients.
31    
32    This function creates the network socket, listens, and as soon as it receives
33    a request (connection), calls the :func:`~myserver.handle_client()`.
34    """
35    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
36
37    # Allows reusing a socket right after it got closed (after ctrl-c)
38    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
39
40    # Attach the socket to the port and interface provided.
41    s.bind((_SERVER_ADDR, port))
42
43    ## Start listening on the socket.
44    s.listen()
45
46    log(f"Server started at {_SERVER_ADDR}:{port}.")
47
48    # Try / catch KeyboardInterrupt to close server socket properly if we hit
49    # the Control-C sequence to interrupt the server.
50    try: 
51        while True:
52            c, addr = s.accept()
53            # Try / catch KeyboardInterrupt to properly close the client socket if
54            # we hit the Control-C sequence to interrupt the server.
55            try: 
56                handle_client(c, addr, root)
57            # If the KeyboardInterrupt is raised, we pass it to the outer loop
58            # to also close the server socket.
59            except KeyboardInterrupt as e:
60                c.close()
61                raise e
62    except KeyboardInterrupt:
63        log("Received KeyboardInterrupt. Closing...")
64        s.close()

Serves http request connections for clients.

This function creates the network socket, listens, and as soon as it receives a request (connection), calls the ~myserver.handle_client()().

def handle_client(c: socket.socket, addr: tuple[str, int], root: str):
 67def handle_client(c: socket.socket, addr: tuple[str, int], root:str):
 68    """
 69    Manages a single connection from a client.
 70
 71    In details, we:
 72    * read data from the socket provided,
 73    * parse this data to build the request and headers,
 74    * call the prepare_resource() or prepare_reply() function accordingly,
 75    * send the reply back.
 76    * optionally write something in the log,
 77    * close the connection.
 78
 79    Parameters
 80    ----------
 81    - c: socket.socket
 82        The socket to communicate with the client.
 83    - addr: tuple[str, int]
 84        The IP address and port of the client, as returned by the accept command.
 85    - root: str
 86        The path to the local directory to serve.
 87
 88    """
 89
 90    buf = c.recv(_BUF_SIZE)
 91    req = parse_request(buf)
 92
 93    # Prepare our reply.
 94    if req['head']['verb'] == 'GET':
 95        reply, code = prepare_resource(root, req)
 96    else:
 97        # Not implemented: we treat only GET calls for now.
 98        reply, code = prepare_reply(b"", "", 501)
 99
100    # Send the reply back.
101    c.send(reply)
102
103    # Trace the action in the logs.
104    log_reply(addr, req, code)
105
106    # Close the connection.
107    c.close()

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.
def prepare_resource(root: str, req: dict):
110def prepare_resource(root: str, req: dict):
111    """
112    Retrieves the content of the resource and sets the status code.
113
114    Parameters
115    ----------
116    - root: str
117        The path to the local directory to serve.
118    - req: dict[str, dict]
119        The request to proceed.
120
121    Returns
122    -------
123    tuple 
124        The reply for the request, including the data and status code.
125        - data: str
126            The data (header + content) to reply on the socket.
127        - code: int
128            The status code for the reply.
129    """
130    code = 200
131    content = b""
132    content_type = ""
133
134    res_path, res_extension = resolve_location(req['head']['resource'], root)
135    if res_path == "":
136        code = 404
137    else:
138        content_type = get_http_content_type(res_extension)
139        content, code = get_resource(res_path)
140
141    return prepare_reply(content, content_type, code)

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.

def prepare_reply(content: bytes, content_type: str, code: int):
144def prepare_reply(content: bytes, content_type: str, code: int):
145    """
146    Generates the proper answer, including the HTTP headers and content of the
147    webpage, and the status code.
148
149    Headers will look like that:
150    ```
151    HTTP/1.0 200 OK
152    Content-Type: text/html
153    Date: Thu, 07 Mar 2024 08:29:45 GMT
154    Content-Length: 152
155    Server: RegardeMamanJeFaisUnServeurWeb/0.1
156    ```
157
158    For more information about:
159    * Content type, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
160    * Status code, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
161
162    Parameters
163    ----------
164    - content: bytes
165        The raw data for the resource.
166    - content_type: str
167        The content type for the resource.
168    - code: int
169        The status code.
170
171    Returns
172    -------
173    tuple 
174        The reply for the request, including the data and status code.
175        - data: str
176            The data (header + content) to reply on the socket.
177        - code: int
178            The status code for the reply.
179    """
180    # Prepare status code
181    http_code_dict = get_http_code(code)
182    if code != 200:
183        content = http_code_dict['html'].encode()
184        content_type = get_http_content_type('html')+"; charset=utf-8"
185
186    # Prepare headers, including content-type, date, content-length, server.
187    header = f"""HTTP/1.0 {http_code_dict['header']}
188Content-Type: {content_type}
189Date: {now_rfc2616()}
190Content-Length: {len(content)}
191Server: RegardeMamanJeFaisUnServeurWeb/0.1
192
193""".encode()
194
195    return header + content, code 

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:

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.