Pier Angelo Vendrame pushed to branch tor-browser-115.6.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits:
- 
da0f3108
by Pier Angelo Vendrame at 2024-01-09T18:38:42+01:00
3 changed files:
- + toolkit/modules/DomainFrontedRequests.sys.mjs
- toolkit/modules/Moat.sys.mjs
- toolkit/modules/moz.build
Changes:
| 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 | +  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
 | |
| 9 | +  Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
 | |
| 10 | +  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
 | |
| 11 | +  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
 | |
| 12 | +  TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
 | |
| 13 | +});
 | |
| 14 | + | |
| 15 | +/**
 | |
| 16 | + * The meek pluggable transport takes the reflector URL and front domain as
 | |
| 17 | + * proxy credentials, which can be prepared with this function.
 | |
| 18 | + *
 | |
| 19 | + * @param {string} proxyType The proxy type (socks for socks5 or socks4)
 | |
| 20 | + * @param {string} reflector The URL of the service hosted by the CDN
 | |
| 21 | + * @param {string} front The domain to use as a front
 | |
| 22 | + * @returns {string[]} An array containing [username, password]
 | |
| 23 | + */
 | |
| 24 | +function makeMeekCredentials(proxyType, reflector, front) {
 | |
| 25 | +  // Construct the per-connection arguments.
 | |
| 26 | +  let meekClientEscapedArgs = "";
 | |
| 27 | + | |
| 28 | +  // Escape aValue per section 3.5 of the PT specification:
 | |
| 29 | +  //   First the "<Key>=<Value>" formatted arguments MUST be escaped,
 | |
| 30 | +  //   such that all backslash, equal sign, and semicolon characters
 | |
| 31 | +  //   are escaped with a backslash.
 | |
| 32 | +  const escapeArgValue = aValue =>
 | |
| 33 | +    aValue
 | |
| 34 | +      ? aValue
 | |
| 35 | +          .replaceAll("\\", "\\\\")
 | |
| 36 | +          .replaceAll("=", "\\=")
 | |
| 37 | +          .replaceAll(";", "\\;")
 | |
| 38 | +      : "";
 | |
| 39 | + | |
| 40 | +  if (reflector) {
 | |
| 41 | +    meekClientEscapedArgs += "url="">";
 | |
| 42 | +    meekClientEscapedArgs += escapeArgValue(reflector);
 | |
| 43 | +  }
 | |
| 44 | + | |
| 45 | +  if (front) {
 | |
| 46 | +    if (meekClientEscapedArgs.length) {
 | |
| 47 | +      meekClientEscapedArgs += ";";
 | |
| 48 | +    }
 | |
| 49 | +    meekClientEscapedArgs += "front=";
 | |
| 50 | +    meekClientEscapedArgs += escapeArgValue(front);
 | |
| 51 | +  }
 | |
| 52 | + | |
| 53 | +  // socks5
 | |
| 54 | +  if (proxyType === "socks") {
 | |
| 55 | +    if (meekClientEscapedArgs.length <= 255) {
 | |
| 56 | +      return [meekClientEscapedArgs, "\x00"];
 | |
| 57 | +    }
 | |
| 58 | +    return [
 | |
| 59 | +      meekClientEscapedArgs.substring(0, 255),
 | |
| 60 | +      meekClientEscapedArgs.substring(255),
 | |
| 61 | +    ];
 | |
| 62 | +  } else if (proxyType === "socks4") {
 | |
| 63 | +    return [meekClientEscapedArgs, undefined];
 | |
| 64 | +  }
 | |
| 65 | +  throw new Error(`Unsupported proxy type ${proxyType}.`);
 | |
| 66 | +}
 | |
| 67 | + | |
| 68 | +/**
 | |
| 69 | + * Subprocess-based implementation to launch and control a PT process.
 | |
| 70 | + */
 | |
