tor-browser

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

test_viaduct_necko_backend.js (11174B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 /* global add_setup, add_task, Assert, info, do_get_profile, do_timeout, registerCleanupFunction, Services */
      8 
      9 const { HttpServer } = ChromeUtils.importESModule(
     10  "resource://testing-common/httpd.sys.mjs"
     11 );
     12 
     13 const { NetUtil } = ChromeUtils.importESModule(
     14  "resource://gre/modules/NetUtil.sys.mjs"
     15 );
     16 
     17 // Create a test HTTP server
     18 let gHttpServer = null;
     19 let gServerPort = -1;
     20 let gServerURL = "";
     21 
     22 // Track requests received by the server
     23 let gRequestsReceived = [];
     24 
     25 /**
     26 * Helper to decode potentially gzipped request body
     27 */
     28 function decodeRequestBody(request) {
     29  let bodyStream = request.bodyInputStream;
     30  let avail = bodyStream.available();
     31  if (avail === 0) {
     32    return "";
     33  }
     34 
     35  if (
     36    request.hasHeader("content-encoding") &&
     37    request.getHeader("content-encoding") === "gzip"
     38  ) {
     39    return "[gzipped content]";
     40  }
     41 
     42  return NetUtil.readInputStreamToString(bodyStream, avail);
     43 }
     44 
     45 /**
     46 * Wait for requests to arrive and settle.
     47 *
     48 * This waits for at least one request to arrive, then continues waiting
     49 * until no new requests arrive for `settleTime` milliseconds. This ensures
     50 * all pending viaduct-necko operations complete before the test proceeds,
     51 * avoiding race conditions where background requests might still be in flight.
     52 */
     53 async function waitForRequestsToSettle(
     54  settleTime = 300,
     55  timeout = 10000,
     56  interval = 50
     57 ) {
     58  let start = Date.now();
     59 
     60  // First, wait for at least one request to arrive
     61  while (gRequestsReceived.length === 0) {
     62    if (Date.now() - start > timeout) {
     63      throw new Error("Timeout waiting for initial request");
     64    }
     65    await new Promise(resolve => do_timeout(interval, resolve));
     66  }
     67 
     68  // Now wait for requests to settle (no new requests for settleTime ms)
     69  let lastCount = 0;
     70  let lastChangeTime = Date.now();
     71 
     72  while (Date.now() - lastChangeTime < settleTime) {
     73    if (Date.now() - start > timeout) {
     74      info(
     75        `Timeout waiting for requests to settle, proceeding with ${gRequestsReceived.length} requests`
     76      );
     77      break;
     78    }
     79 
     80    if (gRequestsReceived.length !== lastCount) {
     81      lastCount = gRequestsReceived.length;
     82      lastChangeTime = Date.now();
     83    }
     84 
     85    await new Promise(resolve => do_timeout(interval, resolve));
     86  }
     87 
     88  info(`Requests settled with ${gRequestsReceived.length} total requests`);
     89 }
     90 
     91 /**
     92 * Setup function to initialize the HTTP server
     93 */
     94 add_setup(async function () {
     95  info("Setting up viaduct-necko test environment");
     96 
     97  // FOG needs a profile directory to store its data
     98  do_get_profile();
     99 
    100  // Create and start the HTTP server
    101  gHttpServer = new HttpServer();
    102 
    103  // Register various test endpoints
    104  setupTestEndpoints();
    105 
    106  gHttpServer.start(-1);
    107  gServerPort = gHttpServer.identity.primaryPort;
    108  gServerURL = `http://localhost:${gServerPort}`;
    109 
    110  info(`Test HTTP server started on port ${gServerPort}`);
    111 
    112  // Set the telemetry port preference to use our test server
    113  Services.prefs.setIntPref("telemetry.fog.test.localhost_port", gServerPort);
    114 
    115  // Enable telemetry upload (needed for viaduct to be used)
    116  Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true);
    117 
    118  // Initialize FOG/Glean which should trigger viaduct-necko initialization
    119  // This internally calls init_necko_backend() through GkRust_Init
    120  Services.fog.testResetFOG();
    121 
    122  registerCleanupFunction(async () => {
    123    Services.prefs.clearUserPref("telemetry.fog.test.localhost_port");
    124    Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
    125    await new Promise(resolve => gHttpServer.stop(resolve));
    126  });
    127 });
    128 
    129 /**
    130 * Setup test endpoints on the HTTP server
    131 */
    132 function setupTestEndpoints() {
    133  // Glean telemetry submission endpoints
    134  gHttpServer.registerPrefixHandler("/submit/", (request, response) => {
    135    const path = request.path;
    136    info(`Viaduct request received: ${request.method} ${path}`);
    137 
    138    // Read the request body
    139    const body = decodeRequestBody(request);
    140 
    141    // Extract ping type from path
    142    // Path format: /submit/firefox-desktop/{ping-type}/1/{uuid}
    143    const pathParts = path.split("/");
    144    const pingType = pathParts[3] || "unknown";
    145 
    146    gRequestsReceived.push({
    147      path,
    148      method: request.method,
    149      pingType,
    150      body,
    151      bodySize: request.hasHeader("content-length")
    152        ? parseInt(request.getHeader("content-length"), 10)
    153        : body.length,
    154      headers: {
    155        "content-type": request.hasHeader("content-type")
    156          ? request.getHeader("content-type")
    157          : null,
    158        "content-encoding": request.hasHeader("content-encoding")
    159          ? request.getHeader("content-encoding")
    160          : null,
    161        "user-agent": request.hasHeader("user-agent")
    162          ? request.getHeader("user-agent")
    163          : null,
    164      },
    165    });
    166 
    167    // Return a response similar to what a telemetry server would return
    168    response.setStatusLine(request.httpVersion, 200, "OK");
    169    response.setHeader("Server", "TestServer/1.0 (Viaduct Backend)");
    170    response.setHeader("Date", new Date().toUTCString());
    171    response.setHeader("Connection", "close");
    172    response.setHeader("Content-Type", "application/json");
    173 
    174    const responseBody = JSON.stringify({
    175      status: "ok",
    176      message: "Test server response",
    177    });
    178    response.setHeader("Content-Length", responseBody.length.toString());
    179    response.write(responseBody);
    180  });
    181 }
    182 
    183 /**
    184 * Test that the viaduct-necko backend was initialized and is processing requests
    185 */
    186 add_task(async function test_viaduct_backend_working() {
    187  info("Testing viaduct-necko backend initialization and request processing");
    188 
    189  const initialRequestCount = gRequestsReceived.length;
    190  info(
    191    `Already received ${initialRequestCount} requests during initialization`
    192  );
    193 
    194  // Wait for requests to arrive AND settle. This is critical to avoid race
    195  // conditions where background viaduct-necko operations might still be
    196  // completing when subsequent tests run.
    197  await waitForRequestsToSettle();
    198 
    199  // Check that we've received requests through viaduct
    200  Assert.ok(
    201    !!gRequestsReceived.length,
    202    `Viaduct-necko backend is processing requests. Received ${gRequestsReceived.length} requests.`
    203  );
    204 
    205  // Verify the requests are health pings
    206  const healthPings = gRequestsReceived.filter(r => r.pingType === "health");
    207  info(`Received ${healthPings.length} health pings through viaduct-necko`);
    208 
    209  // All telemetry submissions should be POST requests
    210  for (const request of gRequestsReceived) {
    211    Assert.equal(
    212      request.method,
    213      "POST",
    214      `Request to ${request.path} should be POST`
    215    );
    216  }
    217 
    218  // Log summary of what was processed
    219  const pingTypes = [...new Set(gRequestsReceived.map(r => r.pingType))];
    220  info(
    221    `Test successful: Viaduct-necko backend processed ${gRequestsReceived.length} requests`
    222  );
    223  info(`Ping types received: ${pingTypes.join(", ")}`);
    224  info(
    225    `The C++ backend successfully handled requests from Rust through the FFI layer`
    226  );
    227 });
    228 
    229 /**
    230 * Test different HTTP parameters and methods.
    231 * We verify different body sizes and headers are handled correctly.
    232 *
    233 * Note: We don't call testResetFOG() again here because it can hang in chaos
    234 * mode due to viaduct's synchronous waiting pattern combined with main-thread
    235 * dispatch. Instead, we analyze the requests already collected from setup.
    236 */
    237 add_task(async function test_different_parameters() {
    238  info("Testing different HTTP parameters through viaduct-necko");
    239 
    240  const requestCount = gRequestsReceived.length;
    241  info(`Analyzing ${requestCount} requests from previous operations`);
    242 
    243  // Verify different content types and encodings were handled
    244  const contentTypes = new Set();
    245  const contentEncodings = new Set();
    246  const bodySizes = new Set();
    247 
    248  for (const request of gRequestsReceived) {
    249    if (request.headers["content-type"]) {
    250      contentTypes.add(request.headers["content-type"]);
    251    }
    252    if (request.headers["content-encoding"]) {
    253      contentEncodings.add(request.headers["content-encoding"]);
    254    }
    255    if (request.bodySize) {
    256      bodySizes.add(request.bodySize);
    257    }
    258  }
    259 
    260  info(`Content types seen: ${Array.from(contentTypes).join(", ")}`);
    261  info(`Content encodings seen: ${Array.from(contentEncodings).join(", ")}`);
    262  info(
    263    `Body sizes seen: ${Array.from(bodySizes)
    264      .sort((a, b) => a - b)
    265      .join(", ")}`
    266  );
    267 
    268  Assert.ok(
    269    !!gRequestsReceived.length,
    270    "Different parameters were processed successfully"
    271  );
    272 
    273  // Verify we're seeing body sizes (at least one request with a body)
    274  Assert.ok(
    275    bodySizes.size >= 1,
    276    `Body sizes handled: ${Array.from(bodySizes).join(", ")}`
    277  );
    278 });
    279 
    280 /**
    281 * Test that headers are properly passed through the FFI layer
    282 */
    283 add_task(async function test_header_handling() {
    284  info("Testing header handling through viaduct-necko");
    285 
    286  // Check the headers that were sent in previous requests
    287  let hasHeaders = false;
    288  let headerCount = 0;
    289 
    290  for (const request of gRequestsReceived) {
    291    if (request.headers && Object.keys(request.headers).length) {
    292      hasHeaders = true;
    293      const nonNullHeaders = Object.entries(request.headers).filter(
    294        ([, value]) => value !== null
    295      );
    296 
    297      if (nonNullHeaders.length) {
    298        headerCount++;
    299        info(
    300          `Request headers found: ${JSON.stringify(Object.fromEntries(nonNullHeaders))}`
    301        );
    302      }
    303    }
    304  }
    305 
    306  Assert.ok(
    307    hasHeaders,
    308    "Headers are properly transmitted through viaduct-necko"
    309  );
    310 
    311  Assert.ok(headerCount > 0, `Found ${headerCount} requests with headers`);
    312 
    313  // Verify specific headers we expect to see
    314  const hasContentType = gRequestsReceived.some(
    315    r => r.headers && r.headers["content-type"] !== null
    316  );
    317  const hasContentEncoding = gRequestsReceived.some(
    318    r => r.headers && r.headers["content-encoding"] !== null
    319  );
    320  const hasUserAgent = gRequestsReceived.some(
    321    r => r.headers && r.headers["user-agent"] !== null
    322  );
    323 
    324  Assert.ok(hasContentType, "Content-Type header is present");
    325  Assert.ok(hasContentEncoding, "Content-Encoding header is present");
    326  Assert.ok(hasUserAgent, "User-Agent header is present");
    327 
    328  info("Headers are properly handled through the Rust -> C++ -> Necko chain");
    329 });
    330 
    331 /**
    332 * Test configuration validation.
    333 * We verify that the configuration is being passed correctly by checking
    334 * that requests complete successfully.
    335 */
    336 add_task(async function test_configuration_validation() {
    337  info("Validating viaduct-necko configuration");
    338 
    339  // We can verify at least that requests are completing successfully
    340  // which means the configuration isn't breaking anything
    341  const successfulRequests = gRequestsReceived.filter(
    342    r => r.method === "POST" && r.path.includes("/submit/")
    343  );
    344 
    345  Assert.ok(
    346    !!successfulRequests.length,
    347    `Configuration is valid: ${successfulRequests.length} successful requests processed`
    348  );
    349 
    350  info(
    351    "Viaduct-necko backend configuration validated through successful request processing"
    352  );
    353 });