tor-browser

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

test_dictionary_compression_dcb.js (37175B)


      1 /**
      2 * Tests for HTTP Compression Dictionary Brotli (dcb) compression functionality
      3 * - Dictionary-based Brotli compression and decompression
      4 * - Content integrity verification with dcb encoding
      5 * - Available-Dictionary header integration for compression
      6 * - Error handling for missing/invalid dictionaries
      7 * - Compression window size limits and edge cases
      8 */
      9 
     10 "use strict";
     11 
     12 // Load cache helpers
     13 Services.scriptloader.loadSubScript("resource://test/head_cache.js", this);
     14 
     15 const { NodeHTTPSServer } = ChromeUtils.importESModule(
     16  "resource://testing-common/NodeServer.sys.mjs"
     17 );
     18 
     19 // Test dictionaries optimized for compression testing
     20 // Since we're not actually brotli-encoding, all decodes will yield 15 bytes
     21 const DCB_TEST_DICTIONARIES = {
     22  html_common: {
     23    id: "html-dict",
     24    content:
     25      '<html><head><title>Common HTML Template</title></head><body><div class="container"><p>',
     26    expected_length: 15,
     27    pattern: "*.html",
     28    type: "raw",
     29  },
     30  html_common_no_dictionary: {
     31    id: "html-dict",
     32    content:
     33      '<html><head><title>Common HTML Template</title></head><body><div class="container"><p>',
     34    expected_length: 196,
     35    pattern: "*.html",
     36    type: "raw",
     37  },
     38  api_json: {
     39    id: "api-dict",
     40    content:
     41      '{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
     42    expected_length: 15,
     43    pattern: "/api/*",
     44    type: "raw",
     45  },
     46  api_v1: {
     47    id: "longer-match-dict",
     48    content:
     49      '{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
     50    expected_length: 15,
     51    pattern: "/api/v1/*",
     52    type: "raw",
     53  },
     54  js_common: {
     55    id: "js-dict",
     56    content:
     57      "function(){return this;};var=function();const=function();let=function();",
     58    expected_length: 15,
     59    pattern: "*.js",
     60    type: "raw",
     61  },
     62  large_dict: {
     63    id: "large-dict",
     64    content: "REPEATED_PATTERN_".repeat(1000), // ~1.5MB dictionary
     65    expected_length: 15,
     66    pattern: "/large/*",
     67    type: "raw",
     68  },
     69 };
     70 
     71 // Test content designed to compress well with dictionaries
     72 const DCB_TEST_CONTENT = {
     73  html_page:
     74    '<html><head><title>Test Page</title></head><body><div class="container"><p>This is test content that should compress well with the HTML dictionary.</p><p>More content here.</p></div></body></html>',
     75 
     76  api_response:
     77    '{"status":"success","data":{"id":12345,"name":"Test User","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-02T12:00:00Z"},"message":"User retrieved successfully","errors":[]}',
     78 
     79  api_v1:
     80    '{"status":"success","data":{"id":12345,"name":"Test User","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-02T12:00:00Z"},"message":"User retrieved successfully","errors":[]}',
     81 
     82  js_code:
     83    'function testFunction(){return this.value;};var result=function(){console.log("test");};const API_URL=function(){return "https://api.example.com";};let userData=function(){return {id:1,name:"test"};}',
     84 
     85  large_content: "REPEATED_PATTERN_DATA_CHUNK_".repeat(50000), // Content that will compress well with large dictionary
     86 
     87  jpeg: "ARBITRARY_DATA_".repeat(1000),
     88 };
     89 
     90 let server = null;
     91 let requestLog = []; // Track requests for verification
     92 
     93 // Create channel for dictionary requests
     94 function makeChan(url) {
     95  let chan = NetUtil.newChannel({
     96    uri: url,
     97    loadUsingSystemPrincipal: true,
     98    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
     99  }).QueryInterface(Ci.nsIHttpChannel);
    100  return chan;
    101 }
    102 
    103 function channelOpenPromise(chan) {
    104  return new Promise(resolve => {
    105    function finish(req, buffer) {
    106      resolve([req, buffer]);
    107    }
    108    // CL_EXPECT_GZIP is needed if we're transferring compressed data; else it asserts content-length
    109    // equals the data length.  (We could also not send content-length)
    110    chan.asyncOpen(
    111      new ChannelListener(
    112        finish,
    113        null,
    114        CL_ALLOW_UNKNOWN_CL | CL_IGNORE_DELAYS | CL_EXPECT_GZIP
    115      )
    116    );
    117  });
    118 }
    119 
    120 // Setup DCB test server with dictionaries and compressed content endpoints
    121 async function setupDCBTestServer() {
    122  let httpServer = new NodeHTTPSServer();
    123  await httpServer.start();
    124 
    125  // Dictionary endpoints - store dictionaries for later compression use
    126  await httpServer.registerPathHandler(
    127    "/dict/html",
    128    function (request, response) {
    129      const DCB_TEST_DICTIONARIES = {
    130        html_common: {
    131          id: "html-dict",
    132          content:
    133            '<html><head><title>Common HTML Template</title></head><body><div class="container"><p>',
    134          pattern: "*.html",
    135          type: "raw",
    136        },
    137      };
    138      let dict = DCB_TEST_DICTIONARIES.html_common;
    139      response.writeHead(200, {
    140        "Content-Type": "application/octet-stream",
    141        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    142        "Cache-Control": "max-age=3600",
    143      });
    144      response.end(dict.content, "binary");
    145    }
    146  );
    147 
    148  await httpServer.registerPathHandler(
    149    "/dict/api",
    150    function (request, response) {
    151      const DCB_TEST_DICTIONARIES = {
    152        api_json: {
    153          id: "api-dict",
    154          content:
    155            '{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
    156          pattern: "/api/*",
    157          type: "raw",
    158        },
    159      };
    160      let dict = DCB_TEST_DICTIONARIES.api_json;
    161      response.writeHead(200, {
    162        "Content-Type": "application/octet-stream",
    163        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    164        "Cache-Control": "max-age=3600",
    165      });
    166      response.end(dict.content, "binary");
    167    }
    168  );
    169 
    170  await httpServer.registerPathHandler(
    171    "/dict/js",
    172    function (request, response) {
    173      const DCB_TEST_DICTIONARIES = {
    174        js_common: {
    175          id: "js-dict",
    176          content:
    177            "function(){return this;};var=function();const=function();let=function();",
    178          pattern: "*.js",
    179          type: "raw",
    180        },
    181      };
    182      let dict = DCB_TEST_DICTIONARIES.js_common;
    183      response.writeHead(200, {
    184        "Content-Type": "application/octet-stream",
    185        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    186        "Cache-Control": "max-age=3600",
    187      });
    188      response.end(dict.content, "binary");
    189    }
    190  );
    191 
    192  await httpServer.registerPathHandler(
    193    "/dict/large",
    194    function (request, response) {
    195      const DCB_TEST_DICTIONARIES = {
    196        large_dict: {
    197          id: "large-dict",
    198          content: "REPEATED_PATTERN_".repeat(1000), // ~1.5MB dictionary
    199          pattern: "/large/*",
    200          type: "raw",
    201        },
    202      };
    203      let dict = DCB_TEST_DICTIONARIES.large_dict;
    204      response.writeHead(200, {
    205        "Content-Type": "application/octet-stream",
    206        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    207        "Cache-Control": "max-age=3600",
    208      });
    209      response.end(dict.content, "binary");
    210    }
    211  );
    212 
    213  // Basic dictionary with valid Use-As-Dictionary header
    214  await httpServer.registerPathHandler(
    215    "/dict/basic",
    216    function (request, response) {
    217      const TEST_DICTIONARIES = {
    218        basic: {
    219          id: "basic-dict",
    220          content: "BASIC_DICTIONARY_DATA",
    221          pattern: "/api/*",
    222          type: "raw",
    223        },
    224      };
    225      let dict = TEST_DICTIONARIES.basic;
    226      response.writeHead(200, {
    227        "Content-Type": "application/octet-stream",
    228        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    229        "Cache-Control": "max-age=3600",
    230      });
    231      response.end(dict.content, "binary");
    232    }
    233  );
    234 
    235  // Dictionary with longer match value
    236  await httpServer.registerPathHandler(
    237    "/dict/longer",
    238    function (request, response) {
    239      const TEST_DICTIONARIES = {
    240        specific: {
    241          id: "longer-match-dict",
    242          content:
    243            '{"status":"success","data":{"id":null,"name":"","created_at":"","updated_at":""},"message":"","errors":[]}',
    244          pattern: "/api/v1/*",
    245          type: "raw",
    246        },
    247      };
    248      let dict = TEST_DICTIONARIES.specific;
    249      response.writeHead(200, {
    250        "Content-Type": "application/octet-stream",
    251        "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`,
    252        "Cache-Control": "max-age=3600",
    253      });
    254      response.end(dict.content, "binary");
    255    }
    256  );
    257 
    258  registerCleanupFunction(async () => {
    259    try {
    260      await httpServer.stop();
    261    } catch (e) {
    262      // Ignore server stop errors during cleanup
    263    }
    264  });
    265 
    266  return httpServer;
    267 }
    268 
    269 async function sync_to_server() {
    270  if (server.processId) {
    271    await server.execute(`global.requestLog = ${JSON.stringify(requestLog)};`);
    272  } else {
    273    dump("Server not running?\n");
    274  }
    275 }
    276 
    277 async function sync_from_server() {
    278  if (server.processId) {
    279    dump(`*** requestLog: ${JSON.stringify(requestLog)}\n`);
    280    requestLog = await server.execute(`global.requestLog`);
    281  } else {
    282    dump("Server not running? (from)\n");
    283  }
    284 }
    285 
    286 // Calculate expected SHA-256 hash for dictionary content
    287 async function calculateDictionaryHash(content) {
    288  const encoded = new TextEncoder().encode(content);
    289  const digest = await crypto.subtle.digest("SHA-256", encoded);
    290  return btoa(String.fromCharCode(...new Uint8Array(digest))); // base64
    291 }
    292 
    293 // Verify dcb decompression result
    294 function verifyDCBResponse(channel, data, dictionary) {
    295  // XXX verify decoded content once we use real Brotli encoding
    296  Assert.equal(data.length, dictionary.expected_length);
    297 
    298  try {
    299    // Note: since we remove dcb encoding in the parent process, we can't see
    300    // it in Content-Encoding here
    301    var contentEncoding;
    302    channel.getOriginalResponseHeader("Content-Encoding", {
    303      visitHeader: function visitOrg(aName, aValue) {
    304        contentEncoding = aValue;
    305      },
    306    });
    307    Assert.equal;
    308    if (contentEncoding === "dcb") {
    309      return true;
    310    }
    311  } catch (e) {
    312    // Content-Encoding header not present or not dcb
    313  }
    314  return false;
    315 }
    316 
    317 // Setup dcb-aware server endpoint
    318 async function registerDCBEndpoint(
    319  httpServer,
    320  path,
    321  dictionary,
    322  content,
    323  shouldCompress = true
    324 ) {
    325  // We have to put all values and functions referenced in the handler into
    326  // this string which will be turned into a function for the handler, because
    327  // NodeHTTPSServer handlers can't access items in the local or global scopes of the
    328  // containing file
    329  let func = `
    330    let path = "${path}";
    331    let dictionary = ${JSON.stringify(dictionary)};
    332    let content = '${content}';
    333    let shouldCompress = ${shouldCompress};
    334    
    335    let availableDict = "";
    336    let hasDictHeader = false;
    337    
    338    // Get content type based on file path
    339    function getContentTypeForPath(path) {
    340      if (path.endsWith('.html')) return 'text/html; charset=utf-8';
    341      if (path.endsWith('.js')) return 'application/javascript';
    342      if (path.includes('/api/')) return 'application/json';
    343      return 'text/plain; charset=utf-8';
    344    }
    345 
    346    // Calculate compression ratio
    347    function calculateCompressionRatio(original, compressed) {
    348      if (typeof original === 'string') original = original.length;
    349      if (typeof compressed === 'string') compressed = compressed.length;
    350      return original / compressed;
    351    }
    352 
    353    // Simulate dcb compression (for server responses)
    354    function simulateDCBCompression(content, dictionary) {
    355      // Note: Real implementation would use actual Brotli compression
    356      // For testing, we simulate with compression markers and realistic size reduction
    357      let simulatedCompressedSize = Math.floor(content.length * 0.4); // Simulate 60% savings
    358      // This needs to be something that the brotli decoder will correctly read, even though this
    359      // will produce the wrong output
    360      let compressedData = "\x21\x38\x00\x04COMPRESSED_DATA\x03";
    361      
    362      return {
    363        compressedData: "\xff\x44\x43\x42" + "12345678901234567890123456789012" + compressedData,
    364        originalSize: content.length,
    365        compressedSize: compressedData.length + 36,
    366        compressionRatio: calculateCompressionRatio(content.length, simulatedCompressedSize + 36)
    367      };
    368    }
    369 
    370    if (request.headers && request.headers['available-dictionary']) {
    371      availableDict = request.headers['available-dictionary'];
    372      hasDictHeader = true;
    373    } else {
    374      shouldCompress = false;
    375    }
    376    
    377    // Log the request for analysis
    378    global.requestLog[global.requestLog.length] = {
    379      path: path,
    380      hasAvailableDict: hasDictHeader,
    381      availableDict: availableDict,
    382      method: request.method
    383    };
    384    
    385    if (shouldCompress && hasDictHeader && availableDict.includes(dictionary.hash)) {
    386      // Simulate dcb compression
    387      let compressed = simulateDCBCompression(content, dictionary);
    388      response.writeHead(200, {
    389        "Content-Encoding": "dcb",
    390        "Content-Type": getContentTypeForPath(path),
    391        "Content-Length": compressed.compressedSize.toString(),
    392      });
    393      
    394      // In a real implementation, this would be actual compressed brotli data
    395      // For testing, we simulate the compressed response
    396 
    397      // Note: these aren't real dictionaries; we've prepended a dummy header
    398      // to pass the requirements for a Brotli dictionary - 4 byte magic number
    399      // plus 32 bytes of hash (which we don't currently check, nor does Brotli).
    400      response.end(compressed.compressedData, "binary");
    401    } else {
    402      // Serve uncompressed
    403      response.writeHead(200, {
    404        "Content-Type": getContentTypeForPath(path),
    405        "Content-Length": content.length,
    406      });
    407      response.end(content, "binary");
    408    }
    409  `;
    410  let handler = new Function("request", "response", func);
    411  return httpServer.registerPathHandler(path, handler);
    412 }
    413 
    414 // Verify dictionary is stored in cache (reused from previous tests)
    415 function verifyDictionaryStored(url, shouldExist, callback) {
    416  let lci = Services.loadContextInfo.custom(false, {
    417    partitionKey: `(https,localhost)`,
    418  });
    419  asyncCheckCacheEntryPresence(url, "disk", shouldExist, lci, callback);
    420 }
    421 
    422 async function setupDicts() {
    423  requestLog = [];
    424  await sync_to_server();
    425 
    426  // Store all test dictionaries and calculate their hashes
    427  const dictPaths = [
    428    "/dict/html",
    429    "/dict/api",
    430    "/dict/js",
    431    "/dict/large",
    432    "/dict/longer",
    433  ];
    434  const dictKeys = [
    435    "html_common",
    436    "api_json",
    437    "js_common",
    438    "large_dict",
    439    "api_v1",
    440  ];
    441 
    442  for (let i = 0; i < dictPaths.length; i++) {
    443    let path = dictPaths[i];
    444    let dictKey = dictKeys[i];
    445    let url = `${server.origin()}${path}`;
    446    dump(
    447      `registering dictionary ${path} for match pattern ${DCB_TEST_DICTIONARIES[dictKey].pattern}\n`
    448    );
    449 
    450    let chan = makeChan(url);
    451    let [, data] = await channelOpenPromise(chan);
    452    // Calculate and store hash for later use
    453    DCB_TEST_DICTIONARIES[dictKey].hash =
    454      ":" +
    455      (await calculateDictionaryHash(DCB_TEST_DICTIONARIES[dictKey].content)) +
    456      ":";
    457 
    458    // Verify dictionary content matches
    459    Assert.equal(
    460      data,
    461      DCB_TEST_DICTIONARIES[dictKey].content,
    462      `Dictionary content matches`
    463    );
    464 
    465    // Verify dictionary was stored
    466    await new Promise(resolve => {
    467      verifyDictionaryStored(url, true, resolve);
    468    });
    469  }
    470 
    471  dump(`**** DCB test setup complete. Dictionaries stored with hashes.\n`);
    472 }
    473 
    474 add_setup(async function () {
    475  Services.prefs.setBoolPref("network.http.dictionaries.enable", true);
    476  if (!server) {
    477    server = await setupDCBTestServer();
    478  }
    479  // Setup baseline dictionaries for compression testing
    480 
    481  // Clear any existing cache
    482  let lci = Services.loadContextInfo.custom(false, {
    483    partitionKey: `(https,localhost)`,
    484  });
    485  evict_cache_entries("all", lci);
    486 
    487  await setupDicts();
    488 });
    489 
    490 // Test basic dictionary-compressed Brotli functionality
    491 add_task(async function test_basic_dcb_compression() {
    492  dump("**** test_basic_dcb_compression\n");
    493  requestLog = [];
    494  await sync_to_server();
    495 
    496  // Setup DCB endpoint for HTML content
    497  let dict = DCB_TEST_DICTIONARIES.html_common;
    498  let content = DCB_TEST_CONTENT.html_page;
    499  await registerDCBEndpoint(server, "/dict/test.html", dict, content, true);
    500 
    501  let url = `${server.origin()}/dict/test.html`;
    502  let chan = makeChan(url);
    503  let [request, data] = await channelOpenPromise(chan);
    504 
    505  // Check if DCB compression was used
    506  let usedDCB = verifyDCBResponse(
    507    request.QueryInterface(Ci.nsIHttpChannel),
    508    data,
    509    dict
    510  );
    511  Assert.ok(usedDCB, "DCB compression should be used");
    512 });
    513 
    514 // Test correct use of URLPattern baseurl
    515 add_task(async function test_baseurl() {
    516  dump("**** test_baseurl\n");
    517  requestLog = [];
    518  await sync_to_server();
    519 
    520  // Setup DCB endpoint for HTML content
    521  let dict = DCB_TEST_DICTIONARIES.html_common;
    522  let content = DCB_TEST_CONTENT.html_page;
    523  await registerDCBEndpoint(server, "/test.html", dict, content, true);
    524 
    525  let url = `${server.origin()}/test.html`;
    526  let chan = makeChan(url);
    527  let [request, data] = await channelOpenPromise(chan);
    528 
    529  Assert.greater(
    530    data.length,
    531    0,
    532    "Should still receive content without dictionary in use"
    533  );
    534  // Check if DCB compression was used (should be false)
    535  Assert.ok(
    536    !verifyDCBResponse(
    537      request.QueryInterface(Ci.nsIHttpChannel),
    538      data,
    539      DCB_TEST_DICTIONARIES.html_common_no_dictionary
    540    ),
    541    "DCB compression should not be used with the wrong path"
    542  );
    543 });
    544 
    545 // Test correct dictionary selection for dcb compression
    546 add_task(async function test_dcb_dictionary_selection() {
    547  requestLog = [];
    548  await sync_to_server();
    549 
    550  dump("**** Testing DCB dictionary selection\n");
    551 
    552  // Test specific pattern matching for dictionary selection
    553  let htmlDict = DCB_TEST_DICTIONARIES.html_common;
    554  let apiDict = DCB_TEST_DICTIONARIES.api_json;
    555 
    556  // Register endpoints that should match different dictionaries
    557  await registerDCBEndpoint(
    558    server,
    559    "/dict/specific-test.html",
    560    htmlDict,
    561    DCB_TEST_CONTENT.html_page,
    562    true
    563  );
    564  await registerDCBEndpoint(
    565    server,
    566    "/api/specific-test",
    567    apiDict,
    568    DCB_TEST_CONTENT.api_response,
    569    true
    570  );
    571 
    572  // Test HTML dictionary selection
    573  let htmlUrl = `${server.origin()}/dict/specific-test.html`;
    574  let htmlChan = makeChan(htmlUrl);
    575  let [, htmlData] = await channelOpenPromise(htmlChan);
    576 
    577  Assert.greater(
    578    htmlData.length,
    579    0,
    580    "HTML dictionary selection test should have content"
    581  );
    582 
    583  // Check if correct dictionary was used
    584  await sync_from_server();
    585  let htmlLogEntry = requestLog.find(
    586    entry => entry.path === "/dict/specific-test.html"
    587  );
    588  Assert.ok(
    589    htmlLogEntry && htmlLogEntry.hasAvailableDict,
    590    "Dictionary selection test: HTML endpoint received Available-Dictionary header"
    591  );
    592 
    593  // Test API dictionary selection
    594  let apiUrl = `${server.origin()}/api/specific-test`;
    595  let apiChan = makeChan(apiUrl);
    596  let [, apiData] = await channelOpenPromise(apiChan);
    597 
    598  Assert.greater(
    599    apiData.length,
    600    0,
    601    "API dictionary selection test should have content"
    602  );
    603 
    604  // Check if correct dictionary was used
    605  await sync_from_server();
    606  let apiLogEntry = requestLog.find(
    607    entry => entry.path === "/api/specific-test"
    608  );
    609  Assert.ok(
    610    apiLogEntry && apiLogEntry.hasAvailableDict,
    611    "Dictionary selection test: API endpoint received Available-Dictionary header"
    612  );
    613 });
    614 
    615 // Test behavior when dictionary is missing/unavailable
    616 add_task(async function test_dcb_missing_dictionary() {
    617  requestLog = [];
    618  await sync_to_server();
    619 
    620  dump("**** Testing DCB missing dictionary\n");
    621 
    622  // Create a fake dictionary that won't be found
    623  let fakeDict = {
    624    id: "missing-dict",
    625    hash: "fake_hash_that_does_not_exist",
    626    content: "This dictionary was not stored",
    627    expected_length: DCB_TEST_CONTENT.jpeg.length,
    628  };
    629 
    630  // *.jpeg Doesn't match any of the patterns in DCB_TEST_DICTIONARIES
    631  await registerDCBEndpoint(
    632    server,
    633    "/missing-dict-test.jpeg",
    634    fakeDict,
    635    DCB_TEST_CONTENT.jpeg,
    636    false
    637  );
    638 
    639  let url = `${server.origin()}/missing-dict-test.jpeg`;
    640  let chan = makeChan(url);
    641  let [request, data] = await channelOpenPromise(chan);
    642 
    643  // Should get uncompressed content when dictionary is missing
    644  Assert.greater(
    645    data.length,
    646    0,
    647    "Missing dictionary test should still return content"
    648  );
    649 
    650  // Verify no dcb compression was applied
    651  let usedDCB = verifyDCBResponse(
    652    request.QueryInterface(Ci.nsIHttpChannel),
    653    data,
    654    fakeDict
    655  );
    656  Assert.ok(!usedDCB, "We should not get DCB encoding for a fake item");
    657 });
    658 
    659 // Test IETF spec compliance for dcb encoding
    660 add_task(async function test_dcb_header_compliance() {
    661  requestLog = [];
    662  await sync_to_server();
    663 
    664  dump("**** Testing DCB header compliance\n");
    665 
    666  let dict = DCB_TEST_DICTIONARIES.api_json;
    667  await registerDCBEndpoint(
    668    server,
    669    "/api/compliance-test",
    670    dict,
    671    DCB_TEST_CONTENT.api_response,
    672    true
    673  );
    674 
    675  let url = `${server.origin()}/api/compliance-test`;
    676  let chan = makeChan(url);
    677  let [request, data] = await channelOpenPromise(chan);
    678 
    679  Assert.greater(data.length, 0, "IETF compliance test should have content");
    680 
    681  let httpChannel = request.QueryInterface(Ci.nsIHttpChannel);
    682 
    683  // Verify proper Content-Type preservation
    684  try {
    685    let contentType = httpChannel.getResponseHeader("Content-Type");
    686    Assert.ok(
    687      contentType.includes("application/json"),
    688      "Content-Type should be preserved through compression"
    689    );
    690  } catch (e) {
    691    Assert.ok(false, "Content-Type header should be present");
    692  }
    693 
    694  // Check for proper dcb handling
    695  await sync_from_server();
    696  let logEntry = requestLog.find(
    697    entry => entry.path === "/api/compliance-test"
    698  );
    699  Assert.ok(
    700    logEntry && logEntry.hasAvailableDict,
    701    "Must have available-dictionary in header compliance"
    702  );
    703  // Verify Available-Dictionary follows IETF Structured Field Byte-Sequence format
    704  // According to RFC 8941, byte sequences are enclosed in colons: :base64data:
    705  let availableDict = logEntry.availableDict;
    706  Assert.ok(
    707    availableDict.startsWith(":"),
    708    "Available-Dictionary should start with ':' (IETF Structured Field Byte-Sequence format)"
    709  );
    710  Assert.ok(
    711    availableDict.endsWith(":"),
    712    "Available-Dictionary should end with ':' (IETF Structured Field Byte-Sequence format)"
    713  );
    714  Assert.greater(
    715    availableDict.length,
    716    2,
    717    "Available-Dictionary should contain base64 data between colons"
    718  );
    719 
    720  // Extract the base64 content between the colons
    721  let base64Content = availableDict.slice(1, -1);
    722  Assert.greater(
    723    base64Content.length,
    724    0,
    725    "Available-Dictionary should have base64 content"
    726  );
    727 
    728  // Basic validation that it looks like base64 (contains valid base64 characters)
    729  let base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
    730  Assert.ok(
    731    base64Regex.test(base64Content),
    732    "Available-Dictionary content should be valid base64"
    733  );
    734 
    735  dump(`**** IETF compliance test: Available-Dictionary = ${availableDict}\n`);
    736 });
    737 
    738 // Test that DCB compression stops working after dictionary cache eviction
    739 add_task(async function test_dcb_compression_after_cache_eviction() {
    740  requestLog = [];
    741  await sync_to_server();
    742 
    743  dump("**** Testing DCB compression after cache eviction\n");
    744 
    745  // Use a specific dictionary for this test
    746  let dict = DCB_TEST_DICTIONARIES.html_common;
    747  let dict2 = DCB_TEST_DICTIONARIES.html_common_no_dictionary;
    748  let testContent = DCB_TEST_CONTENT.html_page;
    749  let testPath = "/dict/cache-eviction-test.html";
    750  let dictUrl = `${server.origin()}/dict/html`;
    751  let contentUrl = `${server.origin()}${testPath}`;
    752 
    753  // Step 1: Ensure dictionary is in cache by fetching it
    754  dump("**** Step 1: Loading dictionary into cache\n");
    755  let dictChan = makeChan(dictUrl);
    756  let [, dictData] = await channelOpenPromise(dictChan);
    757  Assert.equal(dictData, dict.content, "Dictionary loaded successfully");
    758 
    759  // Verify dictionary is cached
    760  await new Promise(resolve => {
    761    verifyDictionaryStored(dictUrl, true, () => {
    762      resolve();
    763    });
    764  });
    765 
    766  // Step 2: Set up DCB endpoint and test compression works
    767  dump("**** Step 2: Testing DCB compression with cached dictionary\n");
    768  await registerDCBEndpoint(server, testPath, dict, testContent, true);
    769 
    770  // Clear request log before testing
    771  requestLog = [];
    772  await sync_to_server();
    773 
    774  let chan1 = makeChan(contentUrl);
    775  let [req1, data1] = await channelOpenPromise(chan1);
    776 
    777  Assert.greater(data1.length, 0, "Should receive content before eviction");
    778 
    779  // Check if DCB compression was used (should be true with cached dictionary)
    780  let usedDCB1 = verifyDCBResponse(
    781    req1.QueryInterface(Ci.nsIHttpChannel),
    782    data1,
    783    dict
    784  );
    785  Assert.ok(usedDCB1, "DCB compression should be used");
    786 
    787  // Step 3: Evict the dictionary from cache
    788  dump("**** Step 3: Evicting dictionary from cache\n");
    789 
    790  // Evict the dictionary cache entry
    791  let lci = Services.loadContextInfo.custom(false, {
    792    partitionKey: `(https,localhost)`,
    793  });
    794  evict_cache_entries("all", lci);
    795  // Force cache sync to ensure everything is written
    796  await new Promise(resolve => {
    797    syncWithCacheIOThread(resolve, true);
    798  });
    799 
    800  dump("**** Step 3.5: verify no longer cache\n");
    801  // Verify dictionary is no longer cached
    802  await new Promise(resolve => {
    803    verifyDictionaryStored(dictUrl, false, () => {
    804      resolve();
    805    });
    806  });
    807 
    808  // Step 4: Test that compression no longer works after eviction
    809  dump("**** Step 4: Testing DCB compression after dictionary eviction\n");
    810 
    811  let chan2 = makeChan(contentUrl);
    812  let [req2, data2] = await channelOpenPromise(chan2);
    813 
    814  Assert.greater(
    815    data2.length,
    816    0,
    817    "Should still receive content after eviction"
    818  );
    819  // Check if DCB compression was used (should be false without cached dictionary)
    820  Assert.ok(
    821    !verifyDCBResponse(req2.QueryInterface(Ci.nsIHttpChannel), data2, dict2),
    822    "DCB compression should not be used without dictionary"
    823  );
    824 
    825  // XXX We can only check this if we actually brotli-compress the data
    826  // Content should still be delivered in both cases, just not compressed in the second case
    827  //Assert.equal(data1.length, data2.length,
    828  //  "Content length should be the same whether compressed or not (in our test simulation)");
    829 
    830  dump("**** Cache eviction test completed successfully\n");
    831 });
    832 
    833 // Test HTTP redirect (302) with dictionary-compressed content
    834 add_task(async function test_dcb_with_http_redirect() {
    835  await setupDicts();
    836 
    837  dump("**** Testing HTTP redirect (302) with dictionary-compressed content\n");
    838 
    839  let dict = DCB_TEST_DICTIONARIES.html_common;
    840  let content = DCB_TEST_CONTENT.html_page;
    841  await registerDCBEndpoint(server, "/dict/test.html", dict, content, true);
    842  let originalPath = "/redirect/original";
    843  let finalPath = "/dict/test.html";
    844  let originalUrl = `${server.origin()}${originalPath}`;
    845  let finalUrl = `${server.origin()}${finalPath}`;
    846 
    847  // Step 1: Set up redirect handler that returns 302 to final URL
    848  let redirectFunc = `
    849    let finalPath = "${finalPath}";
    850    
    851    // Log the request for analysis
    852    global.requestLog[global.requestLog.length] = {
    853      path: "${originalPath}",
    854      method: request.method,
    855      redirectTo: finalPath,
    856      hasAvailableDict: !!request.headers['available-dictionary'],
    857      availableDict: request.headers['available-dictionary'] || null
    858    };
    859    
    860    response.writeHead(302, {
    861      "Location": finalPath,
    862      "Cache-Control": "no-cache"
    863    });
    864    response.end("Redirecting...");
    865  `;
    866  let redirectHandler = new Function("request", "response", redirectFunc);
    867  await server.registerPathHandler(originalPath, redirectHandler);
    868 
    869  // Step 2: Set up final endpoint with DCB compression capability
    870  await registerDCBEndpoint(server, finalPath, dict, content, true);
    871 
    872  // Clear request log before testing
    873  requestLog = [];
    874  await sync_to_server();
    875 
    876  // Step 3: Request the original URL that redirects to potentially DCB-compressed content
    877  let chan = makeChan(originalUrl);
    878  let [req, data] = await channelOpenPromise(chan);
    879 
    880  // Step 4: Verify redirect worked correctly
    881  let finalUri = req.QueryInterface(Ci.nsIHttpChannel).URI.spec;
    882  Assert.equal(
    883    finalUri,
    884    finalUrl,
    885    "Final URI should match the redirected URL after 302 redirect"
    886  );
    887 
    888  // Verify we received some content
    889  Assert.greater(data.length, 0, "Should receive content after redirect");
    890 
    891  // Step 5: Check request log to verify both requests were logged
    892  await sync_from_server();
    893 
    894  // Should have two entries: redirect request and final request
    895  let redirectEntry = requestLog.find(entry => entry.path === originalPath);
    896  let finalEntry = requestLog.find(entry => entry.path === finalPath);
    897 
    898  Assert.ok(redirectEntry, "Redirect request should be logged");
    899  Assert.ok(finalEntry, "Final request should be logged");
    900 
    901  // Step 6: Verify Available-Dictionary header handling
    902  // Note: The redirect request may or may not have Available-Dictionary header depending on implementation
    903  // The important thing is that the final request has it
    904  if (redirectEntry.hasAvailableDict) {
    905    dump(`**** Redirect request includes Available-Dictionary header\n`);
    906  } else {
    907    dump(
    908      `**** Redirect request does not include Available-Dictionary header (expected)\n`
    909    );
    910  }
    911 
    912  // Note: With redirects, Available-Dictionary headers may not be preserved
    913  Assert.ok(
    914    finalEntry.hasAvailableDict,
    915    "Final request includes Available-Dictionary header"
    916  );
    917 
    918  // Available-Dictionary header should contain the dictionary hash for final request
    919  Assert.ok(
    920    finalEntry.availableDict.includes(dict.hash),
    921    "Final request Available-Dictionary should contain correct dictionary hash"
    922  );
    923 
    924  // Step 7: Check if DCB compression was applied
    925  Assert.ok(
    926    verifyDCBResponse(req.QueryInterface(Ci.nsIHttpChannel), data, dict),
    927    "DCB compression successfully applied after redirect"
    928  );
    929 });
    930 
    931 // Test invalid Use-As-Dictionary headers - missing match parameter
    932 add_task(async function test_use_as_dictionary_invalid_missing_match() {
    933  // Invalid dictionary headers
    934  await server.registerPathHandler(
    935    "/dict/invalid-missing-match",
    936    function (request, response) {
    937      response.writeHead(200, {
    938        "Content-Type": "application/octet-stream",
    939        "Use-As-Dictionary": `id="missing-match-dict", type=raw`,
    940        "Cache-Control": "max-age=3600",
    941      });
    942      response.end("INVALID_MISSING_MATCH_DATA", "binary");
    943    }
    944  );
    945  let url = `${server.origin()}/dict/invalid-missing-match`;
    946  let chan = makeChan(url);
    947  let [req, data] = await channelOpenPromise(chan);
    948  // Verify dictionary content matches
    949  Assert.equal(
    950    data,
    951    "INVALID_MISSING_MATCH_DATA",
    952    "Set up missing match dictionary"
    953  );
    954 
    955  let dict = DCB_TEST_DICTIONARIES.html_common_no_dictionary;
    956  let content = DCB_TEST_CONTENT.html_page;
    957  await registerDCBEndpoint(
    958    server,
    959    "/invalid/missing-match",
    960    dict,
    961    content,
    962    true
    963  );
    964 
    965  url = `https://localhost:${server.port()}/invalid/missing-match`;
    966 
    967  chan = makeChan(url);
    968  [req, data] = await channelOpenPromise(chan);
    969 
    970  Assert.equal(data, content, "Content received");
    971 
    972  // Verify invalid header was not processed as dictionary
    973  Assert.ok(
    974    !verifyDCBResponse(req.QueryInterface(Ci.nsIHttpChannel), data, dict),
    975    "DCB compression should not be used when dictionary has no match="
    976  );
    977 
    978  // Invalid dictionary should not be processed as dictionary
    979  dump("**** Missing match parameter test complete\n");
    980 });
    981 
    982 // Test invalid Use-As-Dictionary headers - empty id parameter
    983 add_task(async function test_use_as_dictionary_invalid_empty_id() {
    984  await server.registerPathHandler(
    985    "/dict/invalid-empty-id",
    986    function (request, response) {
    987      response.writeHead(200, {
    988        "Content-Type": "application/octet-stream",
    989        "Use-As-Dictionary": `match="/invalid/*", id="", type=raw`,
    990        "Cache-Control": "max-age=3600",
    991      });
    992      response.end("INVALID_EMPTY_ID_DATA", "binary");
    993    }
    994  );
    995  let url = `${server.origin()}/dict/invalid-empty-id`;
    996  let chan = makeChan(url);
    997  let [req, data] = await channelOpenPromise(chan);
    998  // Verify dictionary content matches
    999  Assert.equal(data, "INVALID_EMPTY_ID_DATA", "Set up empty id dictionary");
   1000 
   1001  let dict = DCB_TEST_DICTIONARIES.html_common_no_dictionary;
   1002  let content = DCB_TEST_CONTENT.html_page;
   1003  await registerDCBEndpoint(server, "/invalid/empty-id", dict, content, true);
   1004 
   1005  url = `https://localhost:${server.port()}/invalid/empty-id`;
   1006 
   1007  chan = makeChan(url);
   1008  [req, data] = await channelOpenPromise(chan);
   1009 
   1010  Assert.equal(data, content, "non-compressed content received");
   1011 
   1012  Assert.ok(
   1013    !verifyDCBResponse(req.QueryInterface(Ci.nsIHttpChannel), data, dict),
   1014    "DCB compression should not be used with dictionary with empty id"
   1015  );
   1016  dump("**** Empty id parameter test complete\n");
   1017 });
   1018 
   1019 // Test Available-Dictionary request header generation
   1020 add_task(async function test_available_dictionary_header_generation() {
   1021  let url = `https://localhost:${server.port()}/api/test`;
   1022  requestLog = [];
   1023  await sync_to_server();
   1024 
   1025  // Calculate expected hash for basic dictionary
   1026  let expectedHashB64 = await calculateDictionaryHash(
   1027    DCB_TEST_DICTIONARIES.api_json.content
   1028  );
   1029 
   1030  // Setup DCB endpoint for HTML content
   1031  let dict = DCB_TEST_DICTIONARIES.html_common;
   1032  let content = DCB_TEST_CONTENT.api_response;
   1033  await registerDCBEndpoint(server, "/api/test", dict, content, true);
   1034 
   1035  let chan = makeChan(url);
   1036  let [, data] = await channelOpenPromise(chan);
   1037 
   1038  Assert.equal(data, DCB_TEST_CONTENT.api_response, "Resource content matches");
   1039 
   1040  // Check request log to see if Available-Dictionary header was sent
   1041  await sync_from_server();
   1042  let logEntry = requestLog.find(entry => entry.path === "/api/test");
   1043  Assert.ok(
   1044    logEntry && logEntry.availableDict != null,
   1045    "Available-Dictionary header should be present"
   1046  );
   1047  if (logEntry && logEntry.availableDict != null) {
   1048    // Verify IETF Structured Field Byte-Sequence format
   1049    Assert.ok(
   1050      logEntry.availableDict.startsWith(":"),
   1051      "Available-Dictionary should start with ':' (IETF Structured Field format)"
   1052    );
   1053    Assert.ok(
   1054      logEntry.availableDict.endsWith(":"),
   1055      "Available-Dictionary should end with ':' (IETF Structured Field format)"
   1056    );
   1057 
   1058    // Verify base64 content
   1059    let base64Content = logEntry.availableDict.slice(1, -1);
   1060    let base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
   1061    Assert.ok(
   1062      base64Regex.test(base64Content),
   1063      "Available-Dictionary content should be valid base64"
   1064    );
   1065    Assert.equal(
   1066      logEntry.availableDict,
   1067      ":" + expectedHashB64 + ":",
   1068      "Available-Dictionary has the right hash"
   1069    );
   1070  }
   1071  dump("**** Available-Dictionary generation test complete\n");
   1072 });
   1073 
   1074 // Test Available-Dictionary header for specific pattern matching
   1075 add_task(async function test_available_dictionary_specific_patterns() {
   1076  let url = `https://localhost:${server.port()}/api/v1/test`;
   1077  requestLog = [];
   1078  await sync_to_server();
   1079 
   1080  let dict = DCB_TEST_DICTIONARIES.api_v1;
   1081  let content = DCB_TEST_CONTENT.api_v1;
   1082  await registerDCBEndpoint(server, "/api/v1/test", dict, content, true);
   1083 
   1084  let chan = makeChan(url);
   1085  await channelOpenPromise(chan);
   1086 
   1087  // Check for Available-Dictionary header
   1088  await sync_from_server();
   1089  let logEntry = requestLog.find(entry => entry.path === "/api/v1/test");
   1090  Assert.ok(
   1091    logEntry && logEntry.availableDict != null,
   1092    "Available-Dictionary header should be present for /api/v1/*"
   1093  );
   1094 
   1095  if (logEntry && logEntry.availableDict != null) {
   1096    // Should match both /api/v1/* (longer-dict) and /api/* (basic-dict) patterns
   1097    // It should always use the longer match, which would be /api/v1/*
   1098    Assert.equal(
   1099      logEntry.availableDict,
   1100      DCB_TEST_DICTIONARIES.api_v1.hash,
   1101      "Longer match pattern for a dictionary should be used"
   1102    );
   1103  }
   1104  dump("**** Specific pattern matching test complete\n");
   1105 });
   1106 
   1107 // Test Available-Dictionary header absence for no matching patterns
   1108 add_task(async function test_available_dictionary_no_match() {
   1109  let url = `https://localhost:${server.port()}/nomatch/test`;
   1110  requestLog = [];
   1111  await sync_to_server();
   1112 
   1113  let dict = DCB_TEST_DICTIONARIES.html_common;
   1114  let content = "NO MATCH TEST DATA";
   1115  await registerDCBEndpoint(server, "/nomatch/test", dict, content, true);
   1116 
   1117  let chan = makeChan(url);
   1118  let [, data] = await channelOpenPromise(chan);
   1119 
   1120  Assert.equal(data, "NO MATCH TEST DATA", "No match content received");
   1121 
   1122  // Check that no Available-Dictionary header was sent
   1123  await sync_from_server();
   1124  let logEntry = requestLog.find(entry => entry.path === "/nomatch/test");
   1125  Assert.ok(logEntry, "Request should be logged");
   1126 
   1127  if (logEntry) {
   1128    Assert.equal(
   1129      logEntry.availableDict,
   1130      "",
   1131      "Available-Dictionary should be null for no match"
   1132    );
   1133  }
   1134  dump("**** No match test complete\n");
   1135 });
   1136 
   1137 // Cleanup
   1138 add_task(async function cleanup() {
   1139  // Clear cache
   1140  let lci = Services.loadContextInfo.custom(false, {
   1141    partitionKey: `(https,localhost)`,
   1142  });
   1143  evict_cache_entries("all", lci);
   1144  dump("**** DCB compression tests completed.\n");
   1145 });