| 71 | +class MeekTransport {
 | |
| 72 | +  // These members are used by consumers to setup the proxy to do requests over
 | |
| 73 | +  // meek. They are passed to newProxyInfoWithAuth.
 | |
| 74 | +  proxyType = null;
 | |
| 75 | +  proxyAddress = null;
 | |
| 76 | +  proxyPort = 0;
 | |
| 77 | +  proxyUsername = null;
 | |
| 78 | +  proxyPassword = null;
 | |
| 79 | + | |
| 80 | +  #inited = false;
 | |
| 81 | +  #meekClientProcess = null;
 | |
| 82 | + | |
| 83 | +  // launches the meekprocess
 | |
| 84 | +  async init(reflector, front) {
 | |
| 85 | +    // ensure we haven't already init'd
 | |
| 86 | +    if (this.#inited) {
 | |
| 87 | +      throw new Error("MeekTransport: Already initialized");
 | |
| 88 | +    }
 | |
| 89 | + | |
| 90 | +    try {
 | |
| 91 | +      // figure out which pluggable transport to use
 | |
| 92 | +      const supportedTransports = ["meek", "meek_lite"];
 | |
| 93 | +      const provider = await lazy.TorProviderBuilder.build();
 | |
| 94 | +      const proxy = (await provider.getPluggableTransports()).find(
 | |
| 95 | +        pt =>
 | |
| 96 | +          pt.type === "exec" &&
 | |
| 97 | +          supportedTransports.some(t => pt.transports.includes(t))
 | |
| 98 | +      );
 | |
| 99 | +      if (!proxy) {
 | |
| 100 | +        throw new Error("No supported transport found.");
 | |
| 101 | +      }
 | |
| 102 | + | |
| 103 | +      const meekTransport = proxy.transports.find(t =>
 | |
| 104 | +        supportedTransports.includes(t)
 | |
| 105 | +      );
 | |
| 106 | +      // Convert meek client path to absolute path if necessary
 | |
| 107 | +      const meekWorkDir = lazy.TorLauncherUtil.getTorFile(
 | |
| 108 | +        "pt-startup-dir",
 | |
| 109 | +        false
 | |
| 110 | +      );
 | |
| 111 | +      if (lazy.TorLauncherUtil.isPathRelative(proxy.pathToBinary)) {
 | |
| 112 | +        const meekPath = meekWorkDir.clone();
 | |
| 113 | +        meekPath.appendRelativePath(proxy.pathToBinary);
 | |
| 114 | +        proxy.pathToBinary = meekPath.path;
 | |
| 115 | +      }
 | |
| 116 | + | |
| 117 | +      // Setup env and start meek process
 | |
| 118 | +      const ptStateDir = lazy.TorLauncherUtil.getTorFile("tordatadir", false);
 | |
| 119 | +      ptStateDir.append("pt_state"); // Match what tor uses.
 | |
| 120 | + | |
| 121 | +      const envAdditions = {
 | |
| 122 | +        TOR_PT_MANAGED_TRANSPORT_VER: "1",
 | |
| 123 | +        TOR_PT_STATE_LOCATION: ptStateDir.path,
 | |
| 124 | +        TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
 | |
| 125 | +        TOR_PT_CLIENT_TRANSPORTS: meekTransport,
 | |
| 126 | +      };
 | |
| 127 | +      if (lazy.TorSettings.proxy.enabled) {
 | |
| 128 | +        envAdditions.TOR_PT_PROXY = lazy.TorSettings.proxy.uri;
 | |
| 129 | +      }
 | |
| 130 | + | |
| 131 | +      const opts = {
 | |
| 132 | +        command: proxy.pathToBinary,
 | |
| 133 | +        arguments: proxy.options.split(/s+/),
 | |
| 134 | +        workdir: meekWorkDir.path,
 | |
| 135 | +        environmentAppend: true,
 | |
| 136 | +        environment: envAdditions,
 | |
| 137 | +        stderr: "pipe",
 | |
| 138 | +      };
 | |
| 139 | + | |
| 140 | +      // Launch meek client
 | |
| 141 | +      this.#meekClientProcess = await lazy.Subprocess.call(opts);
 | |
| 142 | + | |
| 143 | +      // Callback chain for reading stderr
 | |
| 144 | +      const stderrLogger = async () => {
 | |
| 145 | +        while (this.#meekClientProcess) {
 | |
| 146 | +          const errString = await this.#meekClientProcess.stderr.readString();
 | |
| 147 | +          if (errString) {
 | |
| 148 | +            console.log(`MeekTransport: stderr => ${errString}`);
 | |
| 149 | +          }
 | |
| 150 | +        }
 | |
| 151 | +      };
 | |
| 152 | +      stderrLogger();
 | |
| 153 | + | |
| 154 | +      // Read pt's stdout until terminal (CMETHODS DONE) is reached
 | |
| 155 | +      // returns array of lines for parsing
 | |
| 156 | +      const getInitLines = async (stdout = "") => {
 | |
| 157 | +        stdout += await this.#meekClientProcess.stdout.readString();
 | |
| 158 | + | |
| 159 | +        // look for the final message
 | |
| 160 | +        const CMETHODS_DONE = "CMETHODS DONE";
 | |
| 161 | +        let endIndex = stdout.lastIndexOf(CMETHODS_DONE);
 | |
| 162 | +        if (endIndex !== -1) {
 | |
| 163 | +          endIndex += CMETHODS_DONE.length;
 | |
| 164 | +          return stdout.substring(0, endIndex).split("\n");
 | |
| 165 | +        }
 | |
| 166 | +        return getInitLines(stdout);
 | |
| 167 | +      };
 | |
| 168 | + | |
| 169 | +      // read our lines from pt's stdout
 | |
| 170 | +      const meekInitLines = await getInitLines();
 | |
| 171 | +      // tokenize our pt lines
 | |
| 172 | +      const meekInitTokens = meekInitLines.map(line => {
 | |
| 173 | +        const tokens = line.split(" ");
 | |
| 174 | +        return {
 | |
| 175 | +          keyword: tokens[0],
 | |
| 176 | +          args: tokens.slice(1),
 | |
| 177 | +        };
 | |
| 178 | +      });
 | |
| 179 | + | |
| 180 | +      // parse our pt tokens
 | |
| 181 | +      for (const { keyword, args } of meekInitTokens) {
 | |
| 182 | +        const argsJoined = args.join(" ");
 | |
| 183 | +        let keywordError = false;
 | |
| 184 | +        switch (keyword) {
 | |
| 185 | +          case "VERSION": {
 | |
| 186 | +            if (args.length !== 1 || args[0] !== "1") {
 | |
| 187 | +              keywordError = true;
 | |
| 188 | +            }
 | |
| 189 | +            break;
 | |
| 190 | +          }
 | |
| 191 | +          case "PROXY": {
 | |
| 192 | +            if (args.length !== 1 || args[0] !== "DONE") {
 | |
| 193 | +              keywordError = true;
 | |
| 194 | +            }
 | |
| 195 | +            break;
 | |
| 196 | +          }
 | |
| 197 | +          case "CMETHOD": {
 | |
| 198 | +            if (args.length !== 3) {
 | |
| 199 | +              keywordError = true;
 | |
| 200 | +              break;
 | |
| 201 | +            }
 | |
| 202 | +            const transport = args[0];
 | |
| 203 | +            const proxyType = args[1];
 | |
| 204 | +            const addrPortString = args[2];
 | |
| 205 | +            const addrPort = addrPortString.split(":");
 | |
| 206 | + | |
| 207 | +            if (transport !== meekTransport) {
 | |
| 208 | +              throw new Error(
 | |
| 209 | +                `MeekTransport: Expected ${meekTransport} but found ${transport}`
 | |
| 210 | +              );
 | |
| 211 | +            }
 | |
| 212 | +            if (!["socks4", "socks4a", "socks5"].includes(proxyType)) {
 | |
| 213 | +              throw new Error(
 | |
| 214 | +                `MeekTransport: Invalid proxy type => ${proxyType}`
 | |
| 215 | +              );
 | |
| 216 | +            }
 | |
| 217 | +            if (addrPort.length !== 2) {
 | |
| 218 | +              throw new Error(
 | |
| 219 | +                `MeekTransport: Invalid proxy address => ${addrPortString}`
 | |
| 220 | +              );
 | |
| 221 | +            }
 | |
| 222 | +            const addr = addrPort[0];
 | |
| 223 | +            const port = parseInt(addrPort[1]);
 | |
| 224 | +            if (port < 1 || port > 65535) {
 | |
| 225 | +              throw new Error(`MeekTransport: Invalid proxy port => ${port}`);
 | |
| 226 | +            }
 | |
| 227 | + | |
| 228 | +            // convert proxy type to strings used by protocol-proxy-servce
 | |
| 229 | +            this.proxyType = proxyType === "socks5" ? "socks" : "socks4";
 | |
| 230 | +            this.proxyAddress = addr;
 | |
| 231 | +            this.proxyPort = port;
 | |
| 232 | + | |
| 233 | +            break;
 | |
| 234 | +          }
 | |
| 235 | +          // terminal
 | |
| 236 | +          case "CMETHODS": {
 | |
| 237 | +            if (args.length !== 1 || args[0] !== "DONE") {
 | |
| 238 | +              keywordError = true;
 | |
| 239 | +            }
 | |
| 240 | +            break;
 | |
| 241 | +          }
 | |
| 242 | +          // errors (all fall through):
 | |
| 243 | +          case "VERSION-ERROR":
 | |
| 244 | +          case "ENV-ERROR":
 | |
| 245 | +          case "PROXY-ERROR":
 | |
| 246 | +          case "CMETHOD-ERROR":
 | |
| 247 | +            throw new Error(`MeekTransport: ${keyword} => '${argsJoined}'`);
 | |
| 248 | +        }
 | |
| 249 | +        if (keywordError) {
 | |
| 250 | +          throw new Error(
 | |
| 251 | +            `MeekTransport: Invalid ${keyword} keyword args => '${argsJoined}'`
 | |
| 252 | +          );
 | |
| 253 | +        }
 | |
| 254 | +      }
 | |
| 255 | + | |
| 256 | +      // register callback to cleanup on process exit
 | |
| 257 | +      this.#meekClientProcess.wait().then(exitObj => {
 | |
| 258 | +        this.#meekClientProcess = null;
 | |
| 259 | +        this.uninit();
 | |
| 260 | +      });
 | |
| 261 | +      [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
 | |
| 262 | +        this.proxyType,
 | |
| 263 | +        reflector,
 | |
| 264 | +        front
 | |
| 265 | +      );
 | |
| 266 | +      this.#inited = true;
 | |
| 267 | +    } catch (ex) {
 | |
| 268 | +      if (this.#meekClientProcess) {
 | |
| 269 | +        this.#meekClientProcess.kill();
 | |
| 270 | +        this.#meekClientProcess = null;
 | |
| 271 | +      }
 | |
| 272 | +      throw ex;
 | |
| 273 | +    }
 | |
