ObliviousHTTP.sys.mjs (7251B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 7 const lazy = {}; 8 ChromeUtils.defineESModuleGetters(lazy, { 9 HPKEConfigManager: "resource://gre/modules/HPKEConfigManager.sys.mjs", 10 }); 11 ChromeUtils.defineLazyGetter(lazy, "decoder", () => new TextDecoder()); 12 XPCOMUtils.defineLazyServiceGetters(lazy, { 13 ohttpService: [ 14 "@mozilla.org/network/oblivious-http-service;1", 15 Ci.nsIObliviousHttpService, 16 ], 17 }); 18 19 const BinaryInputStream = Components.Constructor( 20 "@mozilla.org/binaryinputstream;1", 21 "nsIBinaryInputStream", 22 "setInputStream" 23 ); 24 25 const StringInputStream = Components.Constructor( 26 "@mozilla.org/io/string-input-stream;1", 27 "nsIStringInputStream", 28 "setByteStringData" 29 ); 30 31 const ArrayBufferInputStream = Components.Constructor( 32 "@mozilla.org/io/arraybuffer-input-stream;1", 33 "nsIArrayBufferInputStream", 34 "setData" 35 ); 36 37 function readFromStream(stream, count) { 38 let binaryStream = new BinaryInputStream(stream); 39 let arrayBuffer = new ArrayBuffer(count); 40 while (count > 0) { 41 let actuallyRead = binaryStream.readArrayBuffer(count, arrayBuffer); 42 if (!actuallyRead) { 43 throw new Error("Nothing read from input stream!"); 44 } 45 count -= actuallyRead; 46 } 47 return arrayBuffer; 48 } 49 50 /** 51 * @typedef {object} OHTTPResponse 52 * An object with properties mimicking that of a Response. 53 * @property {boolean} ok 54 * Indicates whether the request was successful. 55 * @property {number} status 56 * Representation of the HTTP status code. 57 * @property {?Headers} headers 58 * Representing the response headers. 59 * @property {() => ?JSON} json 60 * Returns the parsed JSON response body. 61 * @property {() => ?Blob} blob 62 * Returns a Blob response body. 63 */ 64 65 export class ObliviousHTTP { 66 /** 67 * Get a cached, or fetch a copy of, an OHTTP config from a given URL. 68 * 69 * @param {string} gatewayConfigURL 70 * The URL for the config that needs to be fetched. 71 * The URL should be complete (i.e. include the full path to the config). 72 * @returns {Promise<Uint8Array>} 73 * The config bytes. 74 */ 75 static async getOHTTPConfig(gatewayConfigURL) { 76 return lazy.HPKEConfigManager.get(gatewayConfigURL); 77 } 78 79 /** 80 * Make a request over OHTTP. 81 * 82 * @param {string} obliviousHTTPRelay 83 * The URL of the OHTTP relay to use. 84 * @param {Uint8Array} config 85 * A byte array representing the OHTTP config. 86 * @param {URL|string} requestURL 87 * The URL of the request we want to make over the relay. 88 * @param {object} options 89 * @param {string} [options.method] 90 * The HTTP method to use for the inner request. Only GET, POST, and PUT are 91 * supported right now. 92 * @param {string|ArrayBuffer} [options.body] 93 * The body content to send over the request. 94 * @param {object} options.headers 95 * The request headers to set. Each property of the object represents 96 * a header, with the key the header name and the value the header value. 97 * @param {AbortSignal} options.signal 98 * If the consumer passes an AbortSignal object, aborting the signal 99 * will abort the request. 100 * @param {Function} [options.abortCallback] 101 * Called if the abort signal is triggered before the request completes 102 * fully. 103 * 104 * @returns {Promise<OHTTPResponse>} 105 */ 106 static async ohttpRequest( 107 obliviousHTTPRelay, 108 config, 109 requestURL, 110 { method = "GET", body, headers, signal, abortCallback } 111 ) { 112 let relayURI = Services.io.newURI(obliviousHTTPRelay); 113 let requestURI = URL.isInstance(requestURL) 114 ? requestURL.URI 115 : Services.io.newURI(requestURL); 116 let obliviousHttpChannel = lazy.ohttpService 117 .newChannel(relayURI, requestURI, config) 118 .QueryInterface(Ci.nsIHttpChannel); 119 120 if (method == "POST" || method == "PUT" || method == "DELETE") { 121 let uploadChannel = obliviousHttpChannel.QueryInterface( 122 Ci.nsIUploadChannel2 123 ); 124 let bodyStream; 125 if (typeof body === "string") { 126 bodyStream = new StringInputStream(body); 127 } else if (body instanceof ArrayBuffer) { 128 bodyStream = new ArrayBufferInputStream(body, 0, body.byteLength); 129 } else { 130 throw new Error("ohttpRequest got unexpected body payload type."); 131 } 132 uploadChannel.explicitSetUploadStream( 133 bodyStream, 134 null, 135 -1, 136 method, 137 false 138 ); 139 } else if (method != "GET") { 140 throw new Error(`Unsupported HTTP verb ${method}`); 141 } 142 143 for (let headerName of Object.keys(headers)) { 144 obliviousHttpChannel.setRequestHeader( 145 headerName, 146 headers[headerName], 147 false 148 ); 149 } 150 let abortHandler = () => { 151 abortCallback?.(); 152 obliviousHttpChannel.cancel(Cr.NS_BINDING_ABORTED); 153 }; 154 signal.addEventListener("abort", abortHandler); 155 return new Promise((resolve, reject) => { 156 let listener = { 157 _buffer: [], 158 _headers: null, 159 QueryInterface: ChromeUtils.generateQI([ 160 "nsIStreamListener", 161 "nsIRequestObserver", 162 ]), 163 onStartRequest(request) { 164 this._headers = new Headers(); 165 try { 166 request 167 .QueryInterface(Ci.nsIHttpChannel) 168 .visitResponseHeaders((header, value) => { 169 this._headers.append(header, value); 170 }); 171 } catch (error) { 172 this._headers = null; 173 } 174 }, 175 onDataAvailable(request, stream, offset, count) { 176 this._buffer.push(readFromStream(stream, count)); 177 }, 178 onStopRequest(request, requestStatus) { 179 signal.removeEventListener("abort", abortHandler); 180 let result = this._buffer; 181 try { 182 let ohttpStatus = request.QueryInterface(Ci.nsIObliviousHttpChannel) 183 .relayChannel.responseStatus; 184 if (ohttpStatus == 200) { 185 let httpStatus = request.QueryInterface( 186 Ci.nsIHttpChannel 187 ).responseStatus; 188 resolve({ 189 ok: requestStatus == Cr.NS_OK && httpStatus == 200, 190 status: httpStatus, 191 headers: this._headers, 192 json() { 193 let decodedBuffer = result.reduce((accumulator, currVal) => { 194 return accumulator + lazy.decoder.decode(currVal); 195 }, ""); 196 return JSON.parse(decodedBuffer); 197 }, 198 blob() { 199 return new Blob(result, { type: "image/jpeg" }); 200 }, 201 }); 202 } else { 203 resolve({ 204 ok: false, 205 status: ohttpStatus, 206 headers: null, 207 json() { 208 return null; 209 }, 210 blob() { 211 return null; 212 }, 213 }); 214 } 215 } catch (error) { 216 reject(error); 217 } 218 }, 219 }; 220 obliviousHttpChannel.asyncOpen(listener); 221 }); 222 } 223 }