tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit b5306fec84260df46ed6f792be742a3635167da8
parent 913290f972673b925027966f702bb80e49dc8b36
Author: Valentin Gosu <valentin.gosu@gmail.com>
Date:   Wed, 22 Oct 2025 08:01:11 +0000

Bug 1995053 - Add documentation for NodeServer.sys.mjs r=necko-reviewers,jesup

Differential Revision: https://phabricator.services.mozilla.com/D269097

Diffstat:
Anetwerk/docs/NodeServers.md | 733+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/docs/index.md | 1+
2 files changed, 734 insertions(+), 0 deletions(-)

diff --git a/netwerk/docs/NodeServers.md b/netwerk/docs/NodeServers.md @@ -0,0 +1,733 @@ +# Node HTTP Servers for Testing + +This page describes the Node.js-based HTTP server implementation located in `netwerk/test/httpserver/NodeServer.sys.mjs`. This system provides HTTP, HTTPS, HTTP/2, HTTP/3, WebSocket, and proxy servers for use in xpcshell tests. + +## Overview + +The NodeServer system allows tests to spawn Node.js-based HTTP servers that run in separate processes. Unlike the JavaScript-based httpd.sys.mjs server, these Node servers provide full support for modern protocols like HTTP/2, HTTP/3, WebSockets over HTTP/2, and various proxy configurations. + +## Architecture + +The NodeServer system consists of three main components: + +### 1. NodeServer.sys.mjs (Test Interface) + +This is the Firefox/XPCShell side interface that tests use to control Node servers. It provides: + +- Server classes for different protocols (HTTP, HTTPS, HTTP/2, WebSocket, Proxies) +- Methods to start/stop servers +- Methods to execute code in the Node.js context +- Methods to register request handlers + +### 2. runxpcshelltests.py (Test Harness) + +The xpcshell test harness automatically starts the moz-http2.js server when tests run: + +- Spawns `node moz-http2.js` as a subprocess +- Sets the `MOZNODE_EXEC_PORT` environment variable with the server's HTTP port +- Handles server lifecycle (startup/shutdown) + +### 3. moz-http2.js (Node Server) + +This is the main Node.js HTTP/2 server that: + +- Listens on the port specified in `MOZNODE_EXEC_PORT` +- Handles test requests and DNS resolution +- Provides special endpoints for process management: + - `/fork` - Spawns a new Node.js child process + - `/execute/{id}` - Executes code in a forked process + - `/kill/{id}` - Terminates a forked process + - `/forkH3Server` - Spawns an HTTP/3 server + +## How It Works + +### Server Startup Flow + +``` +runxpcshelltests.py + | + v +Spawns node process: node moz-http2/moz-http2.js + | + v +Sets MOZNODE_EXEC_PORT environment variable + | + v +moz-http2.js server starts listening on random port + | + v +Tests can now use NodeServer.sys.mjs to create servers +``` + +### Process Forking Flow + +When a test creates a server (e.g., `new NodeHTTPServer()`): + +``` +Test calls server.start() + | + v +NodeServer.fork() sends POST to http://127.0.0.1:{MOZNODE_EXEC_PORT}/fork + | + v +moz-http2.js receives /fork request + | + v +Calls fork() to spawn moz-http2-child.js + | + v +Returns unique process ID to test + | + v +Test uses NodeServer.execute(id, code) to run code in child process + | + v +Code is sent via POST to /execute/{id} + | + v +moz-http2.js forwards code to child process via IPC + | + v +moz-http2-child.js receives message, runs eval(code) + | + v +Result is sent back through IPC chain to test +``` + +### Code Execution in Child Process + +The child process (moz-http2-child.js) is extremely simple: + +```javascript +process.on("message", msg => { + const code = msg.code; + let evalResult = eval(code); // Execute the code + process.send({ result: evalResult }); // Send result back +}); +``` + +This allows tests to: + +1. Define classes and functions in the Node.js context +2. Start HTTP servers +3. Register request handlers +4. Query server state + +## Server Types + +### NodeHTTPServer + +Basic HTTP/1.1 server. + +```javascript +const { NodeHTTPServer } = ChromeUtils.importESModule( + "resource://testing-common/NodeServer.sys.mjs" +); + +let server = new NodeHTTPServer(); +await server.start(); // Random port +const port = server.port(); +const origin = server.origin(); // http://localhost:{port} + +// Register a path handler +await server.registerPathHandler("/test", (req, resp) => { + resp.writeHead(200); + resp.end("Hello World"); +}); + +// When done +await server.stop(); +``` + +### NodeHTTPSServer + +HTTPS server using HTTP/1.1. + +```javascript +const { NodeHTTPSServer } = ChromeUtils.importESModule( + "resource://testing-common/NodeServer.sys.mjs" +); + +let server = new NodeHTTPSServer(); +await server.start(8443); // Specific port, or 0 for random +// Uses certificate from netwerk/test/unit/http2-cert.pem +``` + +### NodeHTTP2Server + +HTTP/2 over TLS server. + +```javascript +const { NodeHTTP2Server } = ChromeUtils.importESModule( + "resource://testing-common/NodeServer.sys.mjs" +); + +let server = new NodeHTTP2Server(); +await server.start(); +// Supports HTTP/2 specific features like server push, multiplexing + +// Check session count +let count = await server.sessionCount(); +``` + +### HTTP/3 Server + +HTTP/3 (QUIC) server. + +```javascript +const { HTTP3Server } = ChromeUtils.importESModule( + "resource://testing-common/NodeServer.sys.mjs" +); + +let server = new HTTP3Server(); +let path = "/path/to/http3/server/binary"; +let dbPath = "/path/to/quic/database"; +await server.start(path, dbPath); +const port = server.port(); +const masquePort = server.masque_proxy_port(); +``` + +### NodeWebSocketServer + +WebSocket server over HTTPS. + +```javascript +const { NodeWebSocketServer } = ChromeUtils.importESModule( + "resource://testing-common/NodeServer.sys.mjs" +); + +let server = new NodeWebSocketServer(); +await server.start(); + +// Register custom message handler +await server.registerMessageHandler((data, ws) => { + ws.send("Echo: " + data); +}); +``` + +### NodeWebSocketHttp2Server + +WebSocket over HTTP/2 (RFC 8441). + +```javascript +const { NodeWebSocketHttp2Server } = ChromeUtils.importESModule( + "resource://testing-common/NodeServer.sys.mjs" +); + +let server = new NodeWebSocketHttp2Server(); +await server.start(0, false); // port, fallbackToH1 +``` + +### Proxy Servers + +```javascript +const { NodeHTTPProxyServer, NodeHTTPSProxyServer, NodeHTTP2ProxyServer } = + ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs"); + +// HTTP proxy +let httpProxy = new NodeHTTPProxyServer(); +await httpProxy.start(); + +// HTTPS proxy +let httpsProxy = new NodeHTTPSProxyServer(); +await httpsProxy.start(); + +// HTTP/2 proxy +let http2Proxy = new NodeHTTP2ProxyServer(); +await http2Proxy.start(0, true, 100); // port, auth, maxConcurrentStreams +``` + +## Advanced Usage + +### Registering Path Handlers + +Path handlers are functions that process requests for specific paths: + +```javascript +await server.registerPathHandler("/api/data", (req, resp) => { + // req is Node's http.IncomingMessage + // resp is Node's http.ServerResponse + + resp.setHeader("Content-Type", "application/json"); + resp.writeHead(200); + resp.end(JSON.stringify({ status: "ok" })); +}); +``` + +### Executing Arbitrary Code + +You can execute any JavaScript code in the Node.js context: + +```javascript +// Define a function +await server.execute(` + function customHandler(req, resp) { + resp.writeHead(200); + resp.end("Custom response"); + } +`); + +// Use the function +await server.execute(`global.path_handlers["/custom"] = customHandler`); + +// Query state +let result = await server.execute(`Object.keys(global.path_handlers).length`); +``` + +### Passing Functions + +You can pass JavaScript functions directly: + +```javascript +function myHandler(req, resp) { + resp.writeHead(200); + resp.end("Handler from test"); +} + +// The function is serialized and defined in the Node context +await server.execute(myHandler); + +// Now call it +await server.execute(`myHandler(someReq, someResp)`); +``` + +### Working with Global State + +The Node.js child processes maintain global state: + +```javascript +// Set up global variables +await server.execute(`global.requestCount = 0;`); + +// Use in handlers +await server.registerPathHandler("/count", (req, resp) => { + global.requestCount++; + resp.writeHead(200); + resp.end(`Request ${global.requestCount}`); +}); + +// Query state +let count = await server.execute(`global.requestCount`); +``` + +## Android Support + +The system includes ADB port forwarding support for Android testing: + +```javascript +// Automatically handled when MOZ_ANDROID_DATA_DIR is set +// The ADB class in NodeServer.sys.mjs forwards ports using: +// adb reverse tcp:{port} tcp:{port} +``` + +This means xpcshell-tests on Android can pretend to connect to `localhost:${port}` while the node server actually runs on the host. + +## Certificate Handling + +HTTPS and HTTP/2 servers automatically install test certificates: + +- Certificate: `netwerk/test/unit/http2-cert.pem` +- CA: `netwerk/test/unit/http2-ca.pem` +- Key: `netwerk/test/unit/http2-cert.key` + +Proxy servers use different certificates: + +- Certificate: `netwerk/test/unit/proxy-cert.pem` +- CA: `netwerk/test/unit/proxy-ca.pem` +- Key: `netwerk/test/unit/proxy-cert.key` + +To skip automatic certificate installation: + +```javascript +let server = new NodeHTTPSServer(); +server._skipCert = true; +await server.start(); +``` + +The certificates are valid for the following domains: `localhost`, `foo.example.com`, `alt1.example.com`, `alt2.example.com` +Check `http2-cert.pem.certspec` and `proxy-cert.pem.certspec` for the up to date information. + +If you need the certs to be valid for more domains, consider using: +```javascript +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true); +``` + +## Best Practices + +### Always Stop Servers + +Always stop servers in cleanup to avoid resource leaks: + +```javascript +registerCleanupFunction(async () => { + await server.stop(); +}); +``` + +### Use Random Ports + +Use port 0 (or omit the port parameter) to get a random available port: + +```javascript +await server.start(); // Random port +// NOT: await server.start(8080); // Fixed port causes conflicts +``` + +### Helper Function for Multiple Server Types + +Use the `with_node_servers` helper to test multiple server types: + +```javascript +const { with_node_servers, NodeHTTPServer, NodeHTTP2Server } = + ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs"); + +await with_node_servers( + [NodeHTTPServer, NodeHTTP2Server], + async server => { + // This runs once for each server type + let response = await fetch(server.origin() + "/test"); + // ... test code ... + } +); +// Servers are automatically stopped +``` + +### Error Handling + +Wrap server operations that may fail in try-catch blocks: + +```javascript +try { + await server.execute(` + global.server.listen(port); + `); +} catch (e) { + // Handle execution errors + console.error("Server setup failed:", e); +} +``` + +### Debugging + +To debug issues, you can inspect the Node.js process: + +```javascript +// Log in Node context +await server.execute(`console.log("Debug info:", someVariable)`); + +// Check the xpcshell test output for Node.js console.log output +``` + +## Example Tests + +### Simple HTTP Server Test + +```javascript +add_task(async function test_simple_http_server() { + let server = new NodeHTTPServer(); + await server.start(); + + registerCleanupFunction(async () => { + await server.stop(); + }); + + await server.registerPathHandler("/hello", (req, resp) => { + resp.writeHead(200, { "Content-Type": "text/plain" }); + resp.end("Hello, World!"); + }); + + let response = await fetch(server.origin() + "/hello"); + let text = await response.text(); + Assert.equal(text, "Hello, World!"); +}); +``` + +### HTTP/2 Server Test + +```javascript +add_task(async function test_http2_multiplexing() { + let server = new NodeHTTP2Server(); + await server.start(); + + registerCleanupFunction(async () => { + await server.stop(); + }); + + await server.registerPathHandler("/data", (req, resp) => { + resp.writeHead(200); + resp.end("data"); + }); + + // Make multiple requests + let responses = await Promise.all([ + fetch(server.origin() + "/data"), + fetch(server.origin() + "/data"), + fetch(server.origin() + "/data"), + ]); + + // All requests should use the same HTTP/2 session + let sessionCount = await server.sessionCount(); + Assert.equal(sessionCount, 1, "Should reuse single HTTP/2 session"); +}); +``` + +### WebSocket Test + +```javascript +add_task(async function test_websocket() { + let server = new NodeWebSocketServer(); + await server.start(); + + registerCleanupFunction(async () => { + await server.stop(); + }); + + await server.registerMessageHandler((data, ws) => { + ws.send("Echo: " + data); + }); + + let wsc = new WebSocketConnection(); + await wsc.open(server.origin().replace("https", "wss") + "/"); + wsc.send("test message"); + + let messages = await wsc.receiveMessages(); + Assert.equal(messages[0], "Echo: test message"); + + wsc.close(); + await wsc.finished(); +}); +``` + +### Proxy Test + +```javascript +add_task(async function test_http_proxy() { + let proxy = new NodeHTTPProxyServer(); + await proxy.start(); + + registerCleanupFunction(async () => { + await proxy.stop(); + }); + + // Proxy filter is automatically registered + // All HTTP requests will now go through the proxy + + let response = await fetch("http://example.com/"); + Assert.equal(response.status, 200); +}); +``` + +### Async State Management Test + +This test demonstrates concurrent async operations with proper result routing: + +```javascript +add_task(async function test_async_state_management() { + let server = new NodeHTTP2Server(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + + // Initialize state in the Node.js context + await server.execute(`global.asyncResults = [];`); + + // Define an async function that takes time to complete + await server.execute(` + global.asyncCounter = 0; + global.performAsyncOperation = function(delay, value) { + return new Promise(resolve => { + setTimeout(() => { + global.asyncCounter++; + global.asyncResults.push({ counter: global.asyncCounter, value }); + resolve({ counter: global.asyncCounter, value }); + }, delay); + }); + }; + `); + + // Launch two concurrent async operations with different delays + let op1 = server.execute(`performAsyncOperation(100, "first")`); + let op2 = server.execute(`performAsyncOperation(50, "second")`); + + // Wait for both to complete + let result1 = await op1; + let result2 = await op2; + + // op2 completes first (50ms delay) so it gets counter=1 + equal(result2.counter, 1); + equal(result2.value, "second"); + + // op1 completes second (100ms delay) so it gets counter=2 + equal(result1.counter, 2); + equal(result1.value, "first"); + + // Verify the global state was updated correctly + let results = await server.execute(`global.asyncResults`); + equal(results.length, 2); + equal(results[0].value, "second"); // First to complete + equal(results[1].value, "first"); // Second to complete + + let counter = await server.execute(`global.asyncCounter`); + equal(counter, 2); + + await server.stop(); +}); +``` + +This test demonstrates: +- Multiple concurrent `execute()` calls on the same server +- Each operation receives its correct result despite different completion times +- Global state is properly shared across executions +- The message handler system correctly routes responses to their respective promises + +## Common Pitfalls + +### Not Awaiting Async Operations + +All server operations are asynchronous: + +```javascript +// WRONG +server.start(); +server.registerPathHandler("/test", handler); + +// CORRECT +await server.start(); +await server.registerPathHandler("/test", handler); +``` + +### Forgetting to Stop Servers + +Servers must be explicitly stopped: + +```javascript +// WRONG +add_task(async function test() { + let server = new NodeHTTPServer(); + await server.start(); + // ... test code ... + // Server is never stopped! +}); + +// CORRECT +add_task(async function test() { + let server = new NodeHTTPServer(); + await server.start(); + registerCleanupFunction(async () => { + await server.stop(); + }); + // ... test code ... +}); +``` + +### Hardcoded Ports + +Avoid hardcoded ports as they can cause conflicts when tests run in parallel: + +```javascript +// WRONG +await server.start(8080); + +// CORRECT +await server.start(); // or await server.start(0); +let port = server.port(); +``` + +### Scope Issues in Handlers + +Remember that handlers run in the Node.js context, not the test context: + +```javascript +// WRONG - testVariable is not accessible in Node.js +let testVariable = "value"; +await server.registerPathHandler("/test", (req, resp) => { + resp.end(testVariable); // ERROR: testVariable is undefined +}); + +// CORRECT - Pass values explicitly +let testVariable = "value"; +await server.execute(`global.sharedValue = "${testVariable}"`); +await server.registerPathHandler("/test", (req, resp) => { + resp.end(global.sharedValue); +}); +``` + +## Implementation Details + +### Process IDs + +When you call `NodeServer.fork()`, the moz-http2.js server generates a random 6-character process ID. This ID is used to route commands to the correct child process. + +### Communication Protocol + +Communication uses HTTP POST requests with JSON payloads: + +``` +POST /execute/{processId} +Body: JavaScript code to execute + +Response: { "result": <return value>, "error": "", "errorStack": "", "messageId": <id> } +``` + +### Message Handler System + +The system uses a message handler architecture to support concurrent async operations: + +1. Each `/execute/{processId}` request generates a unique 6-character `messageId` +2. A promise handler is stored in `forked.messageHandlers[messageId] = { resolve, reject }` +3. The `messageId` is sent to the child process along with the code +4. The child process returns the result with the same `messageId` +5. The response is routed to the correct promise handler using the `messageId` + +This design allows multiple async operations to run concurrently on the same child process without interfering with each other. For example, you can call `server.execute()` multiple times in parallel and each will properly receive its own result. + +### Eval-based Execution + +Code execution uses `eval()` in the child process: + +```javascript +// In moz-http2-child.js +process.on("message", msg => { + const code = msg.code; + const messageId = msg.messageId; + let evalResult = eval(code); + if (evalResult instanceof Promise) { + evalResult + .then(x => process.send({ result: x, messageId })) + .catch(e => process.send({ error: e.toString(), messageId })); + } else { + process.send({ result: evalResult, messageId }); + } +}); +``` + +This allows executing: + +- Variable declarations +- Function definitions +- Expressions +- Async operations (Promise returns are handled automatically) +- Concurrent async operations without conflicts + +### Function Serialization + +When you pass a function to `execute()`, it's serialized: + +```javascript +// You pass: +function handler(req, resp) { resp.end("ok"); } + +// The system sends: +"handler = function handler(req, resp) { resp.end(\"ok\"); };" +``` + +## See Also + +- `netwerk/test/httpserver/nsIHttpServer.idl` - JavaScript HTTP server +- `testing/xpcshell/moz-http2/moz-http2.js` - Node HTTP/2 server implementation +- `netwerk/test/unit/` - Example tests using NodeServer +- `netwerk/docs/http_server_for_testing.rst` - JavaScript-based httpd.sys.mjs server diff --git a/netwerk/docs/index.md b/netwerk/docs/index.md @@ -27,6 +27,7 @@ connectivity_checking.md :maxdepth: 1 network_test_guidelines.md http_server_for_testing +NodeServers.md mochitest_with_http3.md ```