| 274 | +  }
 | |
| 275 | + | |
| 276 | +  async uninit() {
 | |
| 277 | +    this.#inited = false;
 | |
| 278 | + | |
| 279 | +    await this.#meekClientProcess?.kill();
 | |
| 280 | +    this.#meekClientProcess = null;
 | |
| 281 | +    this.proxyType = null;
 | |
| 282 | +    this.proxyAddress = null;
 | |
| 283 | +    this.proxyPort = 0;
 | |
| 284 | +    this.proxyUsername = null;
 | |
| 285 | +    this.proxyPassword = null;
 | |
| 286 | +  }
 | |
| 287 | +}
 | |
| 288 | + | |
| 289 | +/**
 | |
| 290 | + * Android implementation of the Meek process.
 | |
| 291 | + *
 | |
| 292 | + * GeckoView does not provide the subprocess module, so we have to use the
 | |
| 293 | + * EventDispatcher, and have a Java handler start and stop the proxy process.
 | |
| 294 | + */
 | |
| 295 | +class MeekTransportAndroid {
 | |
| 296 | +  // These members are used by consumers to setup the proxy to do requests over
 | |
| 297 | +  // meek. They are passed to newProxyInfoWithAuth.
 | |
| 298 | +  proxyType = null;
 | |
| 299 | +  proxyAddress = null;
 | |
| 300 | +  proxyPort = 0;
 | |
| 301 | +  proxyUsername = null;
 | |
| 302 | +  proxyPassword = null;
 | |
| 303 | + | |
| 304 | +  /**
 | |
| 305 | +   * An id for process this instance is linked to.
 | |
| 306 | +   *
 | |
| 307 | +   * Since we do not restrict the transport to be a singleton, we need a handle to
 | |
| 308 | +   * identify the process we want to stop when the transport owner is done.
 | |
| 309 | +   * We use a counter incremented on the Java side for now.
 | |
| 310 | +   *
 | |
| 311 | +   * This number must be a positive integer (i.e., 0 is an invalid handler).
 | |
| 312 | +   *
 | |
| 313 | +   * @type {number}
 | |
| 314 | +   */
 | |
| 315 | +  #id = 0;
 | |
| 316 | + | |
| 317 | +  async init(reflector, front) {
 | |
| 318 | +    // ensure we haven't already init'd
 | |
| 319 | +    if (this.#id) {
 | |
| 320 | +      throw new Error("MeekTransport: Already initialized");
 | |
| 321 | +    }
 | |
| 322 | +    const details = await lazy.EventDispatcher.instance.sendRequestForResult({
 | |
| 323 | +      type: "GeckoView:Tor:StartMeek",
 | |
| 324 | +    });
 | |
| 325 | +    this.#id = details.id;
 | |
| 326 | +    this.proxyType = "socks";
 | |
| 327 | +    this.proxyAddress = details.address;
 | |
| 328 | +    this.proxyPort = details.port;
 | |
| 329 | +    [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
 | |
| 330 | +      this.proxyType,
 | |
| 331 | +      reflector,
 | |
| 332 | +      front
 | |
| 333 | +    );
 | |
| 334 | +  }
 | |
| 335 | + | |
| 336 | +  async uninit() {
 | |
| 337 | +    lazy.EventDispatcher.instance.sendRequest({
 | |
| 338 | +      type: "GeckoView:Tor:StopMeek",
 | |
| 339 | +      id: this.#id,
 | |
| 340 | +    });
 | |
| 341 | +    this.#id = 0;
 | |
| 342 | +    this.proxyType = null;
 | |
| 343 | +    this.proxyAddress = null;
 | |
| 344 | +    this.proxyPort = 0;
 | |
| 345 | +    this.proxyUsername = null;
 | |
| 346 | +    this.proxyPassword = null;
 | |
| 347 | +  }
 | |
| 348 | +}
 | |
| 349 | + | |
| 350 | +/**
 | |
| 351 | + * Callback object to promisify the XPCOM request.
 | |
| 352 | + */
 | |
| 353 | +class ResponseListener {
 | |
| 354 | +  #response = "";
 | |
| 355 | +  #responsePromise;
 | |
| 356 | +  #resolve;
 | |
| 357 | +  #reject;
 | |
| 358 | +  constructor() {
 | |
| 359 | +    this.#response = "";
 | |
| 360 | +    // we need this promise here because await nsIHttpChannel::asyncOpen does
 | |
| 361 | +    // not return only once the request is complete, it seems to return
 | |
| 362 | +    // after it begins, so we have to get the result from this listener object.
 | |
| 363 | +    // This promise is only resolved once onStopRequest is called
 | |
| 364 | +    this.#responsePromise = new Promise((resolve, reject) => {
 | |
| 365 | +      this.#resolve = resolve;
 | |
| 366 | +      this.#reject = reject;
 | |
| 367 | +    });
 | |
| 368 | +  }
 | |
| 369 | + | |
| 370 | +  // callers wait on this for final response
 | |
| 371 | +  response() {
 | |
| 372 | +    return this.#responsePromise;
 | |
| 373 | +  }
 | |
| 374 | + | |
| 375 | +  // noop
 | |
| 376 | +  onStartRequest(request) {}
 | |
| 377 | + | |
| 378 | +  // resolve or reject our Promise
 | |
| 379 | +  onStopRequest(request, status) {
 | |
| 380 | +    try {
 | |
| 381 | +      if (!Components.isSuccessCode(status)) {
 | |
| 382 | +        const errorMessage =
 | |
| 383 | +          lazy.TorLauncherUtil.getLocalizedStringForError(status);
 | |
| 384 | +        this.#reject(new Error(errorMessage));
 | |
| 385 | +      }
 | |
| 386 | +      if (request.responseStatus !== 200) {
 | |
| 387 | +        this.#reject(new Error(request.responseStatusText));
 | |
| 388 | +      }
 | |
| 389 | +    } catch (err) {
 | |
| 390 | +      this.#reject(err);
 | |
| 391 | +    }
 | |
| 392 | +    this.#resolve(this.#response);
 | |
| 393 | +  }
 | |
| 394 | + | |
| 395 | +  // read response data
 | |
| 396 | +  onDataAvailable(request, stream, offset, length) {
 | |
| 397 | +    const scriptableStream = Cc[
 | |
| 398 | +      "@mozilla.org/scriptableinputstream;1"
 | |
| 399 | +    ].createInstance(Ci.nsIScriptableInputStream);
 | |
| 400 | +    scriptableStream.init(stream);
 | |
| 401 | +    this.#response += scriptableStream.read(length);
 | |
| 402 | +  }
 | |
| 403 | +}
 | |
| 404 | + | |
| 405 | +// constructs the json objects and sends the request over moat
 | |
| 406 | +export class DomainFrontRequestBuilder {
 | |
| 407 | +  #inited = false;
 | |
| 408 | +  #meekTransport = null;
 | |
| 409 | + | |
| 410 | +  get inited() {
 | |
| 411 | +    return this.#inited;
 | |
| 412 | +  }
 | |
| 413 | + | |
| 414 | +  async init(reflector, front) {
 | |
| 415 | +    if (this.#inited) {
 | |
| 416 | +      throw new Error("MoatRPC: Already initialized");
 | |
| 417 | +    }
 | |
| 418 | + | |
| 419 | +    const meekTransport =
 | |
| 420 | +      Services.appinfo.OS === "Android"
 | |
| 421 | +        ? new MeekTransportAndroid()
 | |
| 422 | +        : new MeekTransport();
 | |
| 423 | +    await meekTransport.init(reflector, front);
 | |
| 424 | +    this.#meekTransport = meekTransport;
 | |
| 425 | +    this.#inited = true;
 | |
| 426 | +  }
 | |
| 427 | + | |
| 428 | +  async uninit() {
 | |
| 429 | +    await this.#meekTransport?.uninit();
 | |
| 430 | +    this.#meekTransport = null;
 | |
| 431 | +    this.#inited = false;
 | |
| 432 | +  }
 | |
| 433 | + | |
| 434 | +  buildHttpHandler(uriString) {
 | |
| 435 | +    if (!this.#inited) {
 | |
| 436 | +      throw new Error("MoatRPC: Not initialized");
 | |
| 437 | +    }
 | |
| 438 | + | |
| 439 | +    const { proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword } =
 | |
| 440 | +      this.#meekTransport;
 | |
| 441 | + | |
| 442 | +    const proxyPS = Cc[
 | |
| 443 | +      "@mozilla.org/network/protocol-proxy-service;1"
 | |
| 444 | +    ].getService(Ci.nsIProtocolProxyService);
 | |
| 445 | +    const flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
 | |
| 446 | +    const noTimeout = 0xffffffff; // UINT32_MAX
 | |
| 447 | +    const proxyInfo = proxyPS.newProxyInfoWithAuth(
 | |
| 448 | +      proxyType,
 | |
| 449 | +      proxyAddress,
 | |
| 450 | +      proxyPort,
 | |
| 451 | +      proxyUsername,
 | |
| 452 | +      proxyPassword,
 | |
| 453 | +      undefined,
 | |
| 454 | +      undefined,
 | |
| 455 | +      flags,
 | |
| 456 | +      noTimeout,
 | |
| 457 | +      undefined
 | |
| 458 | +    );
 | |
| 459 | + | |
| 460 | +    const uri = Services.io.newURI(uriString);
 | |
| 461 | +    // There does not seem to be a way to directly create an nsILoadInfo from
 | |
| 462 | +    // _javascript_, so we create a throw away non-proxied channel to get one.
 | |
| 463 | +    const secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
 | |
| 464 | +    const loadInfo = Services.io.newChannelFromURI(
 | |
| 465 | +      uri,
 | |
| 466 | +      undefined,
 | |
| 467 | +      Services.scriptSecurityManager.getSystemPrincipal(),
 | |
| 468 | +      undefined,
 | |
| 469 | +      secFlags,
 | |
| 470 | +      Ci.nsIContentPolicy.TYPE_OTHER
 | |
| 471 | +    ).loadInfo;
 | |
| 472 | + | |
| 473 | +    const httpHandler = Services.io
 | |
| 474 | +      .getProtocolHandler("http")
 | |
| 475 | +      .QueryInterface(Ci.nsIHttpProtocolHandler);
 | |
| 476 | +    const ch = httpHandler
 | |
| 477 | +      .newProxiedChannel(uri, proxyInfo, 0, undefined, loadInfo)
 | |
| 478 | +      .QueryInterface(Ci.nsIHttpChannel);
 | |
| 479 | + | |
| 480 | +    // remove all headers except for 'Host"
 | |
| 481 | +    const headers = [];
 | |
| 482 | +    ch.visitRequestHeaders({
 | |
| 483 | +      visitHeader: (key, val) => {
 | |
| 484 | +        if (key !== "Host") {
 | |
| 485 | +          headers.push(key);
 | |
| 486 | +        }
 | |
| 487 | +      },
 | |
| 488 | +    });
 | |
| 489 | +    headers.forEach(key => ch.setRequestHeader(key, "", false));
 | |
| 490 | + | |
| 491 | +    return ch;
 | |
| 492 | +  }
 | |
| 493 | + | |
| 494 | +  /**
 | |
| 495 | +   * Make a POST request with a JSON body.
 | |
| 496 | +   *
 | |
| 497 | +   * @param {string} url The URL to load
 | |
| 498 | +   * @param {object} args The arguments to send to the procedure. It will be
 | |
| 499 | +   * serialized to JSON by this function and then set as POST body
 | |
| 500 | +   * @returns {Promise<object>} A promise with the parsed response
 | |
| 501 | +   */
 | |
| 502 | +  async buildPostRequest(url, args) {
 | |
| 503 | +    const ch = this.buildHttpHandler(url);
 | |
| 504 | + | |
| 505 | +    const argsJson = JSON.stringify(args);
 | |
| 506 | +    const inStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
 | |
| 507 | +      Ci.nsIStringInputStream
 | |
| 508 | +    );
 | |
| 509 | +    inStream.setData(argsJson, argsJson.length);
 | |
| 510 | +    const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
 | |
| 511 | +    const contentType = "application/vnd.api+json";
 | |
| 512 | +    upChannel.setUploadStream(inStream, contentType, argsJson.length);
 | |
| 513 | +    ch.requestMethod = "POST";
 | |
| 514 | + | |
| 515 | +    // Make request
 | |
| 516 | +    const listener = new ResponseListener();
 | |
| 517 | +    await ch.asyncOpen(listener, ch);
 | |
| 518 | + | |
| 519 | +    // wait for response
 | |
| 520 | +    const responseJSON = await listener.response();
 | |
| 521 | + | |
| 522 | +    // parse that JSON
 | |
| 523 | +    return JSON.parse(responseJSON);
 | |
| 524 | +  }
 | |
| 525 | +} | 
| ... | ... | @@ -10,10 +10,8 @@ import { | 
| 10 | 10 |  const lazy = {};
 | 
| 11 | 11 | |
| 12 | 12 |  ChromeUtils.defineESModuleGetters(lazy, {
 | 
| 13 | -  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
 | |
| 14 | -  Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
 | |
| 15 | -  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
 | |
| 16 | -  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
 | |
| 13 | +  DomainFrontRequestBuilder:
 | |
| 14 | +    "resource://gre/modules/DomainFrontedRequests.sys.mjs",
 | |
| 17 | 15 |  });
 | 
| 18 | 16 | |
| 19 | 17 |  const TorLauncherPrefs = Object.freeze({
 | 
| ... | ... | @@ -22,372 +20,9 @@ const TorLauncherPrefs = Object.freeze({ | 
| 22 | 20 |    moat_service: "extensions.torlauncher.moat_service",
 | 
| 23 | 21 |  });
 | 
| 24 | 22 | |
| 25 | -function makeMeekCredentials(proxyType) {
 | |
| 26 | -  // Construct the per-connection arguments.
 | |
| 27 | -  let meekClientEscapedArgs = "";
 | |
| 28 | -  const meekReflector = Services.prefs.getStringPref(
 | |
| 29 | -    TorLauncherPrefs.bridgedb_reflector
 | |
| 30 | -  );
 | |
| 31 | - | |
| 32 | -  // Escape aValue per section 3.5 of the PT specification:
 | |
| 33 | -  //   First the "<Key>=<Value>" formatted arguments MUST be escaped,
 | |
| 34 | -  //   such that all backslash, equal sign, and semicolon characters
 | |
| 35 | -  //   are escaped with a backslash.
 | |
| 36 | -  const escapeArgValue = aValue =>
 | |
| 37 | -    aValue
 | |
| 38 | -      ? aValue
 | |
| 39 | -          .replaceAll("\\", "\\\\")
 | |
| 40 | -          .replaceAll("=", "\\=")
 | |
| 41 | -          .replaceAll(";", "\\;")
 | |
| 42 | -      : "";
 | |
| 43 | - | |
| 44 | -  if (meekReflector) {
 | |
| 45 | -    meekClientEscapedArgs += "url="">";
 | |
| 46 | -    meekClientEscapedArgs += escapeArgValue(meekReflector);
 | |
| 47 | -  }
 | |
| 48 | -  const meekFront = Services.prefs.getStringPref(
 | |
| 49 | -    TorLauncherPrefs.bridgedb_front
 | |
| 50 | -  );
 | |
| 51 | -  if (meekFront) {
 | |
| 52 | -    if (meekClientEscapedArgs.length) {
 | |
| 53 | -      meekClientEscapedArgs += ";";
 | |
| 54 | -    }
 | |
| 55 | -    meekClientEscapedArgs += "front=";
 | |
| 56 | -    meekClientEscapedArgs += escapeArgValue(meekFront);
 | |
| 57 | -  }
 | |
| 58 | - | |
| 59 | -  // socks5
 | |
| 60 | -  if (proxyType === "socks") {
 | |
| 61 | -    if (meekClientEscapedArgs.length <= 255) {
 | |
| 62 | -      return [meekClientEscapedArgs, "\x00"];
 | |
| 63 | -    } else {
 | |
| 64 | -      return [
 | |
| 65 | -        meekClientEscapedArgs.substring(0, 255),
 | |
| 66 | -        meekClientEscapedArgs.substring(255),
 | |
| 67 | -      ];
 | |
| 68 | -    }
 | |
| 69 | -    // socks4
 | |
| 70 | -  } else {
 | |
| 71 | -    return [meekClientEscapedArgs, undefined];
 | |
| 72 | -  }
 | |
| 73 | -}
 | |
| 74 | - | |
| 75 | -//
 | |
| 76 | -// Launches and controls the PT process lifetime
 | |
| 77 | -//
 | |
| 78 | -class MeekTransport {
 | |
| 79 | -  // These members are used by consumers to setup the proxy to do requests over
 | |
| 80 | -  // meek. They are passed to newProxyInfoWithAuth.
 | |
| 81 | -  proxyType = null;
 | |
| 82 | -  proxyAddress = null;
 | |
| 83 | -  proxyPort = 0;
 | |
| 84 | -  proxyUsername = null;
 | |
| 85 | -  proxyPassword = null;
 | |
| 86 | - | |
| 87 | -  #inited = false;
 | |
| 88 | -  #meekClientProcess = null;
 | |
| 89 | - | |
| 90 | -  // launches the meekprocess
 | |
| 91 | -  async init() {
 | |
| 92 | -    // ensure we haven't already init'd
 | |
| 93 | -    if (this.#inited) {
 | |
| 94 | -      throw new Error("MeekTransport: Already initialized");
 | |
| 95 | -    }
 | |
| 96 | - | |
| 97 | -    try {
 | |
| 98 | -      // figure out which pluggable transport to use
 | |
| 99 | -      const supportedTransports = ["meek", "meek_lite"];
 | |
| 100 | -      const provider = await lazy.TorProviderBuilder.build();
 | |
| 101 | -      const proxy = (await provider.getPluggableTransports()).find(
 | |
| 102 | -        pt =>
 | |
| 103 | -          pt.type === "exec" &&
 | |
| 104 | -          supportedTransports.some(t => pt.transports.includes(t))
 | |
| 105 | -      );
 | |
| 106 | -      if (!proxy) {
 | |
| 107 | -        throw new Error("No supported transport found.");
 | |
| 108 | -      }
 | |
| 109 | - | |
| 110 | -      const meekTransport = proxy.transports.find(t =>
 | |
| 111 | -        supportedTransports.includes(t)
 | |
| 112 | -      );
 | |
| 113 | -      // Convert meek client path to absolute path if necessary
 | |
| 114 | -      const meekWorkDir = lazy.TorLauncherUtil.getTorFile(
 | |
| 115 | -        "pt-startup-dir",
 | |
| 116 | -        false
 | |
| 117 | -      );
 | |
| 118 | -      if (lazy.TorLauncherUtil.isPathRelative(proxy.pathToBinary)) {
 | |
| 119 | -        const meekPath = meekWorkDir.clone();
 | |
| 120 | -        meekPath.appendRelativePath(proxy.pathToBinary);
 | |
| 121 | -        proxy.pathToBinary = meekPath.path;
 | |
| 122 | -      }
 | |
| 123 | - | |
| 124 | -      // Setup env and start meek process
 | |
| 125 | -      const ptStateDir = lazy.TorLauncherUtil.getTorFile("tordatadir", false);
 | |
| 126 | -      ptStateDir.append("pt_state"); // Match what tor uses.
 | |
| 127 | - | |
| 128 | -      const envAdditions = {
 | |
| 129 | -        TOR_PT_MANAGED_TRANSPORT_VER: "1",
 | |
| 130 | -        TOR_PT_STATE_LOCATION: ptStateDir.path,
 | |
| 131 | -        TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
 | |
| 132 | -        TOR_PT_CLIENT_TRANSPORTS: meekTransport,
 | |
| 133 | -      };
 | |
| 134 | -      if (TorSettings.proxy.enabled) {
 | |
| 135 | -        envAdditions.TOR_PT_PROXY = TorSettings.proxy.uri;
 | |
| 136 | -      }
 | |
| 137 | - | |
| 138 | -      const opts = {
 | |
| 139 | -        command: proxy.pathToBinary,
 | |
| 140 | -        arguments: proxy.options.split(/s+/),
 | |
| 141 | -        workdir: meekWorkDir.path,
 | |
| 142 | -        environmentAppend: true,
 | |
| 143 | -        environment: envAdditions,
 | |
| 144 | -        stderr: "pipe",
 | |
| 145 | -      };
 | |
| 146 | - | |
| 147 | -      // Launch meek client
 | |
| 148 | -      this.#meekClientProcess = await lazy.Subprocess.call(opts);
 | |
| 149 | - | |
| 150 | -      // Callback chain for reading stderr
 | |
| 151 | -      const stderrLogger = async () => {
 | |
| 152 | -        while (this.#meekClientProcess) {
 | |
| 153 | -          const errString = await this.#meekClientProcess.stderr.readString();
 | |
| 154 | -          if (errString) {
 | |
| 155 | -            console.log(`MeekTransport: stderr => ${errString}`);
 | |
| 156 | -          }
 | |
| 157 | -        }
 | |
| 158 | -      };
 | |
| 159 | -      stderrLogger();
 | |
| 160 | - | |
| 161 | -      // Read pt's stdout until terminal (CMETHODS DONE) is reached
 | |
| 162 | -      // returns array of lines for parsing
 | |
| 163 | -      const getInitLines = async (stdout = "") => {
 | |
| 164 | -        stdout += await this.#meekClientProcess.stdout.readString();
 | |
| 165 | - | |
| 166 | -        // look for the final message
 | |
| 167 | -        const CMETHODS_DONE = "CMETHODS DONE";
 | |
| 168 | -        let endIndex = stdout.lastIndexOf(CMETHODS_DONE);
 | |
| 169 | -        if (endIndex != -1) {
 | |
| 170 | -          endIndex += CMETHODS_DONE.length;
 | |
| 171 | -          return stdout.substring(0, endIndex).split("\n");
 | |
| 172 | -        }
 | |
| 173 | -        return getInitLines(stdout);
 | |
| 174 | -      };
 | |
| 175 | - | |
| 176 | -      // read our lines from pt's stdout
 | |
| 177 | -      const meekInitLines = await getInitLines();
 | |
| 178 | -      // tokenize our pt lines
 | |
| 179 | -      const meekInitTokens = meekInitLines.map(line => {
 | |
| 180 | -        const tokens = line.split(" ");
 | |
| 181 | -        return {
 | |
| 182 | -          keyword: tokens[0],
 | |
| 183 | -          args: tokens.slice(1),
 | |
| 184 | -        };
 | |
| 185 | -      });
 | |
| 186 | - | |
| 187 | -      // parse our pt tokens
 | |
| 188 | -      for (const { keyword, args } of meekInitTokens) {
 | |
| 189 | -        const argsJoined = args.join(" ");
 | |
| 190 | -        let keywordError = false;
 | |
| 191 | -        switch (keyword) {
 | |
| 192 | -          case "VERSION": {
 | |
| 193 | -            if (args.length != 1 || args[0] !== "1") {
 | |
| 194 | -              keywordError = true;
 | |
| 195 | -            }
 | |
| 196 | -            break;
 | |
| 197 | -          }
 | |
| 198 | -          case "PROXY": {
 | |
| 199 | -            if (args.length != 1 || args[0] !== "DONE") {
 | |
| 200 | -              keywordError = true;
 | |
| 201 | -            }
 | |
| 202 | -            break;
 | |
| 203 | -          }
 | |
| 204 | -          case "CMETHOD": {
 | |
| 205 | -            if (args.length != 3) {
 | |
| 206 | -              keywordError = true;
 | |
| 207 | -              break;
 | |
| 208 | -            }
 | |
| 209 | -            const transport = args[0];
 | |
| 210 | -            const proxyType = args[1];
 | |
| 211 | -            const addrPortString = args[2];
 | |
| 212 | -            const addrPort = addrPortString.split(":");
 | |
| 213 | - | |
| 214 | -            if (transport !== meekTransport) {
 | |
| 215 | -              throw new Error(
 | |
| 216 | -                `MeekTransport: Expected ${meekTransport} but found ${transport}`
 | |
| 217 | -              );
 | |
| 218 | -            }
 | |
| 219 | -            if (!["socks4", "socks4a", "socks5"].includes(proxyType)) {
 | |
| 220 | -              throw new Error(
 | |
| 221 | -                `MeekTransport: Invalid proxy type => ${proxyType}`
 | |
| 222 | -              );
 | |
| 223 | -            }
 | |
| 224 | -            if (addrPort.length != 2) {
 | |
| 225 | -              throw new Error(
 | |
| 226 | -                `MeekTransport: Invalid proxy address => ${addrPortString}`
 | |
| 227 | -              );
 | |
| 228 | -            }
 | |
| 229 | -            const addr = addrPort[0];
 | |
| 230 | -            const port = parseInt(addrPort[1]);
 | |
| 231 | -            if (port < 1 || port > 65535) {
 | |
| 232 | -              throw new Error(`MeekTransport: Invalid proxy port => ${port}`);
 | |
| 233 | -            }
 | |
| 234 | - | |
| 235 | -            // convert proxy type to strings used by protocol-proxy-servce
 | |
| 236 | -            this.proxyType = proxyType === "socks5" ? "socks" : "socks4";
 | |
| 237 | -            this.proxyAddress = addr;
 | |
| 238 | -            this.proxyPort = port;
 | |
| 239 | - | |
| 240 | -            break;
 | |
| 241 | -          }
 | |
| 242 | -          // terminal
 | |
| 243 | -          case "CMETHODS": {
 | |
| 244 | -            if (args.length != 1 || args[0] !== "DONE") {
 | |
| 245 | -              keywordError = true;
 | |
| 246 | -            }
 | |
| 247 | -            break;
 | |
| 248 | -          }
 | |
| 249 | -          // errors (all fall through):
 | |
| 250 | -          case "VERSION-ERROR":
 | |
| 251 | -          case "ENV-ERROR":
 | |
| 252 | -          case "PROXY-ERROR":
 | |
| 253 | -          case "CMETHOD-ERROR":
 | |
| 254 | -            throw new Error(`MeekTransport: ${keyword} => '${argsJoined}'`);
 | |
| 255 | -        }
 | |
| 256 | -        if (keywordError) {
 | |
| 257 | -          throw new Error(
 | |
| 258 | -            `MeekTransport: Invalid ${keyword} keyword args => '${argsJoined}'`
 | |
| 259 | -          );
 | |
| 260 | -        }
 | |
| 261 | -      }
 | |
| 262 | - | |
| 263 | -      // register callback to cleanup on process exit
 | |
| 264 | -      this.#meekClientProcess.wait().then(exitObj => {
 | |
| 265 | -        this.#meekClientProcess = null;
 | |
| 266 | -        this.uninit();
 | |
| 267 | -      });
 | |
| 268 | -      [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
 | |
| 269 | -        this.proxyType
 | |
| 270 | -      );
 | |
| 271 | -      this.#inited = true;
 | |
| 272 | -    } catch (ex) {
 | |
| 273 | -      if (this.#meekClientProcess) {
 | |
| 274 | -        this.#meekClientProcess.kill();
 | |
| 275 | -        this.#meekClientProcess = null;
 | |
| 276 | -      }
 | |
| 277 | -      throw ex;
 | |
| 278 | -    }
 | |
| 279 | -  }
 | |
| 280 | - | |
| 281 | -  async uninit() {
 | |
| 282 | -    this.#inited = false;
 | |
| 283 | - | |
| 284 | -    await this.#meekClientProcess?.kill();
 | |
| 285 | -    this.#meekClientProcess = null;
 | |
| 286 | -    this.proxyType = null;
 | |
| 287 | -    this.proxyAddress = null;
 | |
| 288 | -    this.proxyPort = 0;
 | |
| 289 | -    this.proxyUsername = null;
 | |
| 290 | -    this.proxyPassword = null;
 | |
| 291 | -  }
 | |
| 292 | -}
 | |
| 293 | - | |
| 294 | -class MeekTransportAndroid {
 | |
| 295 | -  // These members are used by consumers to setup the proxy to do requests over
 | |
| 296 | -  // meek. They are passed to newProxyInfoWithAuth.
 | |
| 297 | -  proxyType = null;
 | |
| 298 | -  proxyAddress = null;
 | |
| 299 | -  proxyPort = 0;
 | |
| 300 | -  proxyUsername = null;
 | |
| 301 | -  proxyPassword = null;
 | |
| 302 | - | |
| 303 | -  #id = 0;
 | |
| 304 | - | |
| 305 | -  async init() {
 | |
| 306 | -    // ensure we haven't already init'd
 | |
| 307 | -    if (this.#id) {
 | |
| 308 | -      throw new Error("MeekTransport: Already initialized");
 | |
| 309 | -    }
 | |
| 310 | -    const details = await lazy.EventDispatcher.instance.sendRequestForResult({
 | |
| 311 | -      type: "GeckoView:Tor:StartMeek",
 | |
| 312 | -    });
 | |
| 313 | -    this.#id = details.id;
 | |
| 314 | -    this.proxyType = "socks";
 | |
| 315 | -    this.proxyAddress = details.address;
 | |
| 316 | -    this.proxyPort = details.port;
 | |
| 317 | -    [this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
 | |
| 318 | -      this.proxyType
 | |
| 319 | -    );
 | |
| 320 | -  }
 | |
| 321 | - | |
| 322 | -  async uninit() {
 | |
| 323 | -    lazy.EventDispatcher.instance.sendRequest({
 | |
| 324 | -      type: "GeckoView:Tor:StopMeek",
 | |
| 325 | -      id: this.#id,
 | |
| 326 | -    });
 | |
| 327 | -    this.#id = 0;
 | |
| 328 | -    this.proxyType = null;
 | |
| 329 | -    this.proxyAddress = null;
 | |
| 330 | -    this.proxyPort = 0;
 | |
| 331 | -    this.proxyUsername = null;
 | |
| 332 | -    this.proxyPassword = null;
 | |
| 333 | -  }
 | |
| 334 | -}
 | |
| 335 | - | |
| 336 | -//
 | |
| 337 | -// Callback object with a cached promise for the returned Moat data
 | |
| 338 | -//
 | |
| 339 | -class MoatResponseListener {
 | |
| 340 | -  #response = "";
 | |
| 341 | -  #responsePromise;
 | |
| 342 | -  #resolve;
 | |
| 343 | -  #reject;
 | |
| 344 | -  constructor() {
 | |
| 345 | -    this.#response = "";
 | |
| 346 | -    // we need this promise here because await nsIHttpChannel::asyncOpen does
 | |
| 347 | -    // not return only once the request is complete, it seems to return
 | |
| 348 | -    // after it begins, so we have to get the result from this listener object.
 | |
| 349 | -    // This promise is only resolved once onStopRequest is called
 | |
| 350 | -    this.#responsePromise = new Promise((resolve, reject) => {
 | |
| 351 | -      this.#resolve = resolve;
 | |
| 352 | -      this.#reject = reject;
 | |
| 353 | -    });
 | |
| 354 | -  }
 | |
