"""
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()