MozCachedOHTTPParent.sys.mjs (6598B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", 9 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 10 }); 11 12 // These match the serialization scheme used for header key/value pairs in 13 // nsHttpHeaderArray::FlattenOriginalHeader. 14 const RESPONSE_HEADER_KEY_VALUE_DELIMETER = ": "; 15 const RESPONSE_HEADER_DELIMETER = "\r\n"; 16 const RESPONSE_HEADER_METADATA_ELEMENT = "original-response-headers"; 17 18 /** 19 * Parent process JSActor for handling cache lookups for moz-cachged-ohttp 20 * protocol. This actor handles cache operations that require parent process 21 * privileges. 22 */ 23 export class MozCachedOHTTPParent extends JSProcessActorParent { 24 receiveMessage(message) { 25 if ( 26 this.manager.remoteType !== null && 27 this.manager.remoteType !== lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE 28 ) { 29 return Promise.reject(new Error("Process type mismatch")); 30 } 31 32 switch (message.name) { 33 case "tryCache": { 34 let uri = Services.io.newURI(message.data.uriString); 35 return this.tryCache(uri); 36 } 37 case "writeCache": { 38 let uri = Services.io.newURI(message.data.uriString); 39 return this.writeCache( 40 uri, 41 message.data.cacheInputStream, 42 message.data.cacheStreamUpdatePort 43 ); 44 } 45 } 46 47 return Promise.reject(new Error(`Unknown message: ${message.name}`)); 48 } 49 50 /** 51 * Attempts to load the resource from the HTTP cache without making network 52 * requests. On cache miss, provides an output stream for writing to cache. 53 * 54 * @param {nsIURI} resourceURI 55 * The URI of the resource to load from cache 56 * @returns {Promise<object>} 57 * Promise that resolves to result object with success flag and data 58 */ 59 async tryCache(resourceURI) { 60 try { 61 const cacheEntry = await this.#openCacheEntry( 62 resourceURI, 63 Ci.nsICacheStorage.OPEN_READONLY 64 ); 65 66 if (!cacheEntry.dataSize) { 67 throw new Error("Cache entry is empty."); 68 } 69 70 let headersStrings = cacheEntry 71 .getMetaDataElement(RESPONSE_HEADER_METADATA_ELEMENT) 72 .split(RESPONSE_HEADER_DELIMETER); 73 let headersObj = {}; 74 for (let headersString of headersStrings) { 75 let delimeterIndex = headersString.indexOf( 76 RESPONSE_HEADER_KEY_VALUE_DELIMETER 77 ); 78 let key = headersString.substring(0, delimeterIndex); 79 let value = headersString.substring( 80 delimeterIndex + RESPONSE_HEADER_KEY_VALUE_DELIMETER.length 81 ); 82 headersObj[key] = value; 83 } 84 85 // Cache hit - return input stream for reading 86 const inputStream = cacheEntry.openInputStream(0); 87 88 return { 89 success: true, 90 inputStream, 91 headersObj, 92 }; 93 } catch (e) { 94 // Cache miss or error - proceed without caching 95 return { success: false }; 96 } 97 } 98 99 /** 100 * Writes resource data to the HTTP cache. Opens a cache entry for the 101 * specified URI and copies data from the input stream to the cache, handling 102 * cache control messages for expiration and entry management. 103 * 104 * @param {nsIURI} resourceURI 105 * The URI of the resource to cache 106 * @param {nsIInputStream} cacheInputStream 107 * Input stream containing the resource data to cache 108 * @param {MessageChannelPort} cacheStreamUpdatePort 109 * MessagePort for receiving cache control messages: 110 * - "DoomCacheEntry": Remove cache entry (on error/no-cache) 111 * - "WriteCacheExpiry": Set cache expiration time 112 * @returns {Promise<undefined>} 113 * Promise that resolves when caching is complete 114 */ 115 async writeCache(resourceURI, cacheInputStream, cacheStreamUpdatePort) { 116 let cacheEntry; 117 let outputStream; 118 119 try { 120 cacheEntry = await this.#openCacheEntry( 121 resourceURI, 122 Ci.nsICacheStorage.OPEN_NORMALLY 123 ); 124 outputStream = cacheEntry.openOutputStream(0, -1); 125 } catch (e) { 126 return; 127 } 128 129 cacheStreamUpdatePort.onmessage = msg => { 130 switch (msg.data.name) { 131 case "DoomCacheEntry": { 132 cacheEntry.asyncDoom(null); 133 break; 134 } 135 case "WriteOriginalResponseHeaders": { 136 let headers = new Headers(msg.data.headersObj); 137 let headersStrings = []; 138 for (let [key, value] of headers.entries()) { 139 headersStrings.push( 140 `${key}${RESPONSE_HEADER_KEY_VALUE_DELIMETER}${value}` 141 ); 142 } 143 cacheEntry.setMetaDataElement( 144 RESPONSE_HEADER_METADATA_ELEMENT, 145 headersStrings.join(RESPONSE_HEADER_DELIMETER) 146 ); 147 break; 148 } 149 case "WriteCacheExpiry": { 150 cacheEntry.setExpirationTime(msg.data.expiry); 151 break; 152 } 153 } 154 }; 155 try { 156 await new Promise(resolve => { 157 lazy.NetUtil.asyncCopy(cacheInputStream, outputStream, writeResult => { 158 if (!Components.isSuccessCode(writeResult)) { 159 console.error( 160 "Failed to cache moz-cached-ohttp resource with result: ", 161 writeResult 162 ); 163 } 164 165 resolve(); 166 }); 167 }); 168 } finally { 169 cacheStreamUpdatePort.onmessage = null; 170 } 171 } 172 173 /** 174 * Opens a cache entry for the specified URI. 175 * 176 * @param {nsIURI} uri 177 * The URI to open cache entry for. 178 * @param {number} openFlags 179 * Cache storage open flags. 180 * @returns {Promise<nsICacheEntry|null>} 181 * Promise that resolves to cache entry or null if not available. 182 */ 183 async #openCacheEntry(uri, openFlags) { 184 const lci = Services.loadContextInfo.anonymous; 185 const storage = Services.cache2.diskCacheStorage(lci); 186 187 // For read-only access, check existence first 188 if ( 189 openFlags === Ci.nsICacheStorage.OPEN_READONLY && 190 !storage.exists(uri, "") 191 ) { 192 throw new Error("Cache entry does not exist."); 193 } 194 195 return new Promise((resolve, reject) => { 196 storage.asyncOpenURI(uri, "", openFlags, { 197 onCacheEntryCheck: () => Ci.nsICacheEntryOpenCallback.ENTRY_WANTED, 198 onCacheEntryAvailable: (entry, _isNew, status) => { 199 if (Components.isSuccessCode(status)) { 200 resolve(entry); 201 } else { 202 reject(new Error(`Cache entry operation failed: ${status}`)); 203 } 204 }, 205 }); 206 }); 207 } 208 }