| 355 | - | |
| 356 | -  // callers wait on this for final response
 | |
| 357 | -  response() {
 | |
| 358 | -    return this.#responsePromise;
 | |
| 359 | -  }
 | |
| 360 | - | |
| 361 | -  // noop
 | |
| 362 | -  onStartRequest(request) {}
 | |
| 363 | - | |
| 364 | -  // resolve or reject our Promise
 | |
| 365 | -  onStopRequest(request, status) {
 | |
| 366 | -    try {
 | |
| 367 | -      if (!Components.isSuccessCode(status)) {
 | |
| 368 | -        const errorMessage =
 | |
| 369 | -          lazy.TorLauncherUtil.getLocalizedStringForError(status);
 | |
| 370 | -        this.#reject(new Error(errorMessage));
 | |
| 371 | -      }
 | |
| 372 | -      if (request.responseStatus != 200) {
 | |
| 373 | -        this.#reject(new Error(request.responseStatusText));
 | |
| 374 | -      }
 | |
| 375 | -    } catch (err) {
 | |
| 376 | -      this.#reject(err);
 | |
| 377 | -    }
 | |
| 378 | -    this.#resolve(this.#response);
 | |
| 379 | -  }
 | |
| 380 | - | |
| 381 | -  // read response data
 | |
| 382 | -  onDataAvailable(request, stream, offset, length) {
 | |
| 383 | -    const scriptableStream = Cc[
 | |
| 384 | -      "@mozilla.org/scriptableinputstream;1"
 | |
| 385 | -    ].createInstance(Ci.nsIScriptableInputStream);
 | |
| 386 | -    scriptableStream.init(stream);
 | |
| 387 | -    this.#response += scriptableStream.read(length);
 | |
| 388 | -  }
 | |
| 389 | -}
 | |
| 390 | - | |
| 23 | +/**
 | |
| 24 | + * A special response listener that collects the received headers.
 | |
| 25 | + */
 | |
| 391 | 26 |  class InternetTestResponseListener {
 | 
| 392 | 27 |    #promise;
 | 
| 393 | 28 |    #resolve;
 | 
| ... | ... | @@ -436,129 +71,45 @@ class InternetTestResponseListener { | 
| 436 | 71 |    }
 | 
| 437 | 72 |  }
 | 
| 438 | 73 | |
| 439 | -// constructs the json objects and sends the request over moat
 | |
| 74 | +/**
 | |
| 75 | + * Constructs JSON objects and sends requests over Moat.
 | |
| 76 | + * The documentation about the JSON schemas to use are available at
 | |
| 77 | + * https://gitlab.torproject.org/tpo/anti-censorship/rdsys/-/blob/main/doc/moat.md.
 | |
| 78 | + */
 | |
| 440 | 79 |  export class MoatRPC {
 | 
| 441 | -  #inited = false;
 | |
| 442 | -  #meekTransport = null;
 | |
| 443 | - | |
| 444 | -  get inited() {
 | |
| 445 | -    return this.#inited;
 | |
| 446 | -  }
 | |
| 80 | +  #requestBuilder = null;
 | |
| 447 | 81 | |
| 448 | 82 |    async init() {
 | 
| 449 | -    if (this.#inited) {
 | |
| 450 | -      throw new Error("MoatRPC: Already initialized");
 | |
| 83 | +    if (this.#requestBuilder !== null) {
 | |
| 84 | +      return;
 | |
| 451 | 85 |      }
 | 
| 452 | 86 | |
| 453 | -    const meekTransport =
 | |
| 454 | -      Services.appinfo.OS === "Android"
 | |
| 455 | -        ? new MeekTransportAndroid()
 | |
| 456 | -        : new MeekTransport();
 | |
| 457 | -    await meekTransport.init();
 | |
| 458 | -    this.#meekTransport = meekTransport;
 | |
| 459 | -    this.#inited = true;
 | |
| 87 | +    const reflector = Services.prefs.getStringPref(
 | |
| 88 | +      TorLauncherPrefs.bridgedb_reflector
 | |
| 89 | +    );
 | |
| 90 | +    const front = Services.prefs.getStringPref(TorLauncherPrefs.bridgedb_front);
 | |
| 91 | +    const builder = new lazy.DomainFrontRequestBuilder();
 | |
| 92 | +    await builder.init(reflector, front);
 | |
| 93 | +    this.#requestBuilder = builder;
 | |
| 460 | 94 |    }
 | 
| 461 | 95 | |
| 462 | 96 |    async uninit() {
 | 
| 463 | -    await this.#meekTransport?.uninit();
 | |
| 464 | -    this.#meekTransport = null;
 | |
| 465 | -    this.#inited = false;
 | |
| 466 | -  }
 | |
| 467 | - | |
| 468 | -  #makeHttpHandler(uriString) {
 | |
| 469 | -    if (!this.#inited) {
 | |
| 470 | -      throw new Error("MoatRPC: Not initialized");
 | |
| 471 | -    }
 | |
| 472 | - | |
| 473 | -    const { proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword } =
 | |
| 474 | -      this.#meekTransport;
 | |
| 475 | - | |
| 476 | -    const proxyPS = Cc[
 | |
| 477 | -      "@mozilla.org/network/protocol-proxy-service;1"
 | |
| 478 | -    ].getService(Ci.nsIProtocolProxyService);
 | |
| 479 | -    const flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
 | |
| 480 | -    const noTimeout = 0xffffffff; // UINT32_MAX
 | |
| 481 | -    const proxyInfo = proxyPS.newProxyInfoWithAuth(
 | |
| 482 | -      proxyType,
 | |
| 483 | -      proxyAddress,
 | |
| 484 | -      proxyPort,
 | |
| 485 | -      proxyUsername,
 | |
| 486 | -      proxyPassword,
 | |
| 487 | -      undefined,
 | |
| 488 | -      undefined,
 | |
| 489 | -      flags,
 | |
| 490 | -      noTimeout,
 | |
| 491 | -      undefined
 | |
| 492 | -    );
 | |
| 493 | - | |
| 494 | -    const uri = Services.io.newURI(uriString);
 | |
| 495 | -    // There does not seem to be a way to directly create an nsILoadInfo from
 | |
| 496 | -    // _javascript_, so we create a throw away non-proxied channel to get one.
 | |
| 497 | -    const secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
 | |
| 498 | -    const loadInfo = Services.io.newChannelFromURI(
 | |
| 499 | -      uri,
 | |
| 500 | -      undefined,
 | |
| 501 | -      Services.scriptSecurityManager.getSystemPrincipal(),
 | |
| 502 | -      undefined,
 | |
| 503 | -      secFlags,
 | |
| 504 | -      Ci.nsIContentPolicy.TYPE_OTHER
 | |
| 505 | -    ).loadInfo;
 | |
| 506 | - | |
| 507 | -    const httpHandler = Services.io
 | |
| 508 | -      .getProtocolHandler("http")
 | |
| 509 | -      .QueryInterface(Ci.nsIHttpProtocolHandler);
 | |
| 510 | -    const ch = httpHandler
 | |
| 511 | -      .newProxiedChannel(uri, proxyInfo, 0, undefined, loadInfo)
 | |
| 512 | -      .QueryInterface(Ci.nsIHttpChannel);
 | |
| 513 | - | |
| 514 | -    // remove all headers except for 'Host"
 | |
| 515 | -    const headers = [];
 | |
| 516 | -    ch.visitRequestHeaders({
 | |
| 517 | -      visitHeader: (key, val) => {
 | |
| 518 | -        if (key !== "Host") {
 | |
| 519 | -          headers.push(key);
 | |
| 520 | -        }
 | |
| 521 | -      },
 | |
| 522 | -    });
 | |
| 523 | -    headers.forEach(key => ch.setRequestHeader(key, "", false));
 | |
| 524 | - | |
| 525 | -    return ch;
 | |
| 97 | +    await this.#requestBuilder?.uninit();
 | |
| 98 | +    this.#requestBuilder = null;
 | |
| 526 | 99 |    }
 | 
| 527 | 100 | |
| 528 | 101 |    async #makeRequest(procedure, args) {
 | 
| 529 | 102 |      const procedureURIString = `${Services.prefs.getStringPref(
 | 
| 530 | 103 |        TorLauncherPrefs.moat_service
 | 
| 531 | 104 |      )}/${procedure}`;
 | 
| 532 | -    const ch = this.#makeHttpHandler(procedureURIString);
 | |
| 533 | - | |
| 534 | -    // Arrange for the POST data to be sent.
 | |
| 535 | -    const argsJson = JSON.stringify(args);
 | |
| 536 | - | |
| 537 | -    const inStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
 | |
| 538 | -      Ci.nsIStringInputStream
 | |
| 539 | -    );
 | |
| 540 | -    inStream.setData(argsJson, argsJson.length);
 | |
| 541 | -    const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
 | |
| 542 | -    const contentType = "application/vnd.api+json";
 | |
| 543 | -    upChannel.setUploadStream(inStream, contentType, argsJson.length);
 | |
| 544 | -    ch.requestMethod = "POST";
 | |
| 545 | - | |
| 546 | -    // Make request
 | |
| 547 | -    const listener = new MoatResponseListener();
 | |
| 548 | -    await ch.asyncOpen(listener, ch);
 | |
| 549 | - | |
| 550 | -    // wait for response
 | |
| 551 | -    const responseJSON = await listener.response();
 | |
| 552 | - | |
| 553 | -    // parse that JSON
 | |
| 554 | -    return JSON.parse(responseJSON);
 | |
| 105 | +    return this.#requestBuilder.buildPostRequest(procedureURIString, args);
 | |
| 555 | 106 |    }
 | 
| 556 | 107 | |
| 557 | 108 |    async testInternetConnection() {
 | 
| 558 | 109 |      const uri = `${Services.prefs.getStringPref(
 | 
| 559 | 110 |        TorLauncherPrefs.moat_service
 | 
| 560 | 111 |      )}/circumvention/countries`;
 | 
| 561 | -    const ch = this.#makeHttpHandler(uri);
 | |
| 112 | +    const ch = this.#requestBuilder.buildHttpHandler(uri);
 | |
| 562 | 113 |      ch.requestMethod = "HEAD";
 | 
| 563 | 114 | |
| 564 | 115 |      const listener = new InternetTestResponseListener();
 | 
| ... | ... | @@ -566,10 +117,6 @@ export class MoatRPC { | 
| 566 | 117 |      return listener.status;
 | 
| 567 | 118 |    }
 | 
| 568 | 119 | |
| 569 | -  //
 | |
| 570 | -  // Moat APIs
 | |
| 571 | -  //
 | |
| 572 | - | |
| 573 | 120 |    // Receive a CAPTCHA challenge, takes the following parameters:
 | 
| 574 | 121 |    // - transports: array of transport strings available to us eg: ["obfs4", "meek"]
 | 
| 575 | 122 |    //
 | 
| ... | ... | @@ -166,6 +166,7 @@ EXTRA_JS_MODULES += [ | 
| 166 | 166 |      "DateTimePickerPanel.sys.mjs",
 | 
| 167 | 167 |      "DeferredTask.sys.mjs",
 | 
| 168 | 168 |      "Deprecated.sys.mjs",
 | 
| 169 | +    "DomainFrontedRequests.sys.mjs",
 | |
| 169 | 170 |      "DragDropFilter.sys.mjs",
 | 
| 170 | 171 |      "E10SUtils.sys.mjs",
 | 
| 171 | 172 |      "EventEmitter.sys.mjs",
 |