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 handle_request() function,
 74    [* optionally write something in the log,]
 75    * close the connection.
 76
 77    Parameters
 78    ----------
 79    - c: socket.socket
 80        The socket to communicate with the client.
 81    - addr: tuple[str, int]
 82        The IP address and port of the client, as returned by the accept command.
 83    - root: str
 84        The path to the local directory to serve.
 85
 86    """
 87    buf = c.recv(_BUF_SIZE)
 88    req = parse_request(buf)
 89
 90    # Prepare our reply.
 91    if req['head']['verb'] == 'GET':
 92        reply, code = prepare_resource(root, req)
 93    else:
 94        # Not implemented: we treat only GET calls for now.
 95        reply, code = prepare_reply(b"", "", 501)
 96
 97    # Send the reply back.
 98    c.send(reply)
 99
100    # Trace the action in the logs.
101    log_reply(addr, req, code)
102
103    # Close the connection.
104    c.close()
105    
106
107def prepare_resource(root: str, req: dict):
108    """
109    Retrieves the content of the resource and sets the status code.
110
111    Parameters
112    ----------
113    - root: str
114        The path to the local directory to serve.
115    - req: dict[str, dict]
116        The request to proceed.
117
118    Returns
119    -------
120    tuple 
121        The reply for the request, including the data and status code.
122        - data: str
123            The data (header + content) to reply on the socket.
124        - code: int
125            The status code for the reply.
126    """
127    code = 200
128    content = b""
129    content_type = ""
130
131    res_path, res_extension = resolve_location(req['head']['resource'], root)
132    if res_path == "":
133        code = 404
134    else:
135        content_type = get_http_content_type(res_extension)
136        content, code = get_resource(res_path)
137
138    return prepare_reply(content, content_type, code)
139
140
141def prepare_reply(content: bytes, content_type: str, code: int):
142    """
143    Generates the proper answer, including the HTTP headers and content of the
144    webpage, and the status code.
145
146    For more information about:
147    * Content type, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
148    * Status code, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
149
150    Parameters
151    ----------
152    - content: bytes
153        The raw data for the resource.
154    - content_type: str
155        The content type for the resource.
156    - code: int
157        The status code.
158
159    Returns
160    -------
161    tuple 
162        The reply for the request, including the data and status code.
163        - data: str
164            The data (header + content) to reply on the socket.
165        - code: int
166            The status code for the reply.
167    """
168    # Prepare status code
169    http_code_dict = get_http_code(code)
170    if code != 200:
171        content = http_code_dict['html'].encode()
172        content_type = get_http_content_type('html')+"; charset=utf-8"
173
174    # Prepare header
175    header = f"""HTTP/1.0 {http_code_dict['header']}
176Content-Type: {content_type}
177Date: {now_rfc2616()}
178Content-Length: {len(content)}
179Server: RegardeMamanJeFaisUnServeurWeb/0.1
180
181""".encode()
182
183    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 handle_request() function,
 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    buf = c.recv(_BUF_SIZE)
 89    req = parse_request(buf)
 90
 91    # Prepare our reply.
 92    if req['head']['verb'] == 'GET':
 93        reply, code = prepare_resource(root, req)
 94    else:
 95        # Not implemented: we treat only GET calls for now.
 96        reply, code = prepare_reply(b"", "", 501)
 97
 98    # Send the reply back.
 99    c.send(reply)
100
101    # Trace the action in the logs.
102    log_reply(addr, req, code)
103
104    # Close the connection.
105    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 handle_request() function, [* 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):
108def prepare_resource(root: str, req: dict):
109    """
110    Retrieves the content of the resource and sets the status code.
111
112    Parameters
113    ----------
114    - root: str
115        The path to the local directory to serve.
116    - req: dict[str, dict]
117        The request to proceed.
118
119    Returns
120    -------
121    tuple 
122        The reply for the request, including the data and status code.
123        - data: str
124            The data (header + content) to reply on the socket.
125        - code: int
126            The status code for the reply.
127    """
128    code = 200
129    content = b""
130    content_type = ""
131
132    res_path, res_extension = resolve_location(req['head']['resource'], root)
133    if res_path == "":
134        code = 404
135    else:
136        content_type = get_http_content_type(res_extension)
137        content, code = get_resource(res_path)
138
139    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):
142def prepare_reply(content: bytes, content_type: str, code: int):
143    """
144    Generates the proper answer, including the HTTP headers and content of the
145    webpage, and the status code.
146
147    For more information about:
148    * Content type, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
149    * Status code, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
150
151    Parameters
152    ----------
153    - content: bytes
154        The raw data for the resource.
155    - content_type: str
156        The content type for the resource.
157    - code: int
158        The status code.
159
160    Returns
161    -------
162    tuple 
163        The reply for the request, including the data and status code.
164        - data: str
165            The data (header + content) to reply on the socket.
166        - code: int
167            The status code for the reply.
168    """
169    # Prepare status code
170    http_code_dict = get_http_code(code)
171    if code != 200:
172        content = http_code_dict['html'].encode()
173        content_type = get_http_content_type('html')+"; charset=utf-8"
174
175    # Prepare header
176    header = f"""HTTP/1.0 {http_code_dict['header']}
177Content-Type: {content_type}
178Date: {now_rfc2616()}
179Content-Length: {len(content)}
180Server: RegardeMamanJeFaisUnServeurWeb/0.1
181
182""".encode()
183
184    return header + content, code 

Generates the proper answer, including the HTTP headers and content of the webpage, and the status code.

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.