mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 20:29:18 +00:00
Tests/LibWeb: Refactor HTTP echo server script
This commit makes the following changes: - Adds a model "Echo" for the request body - Changes the main endpoint from /create to /echo (more REST-y) - Sends "400: Bad Request" for invalid params or a reserved path - Sends "409: Conflict" when trying to use an already registered path - Prints the server port to stdout - Removes unnecessary subcommands/options (start, stop, --background)
This commit is contained in:
parent
7d49704481
commit
4dd21d0b80
Notes:
github-actions[bot]
2024-12-06 00:09:48 +00:00
Author: https://github.com/rmg-x
Commit: 4dd21d0b80
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2553
Reviewed-by: https://github.com/ADKaster ✅
Reviewed-by: https://github.com/shannonbooth
2 changed files with 77 additions and 142 deletions
|
@ -1,76 +1,89 @@
|
|||
#!/usr/bin/env python3
|
||||
import http.client
|
||||
import argparse
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import socketserver
|
||||
import subprocess
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
"""
|
||||
Description:
|
||||
This script starts a simple HTTP echo server on localhost for use in our in-tree tests.
|
||||
The port is assigned by the OS on startup and printed to stdout.
|
||||
|
||||
Endpoints:
|
||||
- POST /echo <json body>, Creates an echo response for later use. See "Echo" class below for body properties.
|
||||
"""
|
||||
|
||||
|
||||
class Echo:
|
||||
method: str
|
||||
path: str
|
||||
status: int
|
||||
headers: Dict[str, str] | None
|
||||
body: str | None
|
||||
delay_ms: int | None
|
||||
headers: dict | None
|
||||
|
||||
|
||||
# In-memory store for echo responses
|
||||
echo_store = {}
|
||||
echo_store: Dict[str, Echo] = {}
|
||||
|
||||
|
||||
class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=self.static_directory, **kwargs)
|
||||
def __init__(self, *arguments, **kwargs):
|
||||
super().__init__(*arguments, directory=None, **kwargs)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/shutdown":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Goodbye")
|
||||
self.server.server_close()
|
||||
print("Goodbye")
|
||||
sys.exit(0)
|
||||
elif self.path == "/ping":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"pong")
|
||||
elif self.path.startswith("/static/"):
|
||||
# Remove '/static/' prefix and use built-in method
|
||||
if self.path.startswith("/static/"):
|
||||
# Remove "/static/" prefix and use built-in method
|
||||
self.path = self.path[7:]
|
||||
return super().do_GET()
|
||||
else:
|
||||
self.handle_echo()
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == "/create":
|
||||
if self.path == "/echo":
|
||||
content_length = int(self.headers["Content-Length"])
|
||||
post_data = self.rfile.read(content_length)
|
||||
response_def = json.loads(post_data.decode("utf-8"))
|
||||
data = json.loads(post_data.decode("utf-8"))
|
||||
|
||||
method = response_def.get("method", "GET").upper()
|
||||
path = response_def.get("path", "")
|
||||
key = f'{method} {path}'
|
||||
echo = Echo()
|
||||
echo.method = data.get("method", None)
|
||||
echo.path = data.get("path", None)
|
||||
echo.status = data.get("status", None)
|
||||
echo.body = data.get("body", None)
|
||||
echo.delay_ms = data.get("delay_ms", None)
|
||||
echo.headers = data.get("headers", None)
|
||||
|
||||
is_invalid_path = path.startswith('/static') or path == '/create' or path == '/shutdown' or path == '/ping'
|
||||
if (is_invalid_path or key in echo_store):
|
||||
is_using_reserved_path = echo.path.startswith("/static") or echo.path.startswith("/echo")
|
||||
|
||||
# Return 400: Bad Request if invalid params are given or a reserved path is given
|
||||
if echo.method is None or echo.path is None or echo.status is None or is_using_reserved_path:
|
||||
self.send_response(400)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
|
||||
if is_invalid_path:
|
||||
self.wfile.write(b"invalid path, must not be /static, /create, /shutdown, /ping")
|
||||
else:
|
||||
self.wfile.write(b"invalid path, already registered")
|
||||
|
||||
return
|
||||
|
||||
echo_store[key] = response_def
|
||||
# Return 409: Conflict if the method+path combination already exists
|
||||
key = f"{echo.method} {echo.path}"
|
||||
if key in echo_store:
|
||||
self.send_response(409)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
host = self.headers.get('host', 'localhost')
|
||||
path = path.lstrip('/')
|
||||
fetch_url = f'http://{host}/{path}'
|
||||
echo_store[key] = echo
|
||||
|
||||
host = self.headers.get("host", "localhost")
|
||||
path = echo.path.lstrip("/")
|
||||
fetch_url = f"http://{host}/{path}"
|
||||
|
||||
# The params to use on the client when making a request to the newly created echo endpoint
|
||||
fetch_config = {
|
||||
"method": method,
|
||||
"method": echo.method,
|
||||
"url": fetch_url,
|
||||
}
|
||||
|
||||
|
@ -85,7 +98,7 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
self.handle_echo()
|
||||
|
||||
def do_OPTIONS(self):
|
||||
if self.path.startswith("/create"):
|
||||
if self.path.startswith("/echo"):
|
||||
self.send_response(204)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "*")
|
||||
|
@ -105,23 +118,25 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
|
||||
def handle_echo(self):
|
||||
method = self.command.upper()
|
||||
key = f'{method} {self.path}'
|
||||
key = f"{method} {self.path}"
|
||||
|
||||
if key in echo_store:
|
||||
response_def = echo_store[key]
|
||||
echo = echo_store[key]
|
||||
|
||||
if "delay" in response_def:
|
||||
time.sleep(response_def["delay"])
|
||||
if echo.delay_ms is not None:
|
||||
time.sleep(echo.delay_ms / 1000)
|
||||
|
||||
# Send the status code without any default headers
|
||||
self.send_response_only(response_def.get("status", 200))
|
||||
self.send_response_only(echo.status)
|
||||
|
||||
# Set only the headers defined in the echo definition
|
||||
for header, value in response_def.get("headers", {}).items():
|
||||
self.send_header(header, value)
|
||||
self.end_headers()
|
||||
if echo.headers is not None:
|
||||
for header, value in echo.headers.items():
|
||||
self.send_header(header, value)
|
||||
self.end_headers()
|
||||
|
||||
self.wfile.write(response_def.get("body", "").encode("utf-8"))
|
||||
response_body = echo.body or ""
|
||||
self.wfile.write(response_body.encode("utf-8"))
|
||||
else:
|
||||
self.send_error(404, f"Echo response not found for {key}")
|
||||
|
||||
|
@ -132,22 +147,12 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
self.handle_echo()
|
||||
|
||||
|
||||
pid_file_path = "http-test-server.pid.txt"
|
||||
log_file_path = "http-test-server.log"
|
||||
|
||||
|
||||
def run_server(port=8000, static_directory="."):
|
||||
def start_server(port, static_directory):
|
||||
TestHTTPRequestHandler.static_directory = os.path.abspath(static_directory)
|
||||
httpd = socketserver.TCPServer(("", port), TestHTTPRequestHandler)
|
||||
httpd = socketserver.TCPServer(("127.0.0.1", port), TestHTTPRequestHandler)
|
||||
|
||||
print(f"Serving at http://localhost:{port}/")
|
||||
print(
|
||||
f"Serving static files from directory: {TestHTTPRequestHandler.static_directory}"
|
||||
)
|
||||
|
||||
# Save pid to file
|
||||
with open(pid_file_path, "w") as f:
|
||||
f.write(str(os.getpid()))
|
||||
print(httpd.socket.getsockname()[1])
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
|
@ -156,70 +161,9 @@ def run_server(port=8000, static_directory="."):
|
|||
finally:
|
||||
httpd.server_close()
|
||||
|
||||
print("Goodbye")
|
||||
|
||||
|
||||
def stop_server(quiet=False):
|
||||
if os.path.exists(pid_file_path):
|
||||
with open(pid_file_path, "r") as f:
|
||||
pid = int(f.read().strip())
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
os.remove(pid_file_path)
|
||||
print("Server stopped")
|
||||
except ProcessLookupError:
|
||||
print("Server not running")
|
||||
except PermissionError:
|
||||
print("Permission denied when trying to stop the server")
|
||||
elif not quiet:
|
||||
print("No server running")
|
||||
|
||||
|
||||
def start_server_in_background(port, directory):
|
||||
# Launch the server as a detached subprocess
|
||||
with open(log_file_path, "w") as log_file:
|
||||
stop_server(True)
|
||||
subprocess.Popen(
|
||||
[sys.executable, __file__, "start", "-p", str(port), "-d", directory],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
preexec_fn=os.setpgrp,
|
||||
)
|
||||
|
||||
# Sleep to give the server time to start
|
||||
time.sleep(0.05)
|
||||
|
||||
# Verify that the server is up by sending a GET request to /ping
|
||||
max_retries = 3
|
||||
for i in range(max_retries):
|
||||
try:
|
||||
conn = http.client.HTTPConnection("localhost", port, timeout=1)
|
||||
conn.request("GET", "/ping")
|
||||
response = conn.getresponse()
|
||||
if response.status == 200 and response.read().decode().strip() == "pong":
|
||||
print(f"Server successfully started on port {port}")
|
||||
return True
|
||||
except (http.client.HTTPException, ConnectionRefusedError, OSError):
|
||||
if i < max_retries - 1:
|
||||
print(
|
||||
f"Server not ready, retrying in 1 second... (Attempt {i+1}/{max_retries})"
|
||||
)
|
||||
time.sleep(1)
|
||||
else:
|
||||
print(f"Failed to start server after {max_retries} attempts")
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print(f"Server verification failed after {max_retries} attempts")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Run a test HTTP server")
|
||||
parser.add_argument(
|
||||
"-p", "--port", type=int, default=8123, help="Port to run the server on"
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Run a HTTP echo server")
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--directory",
|
||||
|
@ -228,21 +172,12 @@ if __name__ == "__main__":
|
|||
help="Directory to serve static files from",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--background",
|
||||
action="store_true",
|
||||
help="Run the server in the background",
|
||||
"-p",
|
||||
"--port",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Port to run the server on",
|
||||
)
|
||||
parser.add_argument("action", choices=["start", "stop"], help="Action to perform")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.action == "start":
|
||||
if args.background:
|
||||
# Detach the server and run in the background
|
||||
start_server_in_background(args.port, args.directory)
|
||||
print(f"Server started in the background, check '{log_file_path}' for details.")
|
||||
else:
|
||||
# Run normally
|
||||
run_server(port=args.port, static_directory=args.directory)
|
||||
elif args.action == "stop":
|
||||
stop_server()
|
||||
start_server(port=args.port, static_directory=args.directory)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue