SVN / public / code / lecture-10 / search_server.py

Revision 1943
Date2026-01-13T16:14:04+01:00
Committerhb1003
Download
"""
Copyright 2025, University of Freiburg,
Chair of Algorithms and Data Structures.
Hannah Bast <bast@cs.uni-freiburg.de>
"""

import argparse
import socket
from pathlib import Path


class SearchServer:
    """
    A very simple HTTP search server.
    """

    def __init__(self, port: int):
        """
        Initialize with given port.
        """
        self.port = port

    def run(self):
        """
        Run the server loop: create a socket, and then, in an infinite loop,
        wait for requests and do something with them.
        """
        # Create server socket using IPv4 addresses and TCP.
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Allow reuse of port if we start program again after a crash.
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # Say on which machine and port we want to listen for connections.
        server_address = ("0.0.0.0", self.port)
        server_socket.bind(server_address)
        # Start listening
        server_socket.listen()

        # Server loop
        while True:
            # Wait for incoming requests.
            print("")
            print(f"Waiting for a connection on port {self.port} ...")
            client_socket, client_address = server_socket.accept()
            print(f"Connection from {client_address} established")

            # Read incoming request in rounds, until the first empty line.
            request = b""
            while True:
                request += client_socket.recv(4096)
                if b"\r\n\r\n" in request:
                    break
            request = request.decode("utf-8").strip()

            # We are only interested in GET requests and the part between
            # "GET /" and " HTTP/1.1".
            request = request.split("\r\n")[0]
            request = request[len("GET /") : -len(" HTTP/1.1")]
            print(f"Request received: {request}")

            # Handle the request and send back the response.
            response = self.handle_request(request)
            client_socket.sendall(response)
            client_socket.close()

    def handle_request(self, request: str) -> str:
        """
        Handle a single request and send it back as a byte string.
        """

        encoding = "utf-8"
        http_code = "200 ALLES KLAR"
        content_type = f"text/plain; charset={encoding}"

        # Check if there is "?query=..." and if yes, strip it and process it.
        query_result = b""
        query = b""
        if request.startswith("api?query="):
            request, query = request.split("?query=")
            query = query.replace("+", " ")
            # Let's compute the 3-grams of the query string.
            qgrams = []
            for i in range(len(query) - 2):
                qgram = query.lower()[i : i + 3]
                qgrams.append(f'"{qgram}"')
            query_result = "[" + ", ".join(qgrams) + "]"
            response = query_result.encode("utf-8")
            content_type = f"application/json; charset={encoding}"
            # query = query.encode("utf-8")

        # Check if a file with that name exists, and if yes return it.
        elif Path(request).is_file():
            if "xxx" in request:
                http_code = "403 HAU AB"
                response = "That was sneaky!\n".encode("utf-8")
            else:
                print(f'Serving file "{request}"')
                file_name = request
                with open(file_name, "rb") as f:
                    response = f.read()
                if request.endswith(".html"):
                    content_type = f"text/html; charset={encoding}"
                elif request.endswith(".css"):
                    content_type = f"text/css; charset={encoding}"
                elif request.endswith(".js"):
                    content_type = f"application/javascript; charset={encoding}"
                if file_name == "search.html":
                    response = response.replace(b"%RESULT%", query_result)
                    response = response.replace(b"%QUERY%", query)

        else:
            print("Just say thank you")
            response = f'Thank you for "{request}", auf Wiedersehen\n'
            response = response.encode("utf-8")

        # The headers for our response.
        header = f"HTTP/1.1 {http_code}\r\n"
        header += f"Content-Length: {len(response)}\r\n"
        header += f"Content-Type: {content_type}\r\n"
        if request == "api":
            header += "Access-Control-Allow-Origin: *\r\n"
        header += "Connection: close\r\n"

        return (header + "\r\n").encode("utf-8") + response


if __name__ == "__main__":
    # Parse command line arguments.
    parser = argparse.ArgumentParser()
    parser.add_argument("port", type=int, help="port to listen on")
    args = parser.parse_args()

    # Create and run server.
    server = SearchServer(args.port)
    server.run()