tor-browser

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

test_dictionary_retrieval.js (15738B)


      1 /**
      2 * Tests for HTTP Compression Dictionary retrieval functionality
      3 * - Dictionary lookup by origin and pattern matching
      4 * - Available-Dictionary header generation and formatting
      5 * - Dictionary cache hit/miss scenarios
      6 * - Dictionary precedence and selection logic
      7 */
      8 
      9 "use strict";
     10 
     11 // Load cache helpers
     12 Services.scriptloader.loadSubScript("resource://test/head_cache.js", this);
     13 
     14 const { NodeHTTPSServer } = ChromeUtils.importESModule(
     15  "resource://testing-common/NodeServer.sys.mjs"
     16 );
     17 
     18 // Test dictionaries with different patterns and priorities
     19 const RETRIEVAL_TEST_DICTIONARIES = {
     20  api_v1: {
     21    id: "api-v1-dict",
     22    content: "API_V1_COMMON_DATA",
     23    pattern: "/api/v1/*",
     24    type: "raw",
     25  },
     26  api_generic: {
     27    id: "api-generic-dict",
     28    content: "API_GENERIC_DATA",
     29    pattern: "/api/*",
     30    type: "raw",
     31  },
     32  wildcard: {
     33    id: "wildcard-dict",
     34    content: "WILDCARD_DATA",
     35    pattern: "*",
     36    type: "raw",
     37  },
     38  js_files: {
     39    id: "js-dict",
     40    content: "JS_COMMON_CODE",
     41    pattern: "*.js",
     42    type: "raw",
     43  },
     44 };
     45 
     46 let server = null;
     47 let requestLog = []; // Track requests for verification
     48 
     49 async function sync_to_server() {
     50  if (server.processId) {
     51    await server.execute(`global.requestLog = ${JSON.stringify(requestLog)};`);
     52  } else {
     53    dump("Server not running?\n");
     54  }
     55 }
     56 
     57 async function sync_from_server() {
     58  if (server.processId) {
     59    requestLog = await server.execute(`global.requestLog`);
     60  } else {
     61    dump("Server not running? (from)\n");
     62  }
     63 }
     64 
     65 add_setup(async function () {
     66  Services.prefs.setBoolPref("network.http.dictionaries.enable", true);
     67  if (!server) {
     68    server = await setupServer();
     69  }
     70  // Setup baseline dictionaries for compression testing
     71 
     72  // Clear any existing cache
     73  let lci = Services.loadContextInfo.custom(false, {
     74    partitionKey: `(https,localhost)`,
     75  });
     76  evict_cache_entries("all", lci);
     77 });
     78 
     79 // Calculate expected SHA-256 hash for dictionary content
     80 async function calculateDictionaryHash(content) {
     81  let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
     82    Ci.nsICryptoHash
     83  );
     84  hasher.init(Ci.nsICryptoHash.SHA256);
     85  let bytes = new TextEncoder().encode(content);
     86  hasher.update(bytes, bytes.length);
     87  let hash = hasher.finish(false);
     88  return btoa(hash); // Convert to base64
     89 }
     90 
     91 // Setup dictionary test server
     92 async function setupServer() {
     93  if (!server) {
     94    server = new NodeHTTPSServer();
     95    await server.start();
     96 
     97    registerCleanupFunction(async () => {
     98      try {
     99        await server.stop();
    100      } catch (e) {
    101        // Ignore server stop errors during cleanup
    102      }
    103    });
    104  }
    105  return server;
    106 }
    107 
    108 // Create channel for dictionary requests
    109 function makeChan(url) {
    110  let chan = NetUtil.newChannel({
    111    uri: url,
    112    loadUsingSystemPrincipal: true,
    113    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    114  }).QueryInterface(Ci.nsIHttpChannel);
    115  return chan;
    116 }
    117 
    118 function channelOpenPromise(chan) {
    119  return new Promise(resolve => {
    120    function finish(req, buffer) {
    121      resolve([req, buffer]);
    122    }
    123    chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL));
    124  });
    125 }
    126 
    127 // Verify dictionary is stored in cache
    128 function verifyDictionaryStored(url, shouldExist, callback) {
    129  let lci = Services.loadContextInfo.custom(false, {
    130    partitionKey: `(https,localhost)`,
    131  });
    132  asyncCheckCacheEntryPresence(url, "disk", shouldExist, lci, callback);
    133 }
    134 
    135 // Setup server endpoint that expects specific dictionary headers
    136 async function registerDictionaryAwareEndpoint(
    137  httpServer,
    138  path,
    139  responseContent
    140 ) {
    141  // We have to put all values and functions referenced in the handler into
    142  // this string which will be turned into a function for the handler, because
    143  // NodeHTTPSServer handlers can't access items in the local or global scopes of the
    144  // containing file
    145  let func = `
    146    // Log the request for analysis
    147    global.requestLog[global.requestLog.length] = {
    148      path: "${path}",
    149      hasAvailableDict: request.headers['available-dictionary'] !== undefined,
    150      availableDict: request.headers['available-dictionary'] || null
    151    };
    152 
    153    response.writeHead(200, {
    154      "Content-Type": "text/plain",
    155    });
    156    response.end("${responseContent}", "binary");
    157 `;
    158  let handler = new Function("request", "response", func);
    159  return httpServer.registerPathHandler(path, handler);
    160 }
    161 
    162 // Setup retrieval test server with dictionaries and resources
    163 async function setupRetrievalTestServer() {
    164  await setupServer();
    165 
    166  // Dictionary endpoints - store dictionaries with different patterns
    167  await server.registerPathHandler(
    168    "/dict/api-v1",
    169    function (request, response) {
    170      const RETRIEVAL_TEST_DICTIONARIES = {
    171        api_v1: {
    172          id: "api-v1-dict",
    173          content: "API_V1_COMMON_DATA",
    174          pattern: "/api/v1/*",
    175          type: "raw",
    176        },
    177      };
    178      let dict = RETRIEVAL_TEST_DICTIONARIES.api_v1;
    179      response.writeHead(200, {
    180        "Content-Type": "application/octet-stream",
    181        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    182        "Cache-Control": "max-age=3600",
    183      });
    184      response.end(dict.content, "binary");
    185    }
    186  );
    187 
    188  await server.registerPathHandler(
    189    "/dict/api-generic",
    190    function (request, response) {
    191      const RETRIEVAL_TEST_DICTIONARIES = {
    192        api_generic: {
    193          id: "api-generic-dict",
    194          content: "API_GENERIC_DATA",
    195          pattern: "/api/*",
    196          type: "raw",
    197        },
    198      };
    199      let dict = RETRIEVAL_TEST_DICTIONARIES.api_generic;
    200      response.writeHead(200, {
    201        "Content-Type": "application/octet-stream",
    202        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    203        "Cache-Control": "max-age=3600",
    204      });
    205      response.end(dict.content, "binary");
    206    }
    207  );
    208 
    209  await server.registerPathHandler("/wildcard", function (request, response) {
    210    const RETRIEVAL_TEST_DICTIONARIES = {
    211      wildcard: {
    212        id: "wildcard-dict",
    213        content: "WILDCARD_DATA",
    214        pattern: "*",
    215        type: "raw",
    216      },
    217    };
    218    let dict = RETRIEVAL_TEST_DICTIONARIES.wildcard;
    219    response.writeHead(200, {
    220      "Content-Type": "application/octet-stream",
    221      "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    222      "Cache-Control": "max-age=3600",
    223    });
    224    response.end(dict.content, "binary");
    225  });
    226 
    227  await server.registerPathHandler("/js", function (request, response) {
    228    const RETRIEVAL_TEST_DICTIONARIES = {
    229      js_files: {
    230        id: "js-dict",
    231        content: "JS_COMMON_CODE",
    232        pattern: "*.js",
    233        type: "raw",
    234      },
    235    };
    236    let dict = RETRIEVAL_TEST_DICTIONARIES.js_files;
    237    response.writeHead(200, {
    238      "Content-Type": "application/octet-stream",
    239      "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    240      "Cache-Control": "max-age=3600",
    241    });
    242    response.end(dict.content, "binary");
    243  });
    244 
    245  // Resource endpoints that should trigger dictionary retrieval
    246  await registerDictionaryAwareEndpoint(
    247    server,
    248    "/api/v1/users",
    249    "API V1 USERS DATA"
    250  );
    251  await registerDictionaryAwareEndpoint(
    252    server,
    253    "/api/v2/posts",
    254    "API V2 POSTS DATA"
    255  );
    256  await registerDictionaryAwareEndpoint(
    257    server,
    258    "/api/generic",
    259    "GENERIC API DATA"
    260  );
    261  await registerDictionaryAwareEndpoint(server, "/web/page", "WEB PAGE DATA");
    262  await registerDictionaryAwareEndpoint(
    263    server,
    264    "/scripts/app.js",
    265    "JAVASCRIPT CODE"
    266  );
    267  await registerDictionaryAwareEndpoint(
    268    server,
    269    "/styles/main.css",
    270    "CSS STYLES"
    271  );
    272 
    273  return server;
    274 }
    275 
    276 // Setup baseline dictionaries for retrieval testing
    277 add_task(async function test_setup_dictionaries() {
    278  await setupRetrievalTestServer();
    279 
    280  // Clear any existing cache
    281  let lci = Services.loadContextInfo.custom(false, {
    282    partitionKey: `(https,localhost)`,
    283  });
    284  evict_cache_entries("all", lci);
    285  requestLog = [];
    286  await sync_to_server();
    287 
    288  // Store all test dictionaries
    289  const dictPaths = ["/dict/api-v1", "/dict/api-generic", "/wildcard", "/js"];
    290 
    291  for (let path of dictPaths) {
    292    let url = `https://localhost:${server.port()}${path}`;
    293 
    294    let chan = makeChan(url);
    295    let [, data] = await channelOpenPromise(chan);
    296    dump(`**** Dictionary loaded: ${path}, data length: ${data.length}\n`);
    297 
    298    // Verify dictionary was stored
    299    await new Promise(resolve => {
    300      verifyDictionaryStored(url, true, resolve);
    301    });
    302  }
    303  dump("**** Setup complete\n");
    304 });
    305 
    306 // Test basic dictionary lookup and Available-Dictionary header generation
    307 add_task(async function test_basic_dictionary_retrieval() {
    308  let url = `https://localhost:${server.port()}/api/v1/users`;
    309  requestLog = [];
    310  await sync_to_server();
    311 
    312  // Calculate expected hash for api_v1 dictionary
    313  let expectedHash = await calculateDictionaryHash(
    314    RETRIEVAL_TEST_DICTIONARIES.api_v1.content
    315  );
    316 
    317  let chan = makeChan(url);
    318  let [, data] = await channelOpenPromise(chan);
    319 
    320  Assert.equal(data, "API V1 USERS DATA", "Resource content matches");
    321 
    322  // Check request log to see if Available-Dictionary header was sent
    323  await sync_from_server();
    324  let logEntry = requestLog.find(entry => entry.path === "/api/v1/users");
    325  Assert.ok(logEntry && logEntry.hasAvailableDict, "Has Available-Dictionary");
    326  Assert.ok(
    327    logEntry.availableDict.includes(expectedHash),
    328    "Available-Dictionary header should contain expected hash"
    329  );
    330  dump("**** Basic retrieval test complete\n");
    331 });
    332 
    333 // Test URL pattern matching logic for dictionary selection
    334 add_task(async function test_dictionary_pattern_matching() {
    335  const patternMatchTests = [
    336    { url: "/api/v1/users", expectedPattern: "/api/v1/*", dictKey: "api_v1" },
    337    { url: "/api/v2/posts", expectedPattern: "/api/*", dictKey: "api_generic" },
    338    { url: "/api/generic", expectedPattern: "/api/*", dictKey: "api_generic" },
    339    { url: "/scripts/app.js", expectedPattern: "*.js", dictKey: "js_files" },
    340    { url: "/web/page", expectedPattern: "*", dictKey: "wildcard" }, // Only wildcard should match
    341    { url: "/styles/main.css", expectedPattern: "*", dictKey: "wildcard" },
    342  ];
    343 
    344  requestLog = [];
    345  await sync_to_server();
    346 
    347  for (let test of patternMatchTests) {
    348    let url = `https://localhost:${server.port()}${test.url}`;
    349    let expectedDict = RETRIEVAL_TEST_DICTIONARIES[test.dictKey];
    350    let expectedHash = await calculateDictionaryHash(expectedDict.content);
    351 
    352    let chan = makeChan(url);
    353    let [, data] = await channelOpenPromise(chan);
    354 
    355    Assert.greater(data.length, 0, `Resource ${test.url} should have content`);
    356 
    357    // Check request log
    358    await sync_from_server();
    359    let logEntry = requestLog.find(entry => entry.path === test.url);
    360    Assert.ok(
    361      logEntry && logEntry.hasAvailableDict,
    362      `Available-Dictionary header should be present for ${test.url}`
    363    );
    364    if (logEntry && logEntry.hasAvailableDict) {
    365      Assert.ok(
    366        logEntry.availableDict.includes(expectedHash),
    367        `Available-Dictionary header should contain expected hash for ${test.url}`
    368      );
    369    }
    370  }
    371 });
    372 
    373 // Test dictionary precedence when multiple patterns match
    374 add_task(async function test_dictionary_precedence() {
    375  // Test URL that matches multiple patterns: /api/v1/users
    376  // Should match: "/api/v1/*" (most specific), "/api/*", "*" (wildcard)
    377  // Most specific pattern should take precedence
    378 
    379  let url = `https://localhost:${server.port()}/api/v1/users`;
    380  requestLog = [];
    381  await sync_to_server();
    382 
    383  let mostSpecificHash = await calculateDictionaryHash(
    384    RETRIEVAL_TEST_DICTIONARIES.api_v1.content
    385  );
    386 
    387  let chan = makeChan(url);
    388  let [, data] = await channelOpenPromise(chan);
    389 
    390  Assert.equal(data, "API V1 USERS DATA", "Content should match");
    391 
    392  // Check request log for precedence
    393  await sync_from_server();
    394  let logEntry = requestLog.find(entry => entry.path === "/api/v1/users");
    395  Assert.ok(
    396    logEntry && logEntry.hasAvailableDict,
    397    "Available-Dictionary header should be present for precedence test"
    398  );
    399  if (logEntry && logEntry.hasAvailableDict) {
    400    // The most specific pattern (/api/v1/*) should be included
    401    // Implementation may include multiple matching dictionaries
    402    Assert.ok(
    403      logEntry.availableDict.includes(mostSpecificHash),
    404      "Available-Dictionary header should contain most specific pattern hash"
    405    );
    406  }
    407 });
    408 
    409 // Test successful dictionary lookup and usage
    410 add_task(async function test_dictionary_cache_hit() {
    411  let url = `https://localhost:${server.port()}/api/generic`;
    412  requestLog = [];
    413  await sync_to_server();
    414 
    415  let expectedHash = await calculateDictionaryHash(
    416    RETRIEVAL_TEST_DICTIONARIES.api_generic.content
    417  );
    418 
    419  let chan = makeChan(url);
    420  let [, data] = await channelOpenPromise(chan);
    421 
    422  Assert.equal(data, "GENERIC API DATA", "Content should match");
    423 
    424  // Verify dictionary lookup succeeded
    425  await sync_from_server();
    426  let logEntry = requestLog.find(entry => entry.path === "/api/generic");
    427  Assert.ok(
    428    logEntry && logEntry.hasAvailableDict,
    429    "Available-Dictionary header should be present for cache hit"
    430  );
    431  if (logEntry && logEntry.hasAvailableDict) {
    432    Assert.ok(
    433      logEntry.availableDict.includes(expectedHash),
    434      "Available-Dictionary header should contain expected hash for cache hit"
    435    );
    436  }
    437 });
    438 
    439 // Test Available-Dictionary header hash format compliance
    440 add_task(async function test_dictionary_hash_format() {
    441  // Test that dictionary hashes follow IETF spec format: :base64hash:
    442 
    443  let testDict = RETRIEVAL_TEST_DICTIONARIES.api_v1;
    444  let calculatedHash = await calculateDictionaryHash(testDict.content);
    445 
    446  // Verify hash is base64 format
    447  Assert.greater(calculatedHash.length, 0, "Hash should not be empty");
    448 
    449  // Verify base64 pattern (rough check)
    450  let base64Pattern = /^[A-Za-z0-9+/]*={0,2}$/;
    451  Assert.ok(base64Pattern.test(calculatedHash), "Hash should be valid base64");
    452 
    453  // The hash format should be structured field byte sequence: :base64:
    454  let structuredFieldFormat = `:${calculatedHash}:`;
    455  Assert.ok(
    456    structuredFieldFormat.includes(calculatedHash),
    457    "Hash should follow structured field format"
    458  );
    459 });
    460 
    461 // Test retrieval with multiple dictionary matches
    462 add_task(async function test_multiple_dictionary_matches() {
    463  // Create a request that could match multiple dictionaries
    464  let url = `https://localhost:${server.port()}/api/test`;
    465  requestLog = [];
    466  await sync_to_server();
    467 
    468  await registerDictionaryAwareEndpoint(server, "/api/test", "API TEST DATA");
    469 
    470  let apiGenericHash = await calculateDictionaryHash(
    471    RETRIEVAL_TEST_DICTIONARIES.api_generic.content
    472  );
    473  let chan = makeChan(url);
    474  let [, data] = await channelOpenPromise(chan);
    475 
    476  Assert.equal(data, "API TEST DATA", "Content should match");
    477 
    478  // Check for multiple dictionary hashes in Available-Dictionary header
    479  await sync_from_server();
    480  let logEntry = requestLog.find(entry => entry.path === "/api/test");
    481  Assert.ok(
    482    logEntry && logEntry.hasAvailableDict,
    483    "Available-Dictionary header should be present for multiple matches"
    484  );
    485  if (logEntry && logEntry.hasAvailableDict) {
    486    // Could match both /api/* and * patterns - verify the longest pattern's hash is present
    487    // (IETF spec says the longest match should be used)
    488    let hasApiGenericHash = logEntry.availableDict.includes(apiGenericHash);
    489    Assert.ok(
    490      hasApiGenericHash,
    491      "Available-Dictionary header should contain at least one expected hash for multiple matches"
    492    );
    493  }
    494 });
    495 
    496 // Cleanup
    497 add_task(async function cleanup() {
    498  // Clear cache
    499  let lci = Services.loadContextInfo.custom(false, {
    500    partitionKey: `(https,localhost)`,
    501  });
    502  evict_cache_entries("all", lci);
    503 });