/** * Tests for HTTP Compression Dictionary retrieval functionality * - Dictionary lookup by origin and pattern matching * - Available-Dictionary header generation and formatting * - Dictionary cache hit/miss scenarios * - Dictionary precedence and selection logic */ "use strict"; // Load cache helpers Services.scriptloader.loadSubScript("resource://test/head_cache.js", this); const { NodeHTTPSServer } = ChromeUtils.importESModule( "resource://testing-common/NodeServer.sys.mjs" ); // Test dictionaries with different patterns and priorities const RETRIEVAL_TEST_DICTIONARIES = { api_v1: { id: "api-v1-dict", content: "API_V1_COMMON_DATA", pattern: "/api/v1/*", type: "raw", }, api_generic: { id: "api-generic-dict", content: "API_GENERIC_DATA", pattern: "/api/*", type: "raw", }, wildcard: { id: "wildcard-dict", content: "WILDCARD_DATA", pattern: "*", type: "raw", }, js_files: { id: "js-dict", content: "JS_COMMON_CODE", pattern: "*.js", type: "raw", }, }; let server = null; let requestLog = []; // Track requests for verification async function sync_to_server() { if (server.processId) { await server.execute(`global.requestLog = ${JSON.stringify(requestLog)};`); } else { dump("Server not running?\n"); } } async function sync_from_server() { if (server.processId) { requestLog = await server.execute(`global.requestLog`); } else { dump("Server not running? (from)\n"); } } add_setup(async function () { if (!server) { server = await setupServer(); } // Setup baseline dictionaries for compression testing // Clear any existing cache let lci = Services.loadContextInfo.custom(false, { partitionKey: `(https,localhost)`, }); evict_cache_entries("all", lci); }); // Calculate expected SHA-256 hash for dictionary content async function calculateDictionaryHash(content) { let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); hasher.init(Ci.nsICryptoHash.SHA256); let bytes = new TextEncoder().encode(content); hasher.update(bytes, bytes.length); let hash = hasher.finish(false); return btoa(hash); // Convert to base64 } // Setup dictionary test server async function setupServer() { if (!server) { server = new NodeHTTPSServer(); await server.start(); registerCleanupFunction(async () => { try { await server.stop(); } catch (e) { // Ignore server stop errors during cleanup } }); } return server; } // Create channel for dictionary requests function makeChan(url) { let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true, contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, }).QueryInterface(Ci.nsIHttpChannel); return chan; } function channelOpenPromise(chan) { return new Promise(resolve => { function finish(req, buffer) { resolve([req, buffer]); } chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL)); }); } // Verify dictionary is stored in cache function verifyDictionaryStored(url, shouldExist, callback) { let lci = Services.loadContextInfo.custom(false, { partitionKey: `(https,localhost)`, }); asyncCheckCacheEntryPresence(url, "disk", shouldExist, lci, callback); } // Setup server endpoint that expects specific dictionary headers async function registerDictionaryAwareEndpoint( httpServer, path, responseContent ) { // We have to put all values and functions referenced in the handler into // this string which will be turned into a function for the handler, because // NodeHTTPSServer handlers can't access items in the local or global scopes of the // containing file let func = ` // Log the request for analysis global.requestLog[global.requestLog.length] = { path: "${path}", hasAvailableDict: request.headers['available-dictionary'] !== undefined, availableDict: request.headers['available-dictionary'] || null }; response.writeHead(200, { "Content-Type": "text/plain", }); response.end("${responseContent}", "binary"); `; let handler = new Function("request", "response", func); return httpServer.registerPathHandler(path, handler); } // Setup retrieval test server with dictionaries and resources async function setupRetrievalTestServer() { await setupServer(); // Dictionary endpoints - store dictionaries with different patterns await server.registerPathHandler( "/dict/api-v1", function (request, response) { const RETRIEVAL_TEST_DICTIONARIES = { api_v1: { id: "api-v1-dict", content: "API_V1_COMMON_DATA", pattern: "/api/v1/*", type: "raw", }, }; let dict = RETRIEVAL_TEST_DICTIONARIES.api_v1; response.writeHead(200, { "Content-Type": "application/octet-stream", "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`, "Cache-Control": "max-age=3600", }); response.end(dict.content, "binary"); } ); await server.registerPathHandler( "/dict/api-generic", function (request, response) { const RETRIEVAL_TEST_DICTIONARIES = { api_generic: { id: "api-generic-dict", content: "API_GENERIC_DATA", pattern: "/api/*", type: "raw", }, }; let dict = RETRIEVAL_TEST_DICTIONARIES.api_generic; response.writeHead(200, { "Content-Type": "application/octet-stream", "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`, "Cache-Control": "max-age=3600", }); response.end(dict.content, "binary"); } ); await server.registerPathHandler( "/dict/wildcard", function (request, response) { const RETRIEVAL_TEST_DICTIONARIES = { wildcard: { id: "wildcard-dict", content: "WILDCARD_DATA", pattern: "*", type: "raw", }, }; let dict = RETRIEVAL_TEST_DICTIONARIES.wildcard; response.writeHead(200, { "Content-Type": "application/octet-stream", "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`, "Cache-Control": "max-age=3600", }); response.end(dict.content, "binary"); } ); await server.registerPathHandler("/dict/js", function (request, response) { const RETRIEVAL_TEST_DICTIONARIES = { js_files: { id: "js-dict", content: "JS_COMMON_CODE", pattern: "*.js", type: "raw", }, }; let dict = RETRIEVAL_TEST_DICTIONARIES.js_files; response.writeHead(200, { "Content-Type": "application/octet-stream", "Use-As-Dictionary": `match="${dict.pattern}", id="${dict.id}", type=${dict.type}`, "Cache-Control": "max-age=3600", }); response.end(dict.content, "binary"); }); // Resource endpoints that should trigger dictionary retrieval await registerDictionaryAwareEndpoint( server, "/api/v1/users", "API V1 USERS DATA" ); await registerDictionaryAwareEndpoint( server, "/api/v2/posts", "API V2 POSTS DATA" ); await registerDictionaryAwareEndpoint( server, "/api/generic", "GENERIC API DATA" ); await registerDictionaryAwareEndpoint(server, "/web/page", "WEB PAGE DATA"); await registerDictionaryAwareEndpoint( server, "/scripts/app.js", "JAVASCRIPT CODE" ); await registerDictionaryAwareEndpoint( server, "/styles/main.css", "CSS STYLES" ); return server; } // Setup baseline dictionaries for retrieval testing add_task(async function test_setup_dictionaries() { await setupRetrievalTestServer(); // Clear any existing cache let lci = Services.loadContextInfo.custom(false, { partitionKey: `(https,localhost)`, }); evict_cache_entries("all", lci); requestLog = []; await sync_to_server(); // Store all test dictionaries const dictPaths = [ "/dict/api-v1", "/dict/api-generic", "/dict/wildcard", "/dict/js", ]; for (let path of dictPaths) { let url = `https://localhost:${server.port()}${path}`; let chan = makeChan(url); let [, data] = await channelOpenPromise(chan); dump(`**** Dictionary loaded: ${path}, data length: ${data.length}\n`); // Verify dictionary was stored await new Promise(resolve => { verifyDictionaryStored(url, true, resolve); }); } dump("**** Setup complete\n"); }); // Test basic dictionary lookup and Available-Dictionary header generation add_task(async function test_basic_dictionary_retrieval() { let url = `https://localhost:${server.port()}/api/v1/users`; requestLog = []; await sync_to_server(); // Calculate expected hash for api_v1 dictionary let expectedHash = await calculateDictionaryHash( RETRIEVAL_TEST_DICTIONARIES.api_v1.content ); let chan = makeChan(url); let [, data] = await channelOpenPromise(chan); Assert.equal(data, "API V1 USERS DATA", "Resource content matches"); // Check request log to see if Available-Dictionary header was sent await sync_from_server(); let logEntry = requestLog.find(entry => entry.path === "/api/v1/users"); Assert.ok(logEntry && logEntry.hasAvailableDict, "Has Available-Dictionary"); Assert.ok( logEntry.availableDict.includes(expectedHash), "Available-Dictionary header should contain expected hash" ); dump("**** Basic retrieval test complete\n"); }); // Test URL pattern matching logic for dictionary selection add_task(async function test_dictionary_pattern_matching() { const patternMatchTests = [ { url: "/api/v1/users", expectedPattern: "/api/v1/*", dictKey: "api_v1" }, { url: "/api/v2/posts", expectedPattern: "/api/*", dictKey: "api_generic" }, { url: "/api/generic", expectedPattern: "/api/*", dictKey: "api_generic" }, { url: "/scripts/app.js", expectedPattern: "*.js", dictKey: "js_files" }, { url: "/web/page", expectedPattern: "*", dictKey: "wildcard" }, // Only wildcard should match { url: "/styles/main.css", expectedPattern: "*", dictKey: "wildcard" }, ]; requestLog = []; await sync_to_server(); for (let test of patternMatchTests) { let url = `https://localhost:${server.port()}${test.url}`; let expectedDict = RETRIEVAL_TEST_DICTIONARIES[test.dictKey]; let expectedHash = await calculateDictionaryHash(expectedDict.content); let chan = makeChan(url); let [, data] = await channelOpenPromise(chan); Assert.greater(data.length, 0, `Resource ${test.url} should have content`); // Check request log await sync_from_server(); let logEntry = requestLog.find(entry => entry.path === test.url); Assert.ok( logEntry && logEntry.hasAvailableDict, `Available-Dictionary header should be present for ${test.url}` ); if (logEntry && logEntry.hasAvailableDict) { Assert.ok( logEntry.availableDict.includes(expectedHash), `Available-Dictionary header should contain expected hash for ${test.url}` ); } } }); // Test dictionary precedence when multiple patterns match add_task(async function test_dictionary_precedence() { // Test URL that matches multiple patterns: /api/v1/users // Should match: "/api/v1/*" (most specific), "/api/*", "*" (wildcard) // Most specific pattern should take precedence let url = `https://localhost:${server.port()}/api/v1/users`; requestLog = []; await sync_to_server(); let mostSpecificHash = await calculateDictionaryHash( RETRIEVAL_TEST_DICTIONARIES.api_v1.content ); let chan = makeChan(url); let [, data] = await channelOpenPromise(chan); Assert.equal(data, "API V1 USERS DATA", "Content should match"); // Check request log for precedence await sync_from_server(); let logEntry = requestLog.find(entry => entry.path === "/api/v1/users"); Assert.ok( logEntry && logEntry.hasAvailableDict, "Available-Dictionary header should be present for precedence test" ); if (logEntry && logEntry.hasAvailableDict) { // The most specific pattern (/api/v1/*) should be included // Implementation may include multiple matching dictionaries Assert.ok( logEntry.availableDict.includes(mostSpecificHash), "Available-Dictionary header should contain most specific pattern hash" ); } }); // Test successful dictionary lookup and usage add_task(async function test_dictionary_cache_hit() { let url = `https://localhost:${server.port()}/api/generic`; requestLog = []; await sync_to_server(); let expectedHash = await calculateDictionaryHash( RETRIEVAL_TEST_DICTIONARIES.api_generic.content ); let chan = makeChan(url); let [, data] = await channelOpenPromise(chan); Assert.equal(data, "GENERIC API DATA", "Content should match"); // Verify dictionary lookup succeeded await sync_from_server(); let logEntry = requestLog.find(entry => entry.path === "/api/generic"); Assert.ok( logEntry && logEntry.hasAvailableDict, "Available-Dictionary header should be present for cache hit" ); if (logEntry && logEntry.hasAvailableDict) { Assert.ok( logEntry.availableDict.includes(expectedHash), "Available-Dictionary header should contain expected hash for cache hit" ); } }); // Test Available-Dictionary header hash format compliance add_task(async function test_dictionary_hash_format() { // Test that dictionary hashes follow IETF spec format: :base64hash: let testDict = RETRIEVAL_TEST_DICTIONARIES.api_v1; let calculatedHash = await calculateDictionaryHash(testDict.content); // Verify hash is base64 format Assert.greater(calculatedHash.length, 0, "Hash should not be empty"); // Verify base64 pattern (rough check) let base64Pattern = /^[A-Za-z0-9+/]*={0,2}$/; Assert.ok(base64Pattern.test(calculatedHash), "Hash should be valid base64"); // The hash format should be structured field byte sequence: :base64: let structuredFieldFormat = `:${calculatedHash}:`; Assert.ok( structuredFieldFormat.includes(calculatedHash), "Hash should follow structured field format" ); }); // Test retrieval with multiple dictionary matches add_task(async function test_multiple_dictionary_matches() { // Create a request that could match multiple dictionaries let url = `https://localhost:${server.port()}/api/test`; requestLog = []; await sync_to_server(); await registerDictionaryAwareEndpoint(server, "/api/test", "API TEST DATA"); let apiGenericHash = await calculateDictionaryHash( RETRIEVAL_TEST_DICTIONARIES.api_generic.content ); let chan = makeChan(url); let [, data] = await channelOpenPromise(chan); Assert.equal(data, "API TEST DATA", "Content should match"); // Check for multiple dictionary hashes in Available-Dictionary header await sync_from_server(); let logEntry = requestLog.find(entry => entry.path === "/api/test"); Assert.ok( logEntry && logEntry.hasAvailableDict, "Available-Dictionary header should be present for multiple matches" ); if (logEntry && logEntry.hasAvailableDict) { // Could match both /api/* and * patterns - verify the longest pattern's hash is present // (IETF spec says the longest match should be used) let hasApiGenericHash = logEntry.availableDict.includes(apiGenericHash); Assert.ok( hasApiGenericHash, "Available-Dictionary header should contain at least one expected hash for multiple matches" ); } }); // Cleanup add_task(async function cleanup() { // Clear cache let lci = Services.loadContextInfo.custom(false, { partitionKey: `(https,localhost)`, }); evict_cache_entries("all", lci); });