mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-09 17:49:40 +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
|
#!/usr/bin/env python3
|
||||||
import http.client
|
import argparse
|
||||||
import http.server
|
import http.server
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import socketserver
|
import socketserver
|
||||||
import subprocess
|
|
||||||
import argparse
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
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
|
# In-memory store for echo responses
|
||||||
echo_store = {}
|
echo_store: Dict[str, Echo] = {}
|
||||||
|
|
||||||
|
|
||||||
class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *arguments, **kwargs):
|
||||||
super().__init__(*args, directory=self.static_directory, **kwargs)
|
super().__init__(*arguments, directory=None, **kwargs)
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/shutdown":
|
if self.path.startswith("/static/"):
|
||||||
self.send_response(200)
|
# Remove "/static/" prefix and use built-in method
|
||||||
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
|
|
||||||
self.path = self.path[7:]
|
self.path = self.path[7:]
|
||||||
return super().do_GET()
|
return super().do_GET()
|
||||||
else:
|
else:
|
||||||
self.handle_echo()
|
self.handle_echo()
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if self.path == "/create":
|
if self.path == "/echo":
|
||||||
content_length = int(self.headers["Content-Length"])
|
content_length = int(self.headers["Content-Length"])
|
||||||
post_data = self.rfile.read(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()
|
echo = Echo()
|
||||||
path = response_def.get("path", "")
|
echo.method = data.get("method", None)
|
||||||
key = f'{method} {path}'
|
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'
|
is_using_reserved_path = echo.path.startswith("/static") or echo.path.startswith("/echo")
|
||||||
if (is_invalid_path or key in echo_store):
|
|
||||||
|
# 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_response(400)
|
||||||
self.send_header("Content-Type", "text/plain")
|
self.send_header("Content-Type", "text/plain")
|
||||||
self.end_headers()
|
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
|
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')
|
echo_store[key] = echo
|
||||||
path = path.lstrip('/')
|
|
||||||
fetch_url = f'http://{host}/{path}'
|
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
|
# The params to use on the client when making a request to the newly created echo endpoint
|
||||||
fetch_config = {
|
fetch_config = {
|
||||||
"method": method,
|
"method": echo.method,
|
||||||
"url": fetch_url,
|
"url": fetch_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +98,7 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
self.handle_echo()
|
self.handle_echo()
|
||||||
|
|
||||||
def do_OPTIONS(self):
|
def do_OPTIONS(self):
|
||||||
if self.path.startswith("/create"):
|
if self.path.startswith("/echo"):
|
||||||
self.send_response(204)
|
self.send_response(204)
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header("Access-Control-Allow-Methods", "*")
|
self.send_header("Access-Control-Allow-Methods", "*")
|
||||||
|
@ -105,23 +118,25 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
def handle_echo(self):
|
def handle_echo(self):
|
||||||
method = self.command.upper()
|
method = self.command.upper()
|
||||||
key = f'{method} {self.path}'
|
key = f"{method} {self.path}"
|
||||||
|
|
||||||
if key in echo_store:
|
if key in echo_store:
|
||||||
response_def = echo_store[key]
|
echo = echo_store[key]
|
||||||
|
|
||||||
if "delay" in response_def:
|
if echo.delay_ms is not None:
|
||||||
time.sleep(response_def["delay"])
|
time.sleep(echo.delay_ms / 1000)
|
||||||
|
|
||||||
# Send the status code without any default headers
|
# 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
|
# Set only the headers defined in the echo definition
|
||||||
for header, value in response_def.get("headers", {}).items():
|
if echo.headers is not None:
|
||||||
self.send_header(header, value)
|
for header, value in echo.headers.items():
|
||||||
self.end_headers()
|
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:
|
else:
|
||||||
self.send_error(404, f"Echo response not found for {key}")
|
self.send_error(404, f"Echo response not found for {key}")
|
||||||
|
|
||||||
|
@ -132,22 +147,12 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
self.handle_echo()
|
self.handle_echo()
|
||||||
|
|
||||||
|
|
||||||
pid_file_path = "http-test-server.pid.txt"
|
def start_server(port, static_directory):
|
||||||
log_file_path = "http-test-server.log"
|
|
||||||
|
|
||||||
|
|
||||||
def run_server(port=8000, static_directory="."):
|
|
||||||
TestHTTPRequestHandler.static_directory = os.path.abspath(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(httpd.socket.getsockname()[1])
|
||||||
print(
|
sys.stdout.flush()
|
||||||
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()))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
@ -156,70 +161,9 @@ def run_server(port=8000, static_directory="."):
|
||||||
finally:
|
finally:
|
||||||
httpd.server_close()
|
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__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Run a test HTTP server")
|
parser = argparse.ArgumentParser(description="Run a HTTP echo server")
|
||||||
parser.add_argument(
|
|
||||||
"-p", "--port", type=int, default=8123, help="Port to run the server on"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d",
|
"-d",
|
||||||
"--directory",
|
"--directory",
|
||||||
|
@ -228,21 +172,12 @@ if __name__ == "__main__":
|
||||||
help="Directory to serve static files from",
|
help="Directory to serve static files from",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-b",
|
"-p",
|
||||||
"--background",
|
"--port",
|
||||||
action="store_true",
|
type=int,
|
||||||
help="Run the server in the background",
|
default=0,
|
||||||
|
help="Port to run the server on",
|
||||||
)
|
)
|
||||||
parser.add_argument("action", choices=["start", "stop"], help="Action to perform")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.action == "start":
|
start_server(port=args.port, static_directory=args.directory)
|
||||||
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()
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ class HTTPTestServer {
|
||||||
this.baseURL = baseURL;
|
this.baseURL = baseURL;
|
||||||
}
|
}
|
||||||
async createEcho(method, path, options) {
|
async createEcho(method, path, options) {
|
||||||
const result = await fetch(`${this.baseURL}/create`, {
|
const result = await fetch(`${this.baseURL}/echo`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue