[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 2 commits: squash! Bug 40933: Add tor-launcher functionality



Title: GitLab

Pier Angelo Vendrame pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser

Commits:

  • 57b25177
    by Pier Angelo Vendrame at 2023-08-04T20:02:03+02:00
    squash! Bug 40933: Add tor-launcher functionality
    
    Bug 41926: Reimplement the control port
    
  • 9722ca26
    by Pier Angelo Vendrame at 2023-08-04T20:02:04+02:00
    fixup! Bug 10760: Integrate TorButton to TorBrowser core
    
    Removed torbutton.js, tor-control-port.js and utils.js.
    

13 changed files:

Changes:

  • browser/base/content/browser.xhtml
    ... ... @@ -130,17 +130,11 @@
    130 130
       Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this);
    
    131 131
       Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
    
    132 132
       Services.scriptloader.loadSubScript("chrome://browser/content/languageNotification.js", this);
    
    133
    -  Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this);
    
    134 133
     
    
    135 134
       window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
    
    136 135
       window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
    
    137 136
       window.onclose = WindowIsClosing;
    
    138 137
     
    
    139
    -  //onLoad Handler
    
    140
    -  try {
    
    141
    -    window.addEventListener("load", torbutton_init);
    
    142
    -  } catch (e) {}
    
    143
    -
    
    144 138
       window.addEventListener("MozBeforeInitialXULLayout",
    
    145 139
         gBrowserInit.onBeforeInitialXULLayout.bind(gBrowserInit), { once: true });
    
    146 140
     
    

  • toolkit/components/tor-launcher/TorControlPort.sys.mjs
    1
    +import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
    
    2
    +
    
    3
    +/**
    
    4
    + * @callback MessageCallback A callback to receive messages from the control
    
    5
    + * port.
    
    6
    + * @param {string} message The message to handle
    
    7
    + */
    
    8
    +/**
    
    9
    + * @callback RemoveCallback A function used to remove a previously registered
    
    10
    + * callback.
    
    11
    + */
    
    12
    +
    
    13
    +class CallbackDispatcher {
    
    14
    +  #callbackPairs = [];
    
    15
    +
    
    16
    +  /**
    
    17
    +   * Register a callback to handle a certain type of responses.
    
    18
    +   *
    
    19
    +   * @param {RegExp} regex The regex that tells which messages the callback
    
    20
    +   * wants to handle.
    
    21
    +   * @param {MessageCallback} callback The function to call
    
    22
    +   * @returns {RemoveCallback} A function to remove the just added callback
    
    23
    +   */
    
    24
    +  addCallback(regex, callback) {
    
    25
    +    this.#callbackPairs.push([regex, callback]);
    
    26
    +  }
    
    27
    +
    
    28
    +  /**
    
    29
    +   * Push a certain message to all the callbacks whose regex matches it.
    
    30
    +   *
    
    31
    +   * @param {string} message The message to push to the callbacks
    
    32
    +   */
    
    33
    +  pushMessage(message) {
    
    34
    +    for (const [regex, callback] of this.#callbackPairs) {
    
    35
    +      if (message.match(regex)) {
    
    36
    +        callback(message);
    
    37
    +      }
    
    38
    +    }
    
    39
    +  }
    
    40
    +}
    
    41
    +
    
    42
    +/**
    
    43
    + * A wrapper around XPCOM sockets and buffers to handle streams in a standard
    
    44
    + * async JS fashion.
    
    45
    + * This class can handle both Unix sockets and TCP sockets.
    
    46
    + */
    
    47
    +class AsyncSocket {
    
    48
    +  /**
    
    49
    +   * The output stream used for write operations.
    
    50
    +   *
    
    51
    +   * @type {nsIAsyncOutputStream}
    
    52
    +   */
    
    53
    +  #outputStream;
    
    54
    +  /**
    
    55
    +   * The output stream can only have one registered callback at a time, so
    
    56
    +   * multiple writes need to be queued up (see nsIAsyncOutputStream.idl).
    
    57
    +   * Every item is associated with a promise we returned in write, and it will
    
    58
    +   * resolve it or reject it when called by the output stream.
    
    59
    +   *
    
    60
    +   * @type {nsIOutputStreamCallback[]}
    
    61
    +   */
    
    62
    +  #outputQueue = [];
    
    63
    +  /**
    
    64
    +   * The input stream.
    
    65
    +   *
    
    66
    +   * @type {nsIAsyncInputStream}
    
    67
    +   */
    
    68
    +  #inputStream;
    
    69
    +  /**
    
    70
    +   * An input stream adapter that makes reading from scripts easier.
    
    71
    +   *
    
    72
    +   * @type {nsIScriptableInputStream}
    
    73
    +   */
    
    74
    +  #scriptableInputStream;
    
    75
    +  /**
    
    76
    +   * The queue of callbacks to be used when we receive data.
    
    77
    +   * Every item is associated with a promise we returned in read, and it will
    
    78
    +   * resolve it or reject it when called by the input stream.
    
    79
    +   *
    
    80
    +   * @type {nsIInputStreamCallback[]}
    
    81
    +   */
    
    82
    +  #inputQueue = [];
    
    83
    +
    
    84
    +  /**
    
    85
    +   * Connect to a Unix socket. Not available on Windows.
    
    86
    +   *
    
    87
    +   * @param {nsIFile} ipcFile The path to the Unix socket to connect to.
    
    88
    +   */
    
    89
    +  static fromIpcFile(ipcFile) {
    
    90
    +    const sts = Cc[
    
    91
    +      "@mozilla.org/network/socket-transport-service;1"
    
    92
    +    ].getService(Ci.nsISocketTransportService);
    
    93
    +    const socket = new AsyncSocket();
    
    94
    +    const transport = sts.createUnixDomainTransport(ipcFile);
    
    95
    +    socket.#createStreams(transport);
    
    96
    +    return socket;
    
    97
    +  }
    
    98
    +
    
    99
    +  /**
    
    100
    +   * Connect to a TCP socket.
    
    101
    +   *
    
    102
    +   * @param {string} host The hostname to connect the TCP socket to.
    
    103
    +   * @param {number} port The port to connect the TCP socket to.
    
    104
    +   */
    
    105
    +  static fromSocketAddress(host, port) {
    
    106
    +    const sts = Cc[
    
    107
    +      "@mozilla.org/network/socket-transport-service;1"
    
    108
    +    ].getService(Ci.nsISocketTransportService);
    
    109
    +    const socket = new AsyncSocket();
    
    110
    +    const transport = sts.createTransport([], host, port, null, null);
    
    111
    +    socket.#createStreams(transport);
    
    112
    +    return socket;
    
    113
    +  }
    
    114
    +
    
    115
    +  #createStreams(socketTransport) {
    
    116
    +    const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
    
    117
    +    this.#outputStream = socketTransport
    
    118
    +      .openOutputStream(OPEN_UNBUFFERED, 1, 1)
    
    119
    +      .QueryInterface(Ci.nsIAsyncOutputStream);
    
    120
    +
    
    121
    +    this.#inputStream = socketTransport
    
    122
    +      .openInputStream(OPEN_UNBUFFERED, 1, 1)
    
    123
    +      .QueryInterface(Ci.nsIAsyncInputStream);
    
    124
    +    this.#scriptableInputStream = Cc[
    
    125
    +      "@mozilla.org/scriptableinputstream;1"
    
    126
    +    ].createInstance(Ci.nsIScriptableInputStream);
    
    127
    +    this.#scriptableInputStream.init(this.#inputStream);
    
    128
    +  }
    
    129
    +
    
    130
    +  /**
    
    131
    +   * Asynchronously write string to underlying socket.
    
    132
    +   *
    
    133
    +   * When write is called, we create a new promise and queue it on the output
    
    134
    +   * queue. If it is the only element in the queue, we ask the output stream to
    
    135
    +   * run it immediately.
    
    136
    +   * Otherwise, the previous item of the queue will run it after it finishes.
    
    137
    +   *
    
    138
    +   * @param {string} str The string to write to the socket. The underlying
    
    139
    +   * implementation shoulw convert JS strings (UTF-16) into UTF-8 strings.
    
    140
    +   * See also write nsIOutputStream (the first argument is a string, not a
    
    141
    +   * wstring).
    
    142
    +   * @returns {Promise<number>} The number of written bytes
    
    143
    +   */
    
    144
    +  async write(str) {
    
    145
    +    return new Promise((resolve, reject) => {
    
    146
    +      // asyncWait next write request
    
    147
    +      const tryAsyncWait = () => {
    
    148
    +        if (this.#outputQueue.length) {
    
    149
    +          this.#outputStream.asyncWait(
    
    150
    +            this.#outputQueue.at(0), // next request
    
    151
    +            0,
    
    152
    +            0,
    
    153
    +            Services.tm.currentThread
    
    154
    +          );
    
    155
    +        }
    
    156
    +      };
    
    157
    +
    
    158
    +      // Implement an nsIOutputStreamCallback: write the string once possible,
    
    159
    +      // and then start running the following queue item, if any.
    
    160
    +      this.#outputQueue.push({
    
    161
    +        onOutputStreamReady: () => {
    
    162
    +          try {
    
    163
    +            const bytesWritten = this.#outputStream.write(str, str.length);
    
    164
    +
    
    165
    +            // remove this callback object from queue as it is now completed
    
    166
    +            this.#outputQueue.shift();
    
    167
    +
    
    168
    +            // request next wait if there is one
    
    169
    +            tryAsyncWait();
    
    170
    +
    
    171
    +            // finally resolve promise
    
    172
    +            resolve(bytesWritten);
    
    173
    +          } catch (err) {
    
    174
    +            // reject promise on error
    
    175
    +            reject(err);
    
    176
    +          }
    
    177
    +        },
    
    178
    +      });
    
    179
    +
    
    180
    +      // Length 1 imples that there is no in-flight asyncWait, so we may
    
    181
    +      // immediately follow through on this write.
    
    182
    +      if (this.#outputQueue.length === 1) {
    
    183
    +        tryAsyncWait();
    
    184
    +      }
    
    185
    +    });
    
    186
    +  }
    
    187
    +
    
    188
    +  /**
    
    189
    +   * Asynchronously read string from underlying socket and return it.
    
    190
    +   *
    
    191
    +   * When read is called, we create a new promise and queue it on the input
    
    192
    +   * queue. If it is the only element in the queue, we ask the input stream to
    
    193
    +   * run it immediately.
    
    194
    +   * Otherwise, the previous item of the queue will run it after it finishes.
    
    195
    +   *
    
    196
    +   * This function is expected to throw when the underlying socket has been
    
    197
    +   * closed.
    
    198
    +   *
    
    199
    +   * @returns {Promise<string>} The read string
    
    200
    +   */
    
    201
    +  async read() {
    
    202
    +    return new Promise((resolve, reject) => {
    
    203
    +      const tryAsyncWait = () => {
    
    204
    +        if (this.#inputQueue.length) {
    
    205
    +          this.#inputStream.asyncWait(
    
    206
    +            this.#inputQueue.at(0), // next input request
    
    207
    +            0,
    
    208
    +            0,
    
    209
    +            Services.tm.currentThread
    
    210
    +          );
    
    211
    +        }
    
    212
    +      };
    
    213
    +
    
    214
    +      this.#inputQueue.push({
    
    215
    +        onInputStreamReady: stream => {
    
    216
    +          try {
    
    217
    +            if (!this.#scriptableInputStream.available()) {
    
    218
    +              // This means EOF, but not closed yet. However, arriving at EOF
    
    219
    +              // should be an error condition for us, since we are in a socket,
    
    220
    +              // and EOF should mean peer disconnected.
    
    221
    +              // If the stream has been closed, this function itself should
    
    222
    +              // throw.
    
    223
    +              reject(
    
    224
    +                new Error("onInputStreamReady called without available bytes.")
    
    225
    +              );
    
    226
    +              return;
    
    227
    +            }
    
    228
    +
    
    229
    +            // Read our string from input stream.
    
    230
    +            const str = this.#scriptableInputStream.read(
    
    231
    +              this.#scriptableInputStream.available()
    
    232
    +            );
    
    233
    +
    
    234
    +            // Remove this callback object from queue now that we have read.
    
    235
    +            this.#inputQueue.shift();
    
    236
    +
    
    237
    +            // Start waiting for incoming data again if the reading queue is not
    
    238
    +            // empty.
    
    239
    +            tryAsyncWait();
    
    240
    +
    
    241
    +            // Finally resolve the promise.
    
    242
    +            resolve(str);
    
    243
    +          } catch (err) {
    
    244
    +            // E.g., we received a NS_BASE_STREAM_CLOSED because the socket was
    
    245
    +            // closed.
    
    246
    +            reject(err);
    
    247
    +          }
    
    248
    +        },
    
    249
    +      });
    
    250
    +
    
    251
    +      // Length 1 imples that there is no in-flight asyncWait, so we may
    
    252
    +      // immediately follow through on this read.
    
    253
    +      if (this.#inputQueue.length === 1) {
    
    254
    +        tryAsyncWait();
    
    255
    +      }
    
    256
    +    });
    
    257
    +  }
    
    258
    +
    
    259
    +  /**
    
    260
    +   * Close the streams.
    
    261
    +   */
    
    262
    +  close() {
    
    263
    +    this.#outputStream.close();
    
    264
    +    this.#inputStream.close();
    
    265
    +  }
    
    266
    +}
    
    267
    +
    
    268
    +/**
    
    269
    + * @typedef Command
    
    270
    + * @property {string} commandString The string to send over the control port
    
    271
    + * @property {Function} resolve The function to resolve the promise with the
    
    272
    + * response we got on the control port
    
    273
    + * @property {Function} reject The function to reject the promise associated to
    
    274
    + * the command
    
    275
    + */
    
    276
    +
    
    277
    +class TorError extends Error {
    
    278
    +  constructor(command, reply) {
    
    279
    +    super(`${command} -> ${reply}`);
    
    280
    +    this.name = "TorError";
    
    281
    +    const info = reply.match(/(?<code>\d{3})(?:\s(?<message>.+))?/);
    
    282
    +    this.torStatusCode = info.groups.code;
    
    283
    +    if (info.groups.message) {
    
    284
    +      this.torMessage = info.groups.message;
    
    285
    +    }
    
    286
    +  }
    
    287
    +}
    
    288
    +
    
    289
    +class ControlSocket {
    
    290
    +  /**
    
    291
    +   * The socket to write to the control port.
    
    292
    +   *
    
    293
    +   * @type {AsyncSocket}
    
    294
    +   */
    
    295
    +  #socket;
    
    296
    +
    
    297
    +  /**
    
    298
    +   * The dispatcher used for the data we receive over the control port.
    
    299
    +   *
    
    300
    +   * @type {CallbackDispatcher}
    
    301
    +   */
    
    302
    +  #mainDispatcher = new CallbackDispatcher();
    
    303
    +  /**
    
    304
    +   * A secondary dispatcher used only to dispatch aynchronous events.
    
    305
    +   *
    
    306
    +   * @type {CallbackDispatcher}
    
    307
    +   */
    
    308
    +  #notificationDispatcher = new CallbackDispatcher();
    
    309
    +
    
    310
    +  /**
    
    311
    +   * Data we received on a read but that was not a complete line (missing a
    
    312
    +   * final CRLF). We will prepend it to the next read.
    
    313
    +   *
    
    314
    +   * @type {string}
    
    315
    +   */
    
    316
    +  #pendingData = "";
    
    317
    +  /**
    
    318
    +   * The lines we received and are still queued for being evaluated.
    
    319
    +   *
    
    320
    +   * @type {string[]}
    
    321
    +   */
    
    322
    +  #pendingLines = [];
    
    323
    +  /**
    
    324
    +   * The commands that need to be run or receive a response.
    
    325
    +   *
    
    326
    +   * @type {Command[]}
    
    327
    +   */
    
    328
    +  #commandQueue = [];
    
    329
    +
    
    330
    +  constructor(asyncSocket) {
    
    331
    +    this.#socket = asyncSocket;
    
    332
    +
    
    333
    +    // #mainDispatcher pushes only async notifications (650) to
    
    334
    +    // #notificationDispatcher
    
    335
    +    this.#mainDispatcher.addCallback(
    
    336
    +      /^650/,
    
    337
    +      this.#handleNotification.bind(this)
    
    338
    +    );
    
    339
    +    // callback for handling responses and errors
    
    340
    +    this.#mainDispatcher.addCallback(
    
    341
    +      /^[245]\d\d/,
    
    342
    +      this.#handleCommandReply.bind(this)
    
    343
    +    );
    
    344
    +
    
    345
    +    this.#startMessagePump();
    
    346
    +  }
    
    347
    +
    
    348
    +  /**
    
    349
    +   * Return the next line in the queue. If there is not any, block until one is
    
    350
    +   * read (or until a communication error happens, including the underlying
    
    351
    +   * socket being closed while it was still waiting for data).
    
    352
    +   * Any letfovers will be prepended to the next read.
    
    353
    +   *
    
    354
    +   * @returns {Promise<string>} A line read over the socket
    
    355
    +   */
    
    356
    +  async #readLine() {
    
    357
    +    // Keep reading from socket until we have at least a full line to return.
    
    358
    +    while (!this.#pendingLines.length) {
    
    359
    +      if (!this.#socket) {
    
    360
    +        throw new Error(
    
    361
    +          "Read interrupted because the control socket is not available anymore"
    
    362
    +        );
    
    363
    +      }
    
    364
    +      // Read data from our socket and split on newline tokens.
    
    365
    +      // This might still throw when the socket has been closed.
    
    366
    +      this.#pendingData += await this.#socket.read();
    
    367
    +      const lines = this.#pendingData.split("\r\n");
    
    368
    +      // The last line will either be empty string, or a partial read of a
    
    369
    +      // response/event so save it off for the next socket read.
    
    370
    +      this.#pendingData = lines.pop();
    
    371
    +      // Copy remaining full lines to our pendingLines list.
    
    372
    +      this.#pendingLines = this.#pendingLines.concat(lines);
    
    373
    +    }
    
    374
    +    return this.#pendingLines.shift();
    
    375
    +  }
    
    376
    +
    
    377
    +  /**
    
    378
    +   * Blocks until an entire message is ready and returns it.
    
    379
    +   * This function does a rudimentary parsing of the data only to handle
    
    380
    +   * multi-line responses.
    
    381
    +   *
    
    382
    +   * @returns {Promise<string>} The read message (without the final CRLF)
    
    383
    +   */
    
    384
    +  async #readMessage() {
    
    385
    +    // whether we are searching for the end of a multi-line values
    
    386
    +    // See control-spec section 3.9
    
    387
    +    let handlingMultlineValue = false;
    
    388
    +    let endOfMessageFound = false;
    
    389
    +    const message = [];
    
    390
    +
    
    391
    +    do {
    
    392
    +      const line = await this.#readLine();
    
    393
    +      message.push(line);
    
    394
    +
    
    395
    +      if (handlingMultlineValue) {
    
    396
    +        // look for end of multiline
    
    397
    +        if (line === ".") {
    
    398
    +          handlingMultlineValue = false;
    
    399
    +        }
    
    400
    +      } else {
    
    401
    +        // 'Multiline values' are possible. We avoid interrupting one by
    
    402
    +        // detecting it and waiting for a terminating "." on its own line.
    
    403
    +        // (See control-spec section 3.9 and
    
    404
    +        // https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/16990#note_2625464).
    
    405
    +        // Ensure this is the first line of a new message
    
    406
    +        // eslint-disable-next-line no-lonely-if
    
    407
    +        if (message.length === 1 && line.match(/^\d\d\d\+.+?=$/)) {
    
    408
    +          handlingMultlineValue = true;
    
    409
    +        }
    
    410
    +        // look for end of message (notice the space character at end of the
    
    411
    +        // regex!)
    
    412
    +        else if (line.match(/^\d\d\d /)) {
    
    413
    +          if (message.length === 1) {
    
    414
    +            endOfMessageFound = true;
    
    415
    +          } else {
    
    416
    +            const firstReplyCode = message[0].substring(0, 3);
    
    417
    +            const lastReplyCode = line.substring(0, 3);
    
    418
    +            endOfMessageFound = firstReplyCode === lastReplyCode;
    
    419
    +          }
    
    420
    +        }
    
    421
    +      }
    
    422
    +    } while (!endOfMessageFound);
    
    423
    +
    
    424
    +    // join our lines back together to form one message
    
    425
    +    return message.join("\r\n");
    
    426
    +  }
    
    427
    +
    
    428
    +  /**
    
    429
    +   * Read messages on the socket and routed them to a dispatcher until the
    
    430
    +   * socket is open or some error happens (including the underlying socket being
    
    431
    +   * closed).
    
    432
    +   */
    
    433
    +  async #startMessagePump() {
    
    434
    +    try {
    
    435
    +      // This while is inside the try block because it is very likely that it
    
    436
    +      // will be broken by a NS_BASE_STREAM_CLOSED exception, rather than by its
    
    437
    +      // condition becoming false.
    
    438
    +      while (this.#socket) {
    
    439
    +        const message = await this.#readMessage();
    
    440
    +        // log("controlPort >> " + message);
    
    441
    +        this.#mainDispatcher.pushMessage(message);
    
    442
    +      }
    
    443
    +    } catch (err) {
    
    444
    +      try {
    
    445
    +        this.#close(err);
    
    446
    +      } catch (ec) {
    
    447
    +        console.error(
    
    448
    +          "Caught another error while closing the control socket.",
    
    449
    +          ec
    
    450
    +        );
    
    451
    +      }
    
    452
    +    }
    
    453
    +  }
    
    454
    +
    
    455
    +  /**
    
    456
    +   * Start running the first available command in the queue.
    
    457
    +   * To be called when the previous one has finished running.
    
    458
    +   * This makes sure to avoid conflicts when using the control port.
    
    459
    +   */
    
    460
    +  #writeNextCommand() {
    
    461
    +    const cmd = this.#commandQueue[0];
    
    462
    +    // log("controlPort << " + cmd.commandString);
    
    463
    +    this.#socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
    
    464
    +  }
    
    465
    +
    
    466
    +  /**
    
    467
    +   * Send a command over the control port.
    
    468
    +   * This function returns only when it receives a complete message over the
    
    469
    +   * control port. This class does some rudimentary parsing to check wheter it
    
    470
    +   * needs to handle multi-line messages.
    
    471
    +   *
    
    472
    +   * @param {string} commandString
    
    473
    +   * @returns {Promise<string>} The message sent by the control port. It will
    
    474
    +   * always start with 2xx. In case of other codes the function will throw,
    
    475
    +   * instead. This means that the return value will never be an empty string
    
    476
    +   * (even though it will not include the final CRLF).
    
    477
    +   */
    
    478
    +  async sendCommand(commandString) {
    
    479
    +    if (!this.#socket) {
    
    480
    +      throw new Error("ControlSocket not open");
    
    481
    +    }
    
    482
    +
    
    483
    +    // this promise is resolved either in #handleCommandReply, or in
    
    484
    +    // #startMessagePump (on stream error)
    
    485
    +    return new Promise((resolve, reject) => {
    
    486
    +      const command = {
    
    487
    +        commandString,
    
    488
    +        resolve,
    
    489
    +        reject,
    
    490
    +      };
    
    491
    +      this.#commandQueue.push(command);
    
    492
    +      if (this.#commandQueue.length === 1) {
    
    493
    +        this.#writeNextCommand();
    
    494
    +      }
    
    495
    +    });
    
    496
    +  }
    
    497
    +
    
    498
    +  /**
    
    499
    +   * Handles a message starting with 2xx, 4xx, or 5xx.
    
    500
    +   * This function should be used only as a callback for the main dispatcher.
    
    501
    +   *
    
    502
    +   * @param {string} message The message to handle
    
    503
    +   */
    
    504
    +  #handleCommandReply(message) {
    
    505
    +    const cmd = this.#commandQueue.shift();
    
    506
    +    if (message[0] === "2") {
    
    507
    +      cmd.resolve(message);
    
    508
    +    } else if (message.match(/^[45]/)) {
    
    509
    +      cmd.reject(new TorError(cmd.commandString, message));
    
    510
    +    } else {
    
    511
    +      // This should never happen, as the dispatcher should filter the messages
    
    512
    +      // already.
    
    513
    +      cmd.reject(
    
    514
    +        new Error(`Received unexpected message:\n----\n${message}\n----`)
    
    515
    +      );
    
    516
    +    }
    
    517
    +
    
    518
    +    // send next command if one is available
    
    519
    +    if (this.#commandQueue.length) {
    
    520
    +      this.#writeNextCommand();
    
    521
    +    }
    
    522
    +  }
    
    523
    +
    
    524
    +  /**
    
    525
    +   * Re-route an event message to the notification dispatcher.
    
    526
    +   * This function should be used only as a callback for the main dispatcher.
    
    527
    +   *
    
    528
    +   * @param {string} message The message received on the control port
    
    529
    +   */
    
    530
    +  #handleNotification(message) {
    
    531
    +    try {
    
    532
    +      this.#notificationDispatcher.pushMessage(message);
    
    533
    +    } catch (e) {
    
    534
    +      console.error("An event watcher threw", e);
    
    535
    +    }
    
    536
    +  }
    
    537
    +
    
    538
    +  /**
    
    539
    +   * Reject all the commands that are still in queue and close the control
    
    540
    +   * socket.
    
    541
    +   *
    
    542
    +   * @param {object?} reason An error object used to pass a more specific
    
    543
    +   * rejection reason to the commands that are still queued.
    
    544
    +   */
    
    545
    +  #close(reason) {
    
    546
    +    const error = new Error(
    
    547
    +      "The control socket has been closed" +
    
    548
    +        (reason ? `: ${reason.message}` : "")
    
    549
    +    );
    
    550
    +    const commands = this.#commandQueue;
    
    551
    +    this.#commandQueue = [];
    
    552
    +    for (const cmd of commands) {
    
    553
    +      cmd.reject(error);
    
    554
    +    }
    
    555
    +    try {
    
    556
    +      this.#socket?.close();
    
    557
    +    } finally {
    
    558
    +      this.#socket = null;
    
    559
    +    }
    
    560
    +  }
    
    561
    +
    
    562
    +  /**
    
    563
    +   * Closes the socket connected to the control port.
    
    564
    +   */
    
    565
    +  close() {
    
    566
    +    this.#close(null);
    
    567
    +  }
    
    568
    +
    
    569
    +  /**
    
    570
    +   * Register an event watcher.
    
    571
    +   *
    
    572
    +   * @param {RegExp} regex The regex to filter on messages to receive
    
    573
    +   * @param {MessageCallback} callback The callback for the messages
    
    574
    +   */
    
    575
    +  addNotificationCallback(regex, callback) {
    
    576
    +    this.#notificationDispatcher.addCallback(regex, callback);
    
    577
    +  }
    
    578
    +
    
    579
    +  /**
    
    580
    +   * Tells whether the underlying socket is still open.
    
    581
    +   */
    
    582
    +  get isOpen() {
    
    583
    +    return !!this.#socket;
    
    584
    +  }
    
    585
    +}
    
    586
    +
    
    587
    +// ## utils
    
    588
    +// A namespace for utility functions
    
    589
    +let utils = {};
    
    590
    +
    
    591
    +// __utils.identity(x)__.
    
    592
    +// Returns its argument unchanged.
    
    593
    +utils.identity = function (x) {
    
    594
    +  return x;
    
    595
    +};
    
    596
    +
    
    597
    +// __utils.capture(string, regex)__.
    
    598
    +// Takes a string and returns an array of capture items, where regex must have a single
    
    599
    +// capturing group and use the suffix /.../g to specify a global search.
    
    600
    +utils.capture = function (string, regex) {
    
    601
    +  let matches = [];
    
    602
    +  // Special trick to use string.replace for capturing multiple matches.
    
    603
    +  string.replace(regex, function (a, captured) {
    
    604
    +    matches.push(captured);
    
    605
    +  });
    
    606
    +  return matches;
    
    607
    +};
    
    608
    +
    
    609
    +// __utils.extractor(regex)__.
    
    610
    +// Returns a function that takes a string and returns an array of regex matches. The
    
    611
    +// regex must use the suffix /.../g to specify a global search.
    
    612
    +utils.extractor = function (regex) {
    
    613
    +  return function (text) {
    
    614
    +    return utils.capture(text, regex);
    
    615
    +  };
    
    616
    +};
    
    617
    +
    
    618
    +// __utils.splitLines(string)__.
    
    619
    +// Splits a string into an array of strings, each corresponding to a line.
    
    620
    +utils.splitLines = function (string) {
    
    621
    +  return string.split(/\r?\n/);
    
    622
    +};
    
    623
    +
    
    624
    +// __utils.splitAtSpaces(string)__.
    
    625
    +// Splits a string into chunks between spaces. Does not split at spaces
    
    626
    +// inside pairs of quotation marks.
    
    627
    +utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
    
    628
    +
    
    629
    +// __utils.splitAtFirst(string, regex)__.
    
    630
    +// Splits a string at the first instance of regex match. If no match is
    
    631
    +// found, returns the whole string.
    
    632
    +utils.splitAtFirst = function (string, regex) {
    
    633
    +  let match = string.match(regex);
    
    634
    +  return match
    
    635
    +    ? [
    
    636
    +        string.substring(0, match.index),
    
    637
    +        string.substring(match.index + match[0].length),
    
    638
    +      ]
    
    639
    +    : string;
    
    640
    +};
    
    641
    +
    
    642
    +// __utils.splitAtEquals(string)__.
    
    643
    +// Splits a string into chunks between equals. Does not split at equals
    
    644
    +// inside pairs of quotation marks.
    
    645
    +utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
    
    646
    +
    
    647
    +// __utils.mergeObjects(arrayOfObjects)__.
    
    648
    +// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
    
    649
    +// Pure function.
    
    650
    +utils.mergeObjects = function (arrayOfObjects) {
    
    651
    +  let result = {};
    
    652
    +  for (let obj of arrayOfObjects) {
    
    653
    +    for (let key in obj) {
    
    654
    +      result[key] = obj[key];
    
    655
    +    }
    
    656
    +  }
    
    657
    +  return result;
    
    658
    +};
    
    659
    +
    
    660
    +// __utils.listMapData(parameterString, listNames)__.
    
    661
    +// Takes a list of parameters separated by spaces, of which the first several are
    
    662
    +// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
    
    663
    +// to the unnamed parameters, and combine them in a map with the named parameters.
    
    664
    +// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
    
    665
    +//
    
    666
    +//     utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
    
    667
    +//                       ["streamID", "event", "circuitID", "IP"])
    
    668
    +//     // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
    
    669
    +//     //      "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
    
    670
    +utils.listMapData = function (parameterString, listNames) {
    
    671
    +  // Split out the space-delimited parameters.
    
    672
    +  let parameters = utils.splitAtSpaces(parameterString),
    
    673
    +    dataMap = {};
    
    674
    +  // Assign listNames to the first n = listNames.length parameters.
    
    675
    +  for (let i = 0; i < listNames.length; ++i) {
    
    676
    +    dataMap[listNames[i]] = parameters[i];
    
    677
    +  }
    
    678
    +  // Read key-value pairs and copy these to the dataMap.
    
    679
    +  for (let i = listNames.length; i < parameters.length; ++i) {
    
    680
    +    let [key, value] = utils.splitAtEquals(parameters[i]);
    
    681
    +    if (key && value) {
    
    682
    +      dataMap[key] = value;
    
    683
    +    }
    
    684
    +  }
    
    685
    +  return dataMap;
    
    686
    +};
    
    687
    +
    
    688
    +// ## info
    
    689
    +// A namespace for functions related to tor's GETINFO and GETCONF command.
    
    690
    +let info = {};
    
    691
    +
    
    692
    +// __info.keyValueStringsFromMessage(messageText)__.
    
    693
    +// Takes a message (text) response to GETINFO or GETCONF and provides
    
    694
    +// a series of key-value strings, which are either multiline (with a `250+` prefix):
    
    695
    +//
    
    696
    +//     250+config/defaults=
    
    697
    +//     AccountingMax "0 bytes"
    
    698
    +//     AllowDotExit "0"
    
    699
    +//     .
    
    700
    +//
    
    701
    +// or single-line (with a `250-` or `250 ` prefix):
    
    702
    +//
    
    703
    +//     250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
    
    704
    +info.keyValueStringsFromMessage = utils.extractor(
    
    705
    +  /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
    
    706
    +);
    
    707
    +
    
    708
    +// __info.applyPerLine(transformFunction)__.
    
    709
    +// Returns a function that splits text into lines,
    
    710
    +// and applies transformFunction to each line.
    
    711
    +info.applyPerLine = function (transformFunction) {
    
    712
    +  return function (text) {
    
    713
    +    return utils.splitLines(text.trim()).map(transformFunction);
    
    714
    +  };
    
    715
    +};
    
    716
    +
    
    717
    +// __info.routerStatusParser(valueString)__.
    
    718
    +// Parses a router status entry as, described in
    
    719
    +// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
    
    720
    +// (search for "router status entry")
    
    721
    +info.routerStatusParser = function (valueString) {
    
    722
    +  let lines = utils.splitLines(valueString),
    
    723
    +    objects = [];
    
    724
    +  for (let line of lines) {
    
    725
    +    // Drop first character and grab data following it.
    
    726
    +    let myData = line.substring(2),
    
    727
    +      // Accumulate more maps with data, depending on the first character in the line.
    
    728
    +      dataFun = {
    
    729
    +        r: data =>
    
    730
    +          utils.listMapData(data, [
    
    731
    +            "nickname",
    
    732
    +            "identity",
    
    733
    +            "digest",
    
    734
    +            "publicationDate",
    
    735
    +            "publicationTime",
    
    736
    +            "IP",
    
    737
    +            "ORPort",
    
    738
    +            "DirPort",
    
    739
    +          ]),
    
    740
    +        a: data => ({ IPv6: data }),
    
    741
    +        s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
    
    742
    +        v: data => ({ version: data }),
    
    743
    +        w: data => utils.listMapData(data, []),
    
    744
    +        p: data => ({ portList: data.split(",") }),
    
    745
    +      }[line.charAt(0)];
    
    746
    +    if (dataFun !== undefined) {
    
    747
    +      objects.push(dataFun(myData));
    
    748
    +    }
    
    749
    +  }
    
    750
    +  return utils.mergeObjects(objects);
    
    751
    +};
    
    752
    +
    
    753
    +// __info.circuitStatusParser(line)__.
    
    754
    +// Parse the output of a circuit status line.
    
    755
    +info.circuitStatusParser = function (line) {
    
    756
    +  let data = utils.listMapData(line, ["id", "status", "circuit"]),
    
    757
    +    circuit = data.circuit;
    
    758
    +  // Parse out the individual circuit IDs and names.
    
    759
    +  if (circuit) {
    
    760
    +    data.circuit = circuit.split(",").map(function (x) {
    
    761
    +      return x.split(/~|=/);
    
    762
    +    });
    
    763
    +  }
    
    764
    +  return data;
    
    765
    +};
    
    766
    +
    
    767
    +// __info.streamStatusParser(line)__.
    
    768
    +// Parse the output of a stream status line.
    
    769
    +info.streamStatusParser = function (text) {
    
    770
    +  return utils.listMapData(text, [
    
    771
    +    "StreamID",
    
    772
    +    "StreamStatus",
    
    773
    +    "CircuitID",
    
    774
    +    "Target",
    
    775
    +  ]);
    
    776
    +};
    
    777
    +
    
    778
    +// TODO: fix this parsing logic to handle bridgeLine correctly
    
    779
    +// fingerprint/id is an optional parameter
    
    780
    +// __info.bridgeParser(bridgeLine)__.
    
    781
    +// Takes a single line from a `getconf bridge` result and returns
    
    782
    +// a map containing the bridge's type, address, and ID.
    
    783
    +info.bridgeParser = function (bridgeLine) {
    
    784
    +  let result = {},
    
    785
    +    tokens = bridgeLine.split(/\s+/);
    
    786
    +  // First check if we have a "vanilla" bridge:
    
    787
    +  if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
    
    788
    +    result.type = "vanilla";
    
    789
    +    [result.address, result.ID] = tokens;
    
    790
    +    // Several bridge types have a similar format:
    
    791
    +  } else {
    
    792
    +    result.type = tokens[0];
    
    793
    +    if (
    
    794
    +      [
    
    795
    +        "flashproxy",
    
    796
    +        "fte",
    
    797
    +        "meek",
    
    798
    +        "meek_lite",
    
    799
    +        "obfs3",
    
    800
    +        "obfs4",
    
    801
    +        "scramblesuit",
    
    802
    +        "snowflake",
    
    803
    +      ].includes(result.type)
    
    804
    +    ) {
    
    805
    +      [result.address, result.ID] = tokens.slice(1);
    
    806
    +    }
    
    807
    +  }
    
    808
    +  return result.type ? result : null;
    
    809
    +};
    
    810
    +
    
    811
    +// __info.parsers__.
    
    812
    +// A map of GETINFO and GETCONF keys to parsing function, which convert
    
    813
    +// result strings to _javascript_ data.
    
    814
    +info.parsers = {
    
    815
    +  "ns/id/": info.routerStatusParser,
    
    816
    +  "ip-to-country/": utils.identity,
    
    817
    +  "circuit-status": info.applyPerLine(info.circuitStatusParser),
    
    818
    +  bridge: info.bridgeParser,
    
    819
    +  // Currently unused parsers:
    
    820
    +  //  "ns/name/" : info.routerStatusParser,
    
    821
    +  //  "stream-status" : info.applyPerLine(info.streamStatusParser),
    
    822
    +  //  "version" : utils.identity,
    
    823
    +  //  "config-file" : utils.identity,
    
    824
    +};
    
    825
    +
    
    826
    +// __info.getParser(key)__.
    
    827
    +// Takes a key and determines the parser function that should be used to
    
    828
    +// convert its corresponding valueString to _javascript_ data.
    
    829
    +info.getParser = function (key) {
    
    830
    +  return (
    
    831
    +    info.parsers[key] ||
    
    832
    +    info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
    
    833
    +  );
    
    834
    +};
    
    835
    +
    
    836
    +// __info.stringToValue(string)__.
    
    837
    +// Converts a key-value string as from GETINFO or GETCONF to a value.
    
    838
    +info.stringToValue = function (string) {
    
    839
    +  // key should look something like `250+circuit-status=` or `250-circuit-status=...`
    
    840
    +  // or `250 circuit-status=...`
    
    841
    +  let matchForKey = string.match(/^250[ +-](.+?)=/),
    
    842
    +    key = matchForKey ? matchForKey[1] : null;
    
    843
    +  if (key === null) {
    
    844
    +    return null;
    
    845
    +  }
    
    846
    +  // matchResult finds a single-line result for `250-` or `250 `,
    
    847
    +  // or a multi-line one for `250+`.
    
    848
    +  let matchResult =
    
    849
    +      string.match(/^250[ -].+?=(.*)$/) ||
    
    850
    +      string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
    
    851
    +    // Retrieve the captured group (the text of the value in the key-value pair)
    
    852
    +    valueString = matchResult ? matchResult[1] : null,
    
    853
    +    // Get the parser function for the key found.
    
    854
    +    parse = info.getParser(key.toLowerCase());
    
    855
    +  if (parse === undefined) {
    
    856
    +    throw new Error("No parser found for '" + key + "'");
    
    857
    +  }
    
    858
    +  // Return value produced by the parser.
    
    859
    +  return parse(valueString);
    
    860
    +};
    
    861
    +
    
    862
    +/**
    
    863
    + * @typedef {object} Bridge
    
    864
    + * @property {string} transport The transport of the bridge, or vanilla if not
    
    865
    + * specified.
    
    866
    + * @property {string} addr The IP address and port of the bridge
    
    867
    + * @property {string} id The fingerprint of the bridge
    
    868
    + * @property {string} args Optional arguments passed to the bridge
    
    869
    + */
    
    870
    +/**
    
    871
    + * @typedef {object} PTInfo The information about a pluggable transport
    
    872
    + * @property {string[]} transports An array with all the transports supported by
    
    873
    + * this configuration.
    
    874
    + * @property {string} type Either socks4, socks5 or exec
    
    875
    + * @property {string} [ip] The IP address of the proxy (only for socks4 and
    
    876
    + * socks5)
    
    877
    + * @property {integer} [port] The port of the proxy (only for socks4 and socks5)
    
    878
    + * @property {string} [pathToBinary] Path to the binary that is run (only for
    
    879
    + * exec)
    
    880
    + * @property {string} [options] Optional options passed to the binary (only for
    
    881
    + * exec)
    
    882
    + */
    
    883
    +/**
    
    884
    + * @typedef {object} OnionAuthKeyInfo
    
    885
    + * @property {string} address The address of the onion service
    
    886
    + * @property {string} typeAndKey Onion service key and type of key, as
    
    887
    + * `type:base64-private-key`
    
    888
    + * @property {string} Flags Additional flags, such as Permanent
    
    889
    + */
    
    890
    +/**
    
    891
    + * @callback EventFilterCallback
    
    892
    + * @param {any} data Either a raw string, or already parsed data
    
    893
    + * @returns {boolean}
    
    894
    + */
    
    895
    +/**
    
    896
    + * @callback EventCallback
    
    897
    + * @param {any} data Either a raw string, or already parsed data
    
    898
    + */
    
    899
    +
    
    900
    +class TorController {
    
    901
    +  /**
    
    902
    +   * The control socket
    
    903
    +   *
    
    904
    +   * @type {ControlSocket}
    
    905
    +   */
    
    906
    +  #socket;
    
    907
    +
    
    908
    +  /**
    
    909
    +   * A map of EVENT keys to parsing functions, which convert result strings to
    
    910
    +   * _javascript_ data.
    
    911
    +   */
    
    912
    +  #eventParsers = {
    
    913
    +    stream: info.streamStatusParser,
    
    914
    +    // Currently unused:
    
    915
    +    // "circ" : info.circuitStatusParser,
    
    916
    +  };
    
    917
    +
    
    918
    +  /**
    
    919
    +   * Builds a new TorController.
    
    920
    +   *
    
    921
    +   * @param {AsyncSocket} socket The socket to communicate to the control port
    
    922
    +   */
    
    923
    +  constructor(socket) {
    
    924
    +    this.#socket = new ControlSocket(socket);
    
    925
    +  }
    
    926
    +
    
    927
    +  /**
    
    928
    +   * Tells whether the underlying socket is open.
    
    929
    +   *
    
    930
    +   * @returns {boolean}
    
    931
    +   */
    
    932
    +  get isOpen() {
    
    933
    +    return this.#socket.isOpen;
    
    934
    +  }
    
    935
    +
    
    936
    +  /**
    
    937
    +   * Close the underlying socket.
    
    938
    +   */
    
    939
    +  close() {
    
    940
    +    this.#socket.close();
    
    941
    +  }
    
    942
    +
    
    943
    +  /**
    
    944
    +   * Send a command over the control port.
    
    945
    +   * TODO: Make this function private, and force the operations to go through
    
    946
    +   * specialized methods.
    
    947
    +   *
    
    948
    +   * @param {string} cmd The command to send
    
    949
    +   * @returns {Promise<string>} A 2xx response obtained from the control port.
    
    950
    +   * For other codes, this function will throw. The returned string will never
    
    951
    +   * be empty.
    
    952
    +   */
    
    953
    +  async sendCommand(cmd) {
    
    954
    +    return this.#socket.sendCommand(cmd);
    
    955
    +  }
    
    956
    +
    
    957
    +  /**
    
    958
    +   * Send a simple command whose response is expected to be simply a "250 OK".
    
    959
    +   * The function will not return a reply, but will throw if an unexpected one
    
    960
    +   * is received.
    
    961
    +   *
    
    962
    +   * @param {string} command The command to send
    
    963
    +   */
    
    964
    +  async #sendCommandSimple(command) {
    
    965
    +    const reply = await this.sendCommand(command);
    
    966
    +    if (!/^250 OK\s*$/i.test(reply)) {
    
    967
    +      throw new TorError(command, reply);
    
    968
    +    }
    
    969
    +  }
    
    970
    +
    
    971
    +  /**
    
    972
    +   * Authenticate to the tor daemon.
    
    973
    +   * Notice that a failure in the authentication makes the connection close.
    
    974
    +   *
    
    975
    +   * @param {string} password The password for the control port.
    
    976
    +   */
    
    977
    +  async authenticate(password) {
    
    978
    +    if (password) {
    
    979
    +      this.#expectString(password, "password");
    
    980
    +    }
    
    981
    +    await this.#sendCommandSimple(`authenticate ${password || ""}`);
    
    982
    +  }
    
    983
    +
    
    984
    +  /**
    
    985
    +   * Sends a GETINFO for a single key.
    
    986
    +   *
    
    987
    +   * @param {string} key The key to get value for
    
    988
    +   * @returns {any} The return value depends on the requested key
    
    989
    +   */
    
    990
    +  async getInfo(key) {
    
    991
    +    this.#expectString(key, "key");
    
    992
    +    const response = await this.sendCommand(`getinfo ${key}`);
    
    993
    +    return this.#getMultipleResponseValues(response)[0];
    
    994
    +  }
    
    995
    +
    
    996
    +  /**
    
    997
    +   * Sends a GETINFO for a single key.
    
    998
    +   * control-spec.txt says "one ReplyLine is sent for each requested value", so,
    
    999
    +   * we expect to receive only one line starting with `250-keyword=`, or one
    
    1000
    +   * line starting with `250+keyword=` (in which case we will match until a
    
    1001
    +   * period).
    
    1002
    +   * This function could be possibly extended to handle several keys at once,
    
    1003
    +   * but we currently do not need this functionality, so we preferred keeping
    
    1004
    +   * the function simpler.
    
    1005
    +   *
    
    1006
    +   * @param {string} key The key to get value for
    
    1007
    +   * @returns {Promise<string>} The string we received (only the value, without
    
    1008
    +   * the key). We do not do any additional parsing on it.
    
    1009
    +   */
    
    1010
    +  async #getInfo(key) {
    
    1011
    +    this.#expectString(key);
    
    1012
    +    const cmd = `GETINFO ${key}`;
    
    1013
    +    const reply = await this.sendCommand(cmd);
    
    1014
    +    const match =
    
    1015
    +      reply.match(/^250-([^=]+)=(.*)$/m) ||
    
    1016
    +      reply.match(/^250\+([^=]+)=([\s\S]*?)^\.\r?\n^250 OK\s*$/m);
    
    1017
    +    if (!match || match[1] !== key) {
    
    1018
    +      throw new TorError(cmd, reply);
    
    1019
    +    }
    
    1020
    +    return match[2];
    
    1021
    +  }
    
    1022
    +
    
    1023
    +  /**
    
    1024
    +   * Ask Tor its bootstrap phase.
    
    1025
    +   *
    
    1026
    +   * @returns {object} An object with the bootstrap information received from
    
    1027
    +   * Tor. Its keys might vary, depending on the input
    
    1028
    +   */
    
    1029
    +  async getBootstrapPhase() {
    
    1030
    +    return this.#parseBootstrapStatus(
    
    1031
    +      await this.#getInfo("status/bootstrap-phase")
    
    1032
    +    );
    
    1033
    +  }
    
    1034
    +
    
    1035
    +  /**
    
    1036
    +   * Get the IPv4 and optionally IPv6 addresses of an onion router.
    
    1037
    +   *
    
    1038
    +   * @param {NodeFingerprint} id The fingerprint of the node the caller is
    
    1039
    +   * interested in
    
    1040
    +   * @returns {string[]} The IP addresses (one IPv4 and optionally an IPv6)
    
    1041
    +   */
    
    1042
    +  async getNodeAddresses(id) {
    
    1043
    +    this.#expectString(id, "id");
    
    1044
    +    const reply = await this.#getInfo(`ns/id/${id}`);
    
    1045
    +    // See dir-spec.txt.
    
    1046
    +    // r nickname identity digest publication IP OrPort DirPort
    
    1047
    +    const rLine = reply.match(/^r\s+(.*)$/m);
    
    1048
    +    const v4 = rLine ? rLine[1].split(/\s+/) : [];
    
    1049
    +    // Tor should already reply with a 552 when a relay cannot be found.
    
    1050
    +    // Also, publication is a date with a space inside, so it is counted twice.
    
    1051
    +    if (!rLine || v4.length !== 8) {
    
    1052
    +      throw new Error(`Received an invalid node information: ${reply}`);
    
    1053
    +    }
    
    1054
    +    const addresses = [v4[5]];
    
    1055
    +    // a address:port
    
    1056
    +    // dir-spec.txt also states only the first one should be taken
    
    1057
    +    // TODO: The consumers do not care about the port or the square brackets
    
    1058
    +    // either. Remove them when integrating this function with the rest
    
    1059
    +    const v6 = reply.match(/^a\s+(\[[0-9a-fA-F:]+\]:[0-9]{1,5})$/m);
    
    1060
    +    if (v6) {
    
    1061
    +      addresses.push(v6[1]);
    
    1062
    +    }
    
    1063
    +    return addresses;
    
    1064
    +  }
    
    1065
    +
    
    1066
    +  /**
    
    1067
    +   * Maps IP addresses to 2-letter country codes, or ?? if unknown.
    
    1068
    +   *
    
    1069
    +   * @param {string} ip The IP address to look for
    
    1070
    +   * @returns {Promise<string>} A promise with the country code. If unknown, the
    
    1071
    +   * promise is resolved with "??". It is rejected only when the underlying
    
    1072
    +   * GETINFO command fails or if an exception is thrown
    
    1073
    +   */
    
    1074
    +  async getIPCountry(ip) {
    
    1075
    +    this.#expectString(ip, "ip");
    
    1076
    +    return this.#getInfo(`ip-to-country/${ip}`);
    
    1077
    +  }
    
    1078
    +
    
    1079
    +  /**
    
    1080
    +   * Ask tor which ports it is listening to for SOCKS connections.
    
    1081
    +   *
    
    1082
    +   * @returns {Promise<string[]>} An array of addresses. It might be empty
    
    1083
    +   * (e.g., when DisableNetwork is set)
    
    1084
    +   */
    
    1085
    +  async getSocksListeners() {
    
    1086
    +    const listeners = await this.#getInfo("net/listeners/socks");
    
    1087
    +    return Array.from(listeners.matchAll(/\s*("(?:[^"\\]|\\.)*"|\S+)\s*/g), m =>
    
    1088
    +      TorParsers.unescapeString(m[1])
    
    1089
    +    );
    
    1090
    +  }
    
    1091
    +
    
    1092
    +  // Configuration
    
    1093
    +
    
    1094
    +  /**
    
    1095
    +   * Sends a GETCONF for a single key.
    
    1096
    +   * GETCONF with a single argument returns results with one or more lines that
    
    1097
    +   * look like `250[- ]key=value`.
    
    1098
    +   * Any GETCONF lines that contain a single keyword only are currently dropped.
    
    1099
    +   * So we can use similar parsing to that for getInfo.
    
    1100
    +   *
    
    1101
    +   * @param {string} key The key to get value for
    
    1102
    +   * @returns {any} A parsed config value (it depends if a parser is known)
    
    1103
    +   */
    
    1104
    +  async getConf(key) {
    
    1105
    +    this.#expectString(key, "key");
    
    1106
    +    return this.#getMultipleResponseValues(
    
    1107
    +      await this.sendCommand(`getconf ${key}`)
    
    1108
    +    );
    
    1109
    +  }
    
    1110
    +
    
    1111
    +  /**
    
    1112
    +   * Sends a GETCONF for a single key.
    
    1113
    +   * The function could be easily generalized to get multiple keys at once, but
    
    1114
    +   * we do not need this functionality, at the moment.
    
    1115
    +   *
    
    1116
    +   * @param {string} key The keys to get info for
    
    1117
    +   * @returns {Promise<string[]>} The values obtained from the control port.
    
    1118
    +   * The key is removed, and the values unescaped, but they are not parsed.
    
    1119
    +   * The array might contain an empty string, which means that the default value
    
    1120
    +   * is used.
    
    1121
    +   */
    
    1122
    +  async #getConf(key) {
    
    1123
    +    this.#expectString(key, "key");
    
    1124
    +    // GETCONF expects a `keyword`, which should be only alpha characters,
    
    1125
    +    // according to the definition in control-port.txt. But as a matter of fact,
    
    1126
    +    // several configuration keys include numbers (e.g., Socks4Proxy). So, we
    
    1127
    +    // accept also numbers in this regular _expression_. One of the reason to
    
    1128
    +    // sanitize the input is that we then use it to create a regular _expression_.
    
    1129
    +    // Sadly, _javascript_ does not provide a function to escape/quote a string
    
    1130
    +    // for inclusion in a regex. Should we remove this limitation, we should
    
    1131
    +    // also implement a regex sanitizer, or switch to another pattern, like
    
    1132
    +    // `([^=])` and then filter on the keyword.
    
    1133
    +    if (!/^[A-Za-z0-9]+$/.test(key)) {
    
    1134
    +      throw new Error("The key can be composed only of letters and numbers.");
    
    1135
    +    }
    
    1136
    +    const cmd = `GETCONF ${key}`;
    
    1137
    +    const reply = await this.sendCommand(cmd);
    
    1138
    +    // From control-spec.txt: a 'default' value semantically different from an
    
    1139
    +    // empty string will not have an equal sign, just `250 $key`.
    
    1140
    +    const defaultRe = new RegExp(`^250[-\\s]${key}$`, "gim");
    
    1141
    +    if (reply.match(defaultRe)) {
    
    1142
    +      return [];
    
    1143
    +    }
    
    1144
    +    const re = new RegExp(`^250[-\\s]${key}=(.*)$`, "gim");
    
    1145
    +    const values = Array.from(reply.matchAll(re), m =>
    
    1146
    +      TorParsers.unescapeString(m[1])
    
    1147
    +    );
    
    1148
    +    if (!values.length) {
    
    1149
    +      throw new TorError(cmd, reply);
    
    1150
    +    }
    
    1151
    +    return values;
    
    1152
    +  }
    
    1153
    +
    
    1154
    +  /**
    
    1155
    +   * Get the bridges Tor has been configured with.
    
    1156
    +   *
    
    1157
    +   * @returns {Bridge[]} The configured bridges
    
    1158
    +   */
    
    1159
    +  async getBridges() {
    
    1160
    +    return (await this.#getConf("BRIDGE")).map(TorParsers.parseBridgeLine);
    
    1161
    +  }
    
    1162
    +
    
    1163
    +  /**
    
    1164
    +   * Get the configured pluggable transports.
    
    1165
    +   *
    
    1166
    +   * @returns {PTInfo[]} An array with the info of all the configured pluggable
    
    1167
    +   * transports.
    
    1168
    +   */
    
    1169
    +  async getPluggableTransports() {
    
    1170
    +    return (await this.#getConf("ClientTransportPlugin")).map(ptLine => {
    
    1171
    +      // man 1 tor: ClientTransportPlugin transport socks4|socks5 IP:PORT
    
    1172
    +      const socksLine = ptLine.match(
    
    1173
    +        /(\S+)\s+(socks[45])\s+([\d.]{7,15}|\[[\da-fA-F:]+\]):(\d{1,5})/i
    
    1174
    +      );
    
    1175
    +      // man 1 tor: transport exec path-to-binary [options]
    
    1176
    +      const execLine = ptLine.match(
    
    1177
    +        /(\S+)\s+(exec)\s+("(?:[^"\\]|\\.)*"|\S+)\s*(.*)/i
    
    1178
    +      );
    
    1179
    +      if (socksLine) {
    
    1180
    +        return {
    
    1181
    +          transports: socksLine[1].split(","),
    
    1182
    +          type: socksLine[2].toLowerCase(),
    
    1183
    +          ip: socksLine[3],
    
    1184
    +          port: parseInt(socksLine[4], 10),
    
    1185
    +        };
    
    1186
    +      } else if (execLine) {
    
    1187
    +        return {
    
    1188
    +          transports: execLine[1].split(","),
    
    1189
    +          type: execLine[2].toLowerCase(),
    
    1190
    +          pathToBinary: TorParsers.unescapeString(execLine[3]),
    
    1191
    +          options: execLine[4],
    
    1192
    +        };
    
    1193
    +      }
    
    1194
    +      throw new Error(
    
    1195
    +        `Received an invalid ClientTransportPlugin line: ${ptLine}`
    
    1196
    +      );
    
    1197
    +    });
    
    1198
    +  }
    
    1199
    +
    
    1200
    +  /**
    
    1201
    +   * Send multiple configuration values to tor.
    
    1202
    +   *
    
    1203
    +   * @param {object} values The values to set
    
    1204
    +   */
    
    1205
    +  async setConf(values) {
    
    1206
    +    const args = Object.entries(values)
    
    1207
    +      .flatMap(([key, value]) => {
    
    1208
    +        if (value === undefined || value === null) {
    
    1209
    +          return [key];
    
    1210
    +        }
    
    1211
    +        if (Array.isArray(value)) {
    
    1212
    +          return value.length
    
    1213
    +            ? value.map(v => `${key}=${TorParsers.escapeString(v)}`)
    
    1214
    +            : key;
    
    1215
    +        } else if (typeof value === "string" || value instanceof String) {
    
    1216
    +          return `${key}=${TorParsers.escapeString(value)}`;
    
    1217
    +        } else if (typeof value === "boolean") {
    
    1218
    +          return `${key}=${value ? "1" : "0"}`;
    
    1219
    +        } else if (typeof value === "number") {
    
    1220
    +          return `${key}=${value}`;
    
    1221
    +        }
    
    1222
    +        throw new Error(`Unsupported type ${typeof value} (key ${key})`);
    
    1223
    +      })
    
    1224
    +      .join(" ");
    
    1225
    +    return this.#sendCommandSimple(`SETCONF ${args}`);
    
    1226
    +  }
    
    1227
    +
    
    1228
    +  /**
    
    1229
    +   * Enable or disable the network.
    
    1230
    +   * Notice: switching from network disabled to network enabled will trigger a
    
    1231
    +   * bootstrap on C tor! (Or stop the current one).
    
    1232
    +   *
    
    1233
    +   * @param {boolean} enabled Tell whether the network should be enabled
    
    1234
    +   */
    
    1235
    +  async setNetworkEnabled(enabled) {
    
    1236
    +    return this.setConf({ DisableNetwork: !enabled });
    
    1237
    +  }
    
    1238
    +
    
    1239
    +  /**
    
    1240
    +   * Ask Tor to write out its config options into its torrc.
    
    1241
    +   */
    
    1242
    +  async flushSettings() {
    
    1243
    +    return this.#sendCommandSimple("SAVECONF");
    
    1244
    +  }
    
    1245
    +
    
    1246
    +  // Onion service authentication
    
    1247
    +
    
    1248
    +  /**
    
    1249
    +   * Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private
    
    1250
    +   * keys.
    
    1251
    +   *
    
    1252
    +   * @returns {OnionAuthKeyInfo[]}
    
    1253
    +   */
    
    1254
    +  async onionAuthViewKeys() {
    
    1255
    +    const cmd = "onion_client_auth_view";
    
    1256
    +    const message = await this.sendCommand(cmd);
    
    1257
    +    // Either `250-CLIENT`, or `250 OK` if no keys are available.
    
    1258
    +    if (!message.startsWith("250")) {
    
    1259
    +      throw new TorError(cmd, message);
    
    1260
    +    }
    
    1261
    +    const re =
    
    1262
    +      /^250-CLIENT\s+(?<HSAddress>[A-Za-z2-7]+)\s+(?<KeyType>[^:]+):(?<PrivateKeyBlob>\S+)(?:\s(?<other>.+))?$/gim;
    
    1263
    +    return Array.from(message.matchAll(re), match => {
    
    1264
    +      // TODO: Change the consumer and make the fields more consistent with what
    
    1265
    +      // we get (e.g., separate key and type, and use a boolen for permanent).
    
    1266
    +      const info = {
    
    1267
    +        hsAddress: match.groups.HSAddress,
    
    1268
    +        typeAndKey: `${match.groups.KeyType}:${match.groups.PrivateKeyBlob}`,
    
    1269
    +      };
    
    1270
    +      const maybeFlags = match.groups.other?.match(/Flags=(\S+)/);
    
    1271
    +      if (maybeFlags) {
    
    1272
    +        info.Flags = maybeFlags[1];
    
    1273
    +      }
    
    1274
    +      return info;
    
    1275
    +    });
    
    1276
    +  }
    
    1277
    +
    
    1278
    +  /**
    
    1279
    +   * Sends an ONION_CLIENT_AUTH_ADD command to add a private key to the Tor
    
    1280
    +   * configuration.
    
    1281
    +   *
    
    1282
    +   * @param {string} address The address of the onion service
    
    1283
    +   * @param {string} b64PrivateKey The private key of the service, in base64
    
    1284
    +   * @param {boolean} isPermanent Tell whether the key should be saved forever
    
    1285
    +   */
    
    1286
    +  async onionAuthAdd(address, b64PrivateKey, isPermanent) {
    
    1287
    +    this.#expectString(address, "address");
    
    1288
    +    this.#expectString(b64PrivateKey, "b64PrivateKey");
    
    1289
    +    const keyType = "x25519";
    
    1290
    +    let cmd = `onion_client_auth_add ${address} ${keyType}:${b64PrivateKey}`;
    
    1291
    +    if (isPermanent) {
    
    1292
    +      cmd += " Flags=Permanent";
    
    1293
    +    }
    
    1294
    +    const reply = await this.sendCommand(cmd);
    
    1295
    +    const status = reply.substring(0, 3);
    
    1296
    +    if (status !== "250" && status !== "251" && status !== "252") {
    
    1297
    +      throw new TorError(cmd, reply);
    
    1298
    +    }
    
    1299
    +  }
    
    1300
    +
    
    1301
    +  /**
    
    1302
    +   * Sends an ONION_CLIENT_AUTH_REMOVE command to remove a private key from the
    
    1303
    +   * Tor configuration.
    
    1304
    +   *
    
    1305
    +   * @param {string} address The address of the onion service
    
    1306
    +   */
    
    1307
    +  async onionAuthRemove(address) {
    
    1308
    +    this.#expectString(address, "address");
    
    1309
    +    const cmd = `onion_client_auth_remove ${address}`;
    
    1310
    +    const reply = await this.sendCommand(cmd);
    
    1311
    +    const status = reply.substring(0, 3);
    
    1312
    +    if (status !== "250" && status !== "251") {
    
    1313
    +      throw new TorError(cmd, reply);
    
    1314
    +    }
    
    1315
    +  }
    
    1316
    +
    
    1317
    +  // Daemon ownership
    
    1318
    +
    
    1319
    +  /**
    
    1320
    +   * Instructs Tor to shut down when this control connection is closed.
    
    1321
    +   * If multiple connection sends this request, Tor will shut dwon when any of
    
    1322
    +   * them is closed.
    
    1323
    +   */
    
    1324
    +  async takeOwnership() {
    
    1325
    +    return this.#sendCommandSimple("TAKEOWNERSHIP");
    
    1326
    +  }
    
    1327
    +
    
    1328
    +  /**
    
    1329
    +   * The __OwningControllerProcess argument can be used to make Tor periodically
    
    1330
    +   * check if a certain PID is still present, or terminate itself otherwise.
    
    1331
    +   * When switching to the ownership tied to the control port, this mechanism
    
    1332
    +   * should be stopped by calling this function.
    
    1333
    +   */
    
    1334
    +  async resetOwningControllerProcess() {
    
    1335
    +    return this.#sendCommandSimple("RESETCONF __OwningControllerProcess");
    
    1336
    +  }
    
    1337
    +
    
    1338
    +  // Signals
    
    1339
    +
    
    1340
    +  /**
    
    1341
    +   * Ask Tor to swtich to new circuits and clear the DNS cache.
    
    1342
    +   */
    
    1343
    +  async newnym() {
    
    1344
    +    return this.#sendCommandSimple("SIGNAL NEWNYM");
    
    1345
    +  }
    
    1346
    +
    
    1347
    +  // Events monitoring
    
    1348
    +
    
    1349
    +  /**
    
    1350
    +   * Enable receiving certain events.
    
    1351
    +   * As per control-spec.txt, any events turned on in previous calls but not
    
    1352
    +   * included in this one will be turned off.
    
    1353
    +   *
    
    1354
    +   * @param {string[]} types The events to enable. If empty, no events will be
    
    1355
    +   * watched.
    
    1356
    +   */
    
    1357
    +  setEvents(types) {
    
    1358
    +    if (!types.every(t => typeof t === "string" || t instanceof String)) {
    
    1359
    +      throw new Error("Event types must be strings");
    
    1360
    +    }
    
    1361
    +    return this.#sendCommandSimple("SETEVENTS " + types.join(" "));
    
    1362
    +  }
    
    1363
    +
    
    1364
    +  /**
    
    1365
    +   * Watches for a particular type of asynchronous event.
    
    1366
    +   * Notice: we only observe `"650" SP...` events, currently (no `650+...` or
    
    1367
    +   * `650-...` events).
    
    1368
    +   * Also, you need to enable the events in the control port with SETEVENTS,
    
    1369
    +   * first.
    
    1370
    +   *
    
    1371
    +   * @param {string} type The event type to catch
    
    1372
    +   * @param {EventFilterCallback?} filter An optional callback to filter
    
    1373
    +   * events for which the callback will be called. If null, all events will be
    
    1374
    +   * passed.
    
    1375
    +   * @param {EventCallback} callback The callback that will handle the event
    
    1376
    +   * @param {boolean} raw Tell whether to ignore the data parser, even if
    
    1377
    +   * supported
    
    1378
    +   */
    
    1379
    +  watchEvent(type, filter, callback, raw = false) {
    
    1380
    +    this.#expectString(type, "type");
    
    1381
    +    const start = `650 ${type}`;
    
    1382
    +    this.#socket.addNotificationCallback(new RegExp(`^${start}`), message => {
    
    1383
    +      // Remove also the initial text
    
    1384
    +      const dataText = message.substring(start.length + 1);
    
    1385
    +      const parser = this.#eventParsers[type.toLowerCase()];
    
    1386
    +      const data = dataText && parser ? parser(dataText) : null;
    
    1387
    +      // FIXME: This is the original code, but we risk of not filtering on the
    
    1388
    +      // data, if we ask for raw data (which we always do at the moment, but we
    
    1389
    +      // do not use a filter either...)
    
    1390
    +      if (filter === null || filter(data)) {
    
    1391
    +        callback(data && !raw ? data : message);
    
    1392
    +      }
    
    1393
    +    });
    
    1394
    +  }
    
    1395
    +
    
    1396
    +  // Other helpers
    
    1397
    +
    
    1398
    +  /**
    
    1399
    +   * Parse a bootstrap status line.
    
    1400
    +   *
    
    1401
    +   * @param {string} line The line to parse, without the command/notification
    
    1402
    +   * prefix
    
    1403
    +   * @returns {object} An object with the bootstrap information received from
    
    1404
    +   * Tor. Its keys might vary, depending on the input
    
    1405
    +   */
    
    1406
    +  #parseBootstrapStatus(line) {
    
    1407
    +    const match = line.match(/^(NOTICE|WARN) BOOTSTRAP\s*(.*)/);
    
    1408
    +    if (!match) {
    
    1409
    +      throw Error(
    
    1410
    +        `Received an invalid response for the bootstrap phase: ${line}`
    
    1411
    +      );
    
    1412
    +    }
    
    1413
    +    const status = {
    
    1414
    +      TYPE: match[1],
    
    1415
    +      ...this.#getKeyValues(match[2]),
    
    1416
    +    };
    
    1417
    +    if (status.PROGRESS !== undefined) {
    
    1418
    +      status.PROGRESS = parseInt(status.PROGRESS, 10);
    
    1419
    +    }
    
    1420
    +    if (status.COUNT !== undefined) {
    
    1421
    +      status.COUNT = parseInt(status.COUNT, 10);
    
    1422
    +    }
    
    1423
    +    return status;
    
    1424
    +  }
    
    1425
    +
    
    1426
    +  /**
    
    1427
    +   * Throw an exception when value is not a string.
    
    1428
    +   *
    
    1429
    +   * @param {any} value The value to check
    
    1430
    +   * @param {string} name The name of the `value` argument
    
    1431
    +   */
    
    1432
    +  #expectString(value, name) {
    
    1433
    +    if (typeof value !== "string" && !(value instanceof String)) {
    
    1434
    +      throw new Error(`The ${name} argument is expected to be a string.`);
    
    1435
    +    }
    
    1436
    +  }
    
    1437
    +
    
    1438
    +  /**
    
    1439
    +   * Return an object with all the matches that are in the form `key="value"` or
    
    1440
    +   * `key=value`. The values will be unescaped, but no additional parsing will
    
    1441
    +   * be done (e.g., numbers will be returned as strings).
    
    1442
    +   * If keys are repeated, only the last one will be taken.
    
    1443
    +   *
    
    1444
    +   * @param {string} str The string to match tokens in
    
    1445
    +   * @returns {object} An object with all the various tokens. If none is found,
    
    1446
    +   * an empty object is returned.
    
    1447
    +   */
    
    1448
    +  #getKeyValues(str) {
    
    1449
    +    return Object.fromEntries(
    
    1450
    +      Array.from(
    
    1451
    +        str.matchAll(/\s*([^=]+)=("(?:[^"\\]|\\.)*"|\S+)\s*/g) || [],
    
    1452
    +        pair => [pair[1], TorParsers.unescapeString(pair[2])]
    
    1453
    +      )
    
    1454
    +    );
    
    1455
    +  }
    
    1456
    +
    
    1457
    +  /**
    
    1458
    +   * Process multiple responses to a GETINFO or GETCONF request.
    
    1459
    +   *
    
    1460
    +   * @param {string} message The message to process
    
    1461
    +   * @returns {object[]} The keys depend on the message
    
    1462
    +   */
    
    1463
    +  #getMultipleResponseValues(message) {
    
    1464
    +    return info
    
    1465
    +      .keyValueStringsFromMessage(message)
    
    1466
    +      .map(info.stringToValue)
    
    1467
    +      .filter(x => x);
    
    1468
    +  }
    
    1469
    +}
    
    1470
    +
    
    1471
    +const controlPortInfo = {};
    
    1472
    +
    
    1473
    +/**
    
    1474
    + * Sets Tor control port connection parameters to be used in future calls to
    
    1475
    + * the controller() function.
    
    1476
    + *
    
    1477
    + * Example:
    
    1478
    + *   configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
    
    1479
    + *
    
    1480
    + * @param {nsIFile?} ipcFile An optional file to use to communicate to the
    
    1481
    + * control port on Unix platforms
    
    1482
    + * @param {string?} host The hostname to connect to the control port. Mutually
    
    1483
    + * exclusive with ipcFile
    
    1484
    + * @param {integer?} port The port number of the control port. To be used only
    
    1485
    + * with host. The default is 9151.
    
    1486
    + * @param {string} password The password of the control port in clear text.
    
    1487
    + */
    
    1488
    +export function configureControlPortModule(ipcFile, host, port, password) {
    
    1489
    +  controlPortInfo.ipcFile = ipcFile;
    
    1490
    +  controlPortInfo.host = host;
    
    1491
    +  controlPortInfo.port = port || 9151;
    
    1492
    +  controlPortInfo.password = password;
    
    1493
    +}
    
    1494
    +
    
    1495
    +/**
    
    1496
    + * Instantiates and returns a controller object that is connected and
    
    1497
    + * authenticated to a Tor ControlPort using the connection parameters
    
    1498
    + * provided in the most recent call to configureControlPortModule().
    
    1499
    + *
    
    1500
    + * Example:
    
    1501
    + *     // Get a new controller
    
    1502
    + *     let c = await controller();
    
    1503
    + *     // Send command and receive a `250` reply or an error message:
    
    1504
    + *     let replyPromise = await c.getInfo("ip-to-country/16.16.16.16");
    
    1505
    + *     // Close the controller permanently
    
    1506
    + *     c.close();
    
    1507
    + */
    
    1508
    +export async function controller() {
    
    1509
    +  if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
    
    1510
    +    throw new Error("Please call configureControlPortModule first");
    
    1511
    +  }
    
    1512
    +  let socket;
    
    1513
    +  if (controlPortInfo.ipcFile) {
    
    1514
    +    socket = AsyncSocket.fromIpcFile(controlPortInfo.ipcFile);
    
    1515
    +  } else {
    
    1516
    +    socket = AsyncSocket.fromSocketAddress(
    
    1517
    +      controlPortInfo.host,
    
    1518
    +      controlPortInfo.port
    
    1519
    +    );
    
    1520
    +  }
    
    1521
    +  const controller = new TorController(socket);
    
    1522
    +  try {
    
    1523
    +    await controller.authenticate(controlPortInfo.password);
    
    1524
    +  } catch (e) {
    
    1525
    +    try {
    
    1526
    +      controller.close();
    
    1527
    +    } catch (ec) {
    
    1528
    +      // TODO: Use a custom logger?
    
    1529
    +      console.error("Cannot close the socket", ec);
    
    1530
    +    }
    
    1531
    +    throw e;
    
    1532
    +  }
    
    1533
    +  return controller;
    
    1534
    +}

  • toolkit/components/tor-launcher/TorMonitorService.sys.mjs
    ... ... @@ -15,14 +15,9 @@ const lazy = {};
    15 15
     
    
    16 16
     ChromeUtils.defineESModuleGetters(lazy, {
    
    17 17
       TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
    
    18
    +  controller: "resource://gre/modules/TorControlPort.sys.mjs",
    
    18 19
     });
    
    19 20
     
    
    20
    -ChromeUtils.defineModuleGetter(
    
    21
    -  lazy,
    
    22
    -  "controller",
    
    23
    -  "resource://torbutton/modules/tor-control-port.js"
    
    24
    -);
    
    25
    -
    
    26 21
     ChromeUtils.defineESModuleGetters(lazy, {
    
    27 22
       TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
    
    28 23
     });
    
    ... ... @@ -172,9 +167,7 @@ export const TorMonitorService = {
    172 167
         const cmd = "GETINFO";
    
    173 168
         const key = "status/bootstrap-phase";
    
    174 169
         let reply = await this._connection.sendCommand(`${cmd} ${key}`);
    
    175
    -    if (!reply) {
    
    176
    -      throw new Error("We received an empty reply");
    
    177
    -    }
    
    170
    +
    
    178 171
         // A typical reply looks like:
    
    179 172
         //  250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
    
    180 173
         //  250 OK
    
    ... ... @@ -335,8 +328,7 @@ export const TorMonitorService = {
    335 328
     
    
    336 329
         let conn;
    
    337 330
         try {
    
    338
    -      const avoidCache = true;
    
    339
    -      conn = await lazy.controller(avoidCache);
    
    331
    +      conn = await lazy.controller();
    
    340 332
         } catch (e) {
    
    341 333
           logger.error("Cannot open a control port connection", e);
    
    342 334
           if (conn) {
    
    ... ... @@ -353,12 +345,10 @@ export const TorMonitorService = {
    353 345
         }
    
    354 346
     
    
    355 347
         // TODO: optionally monitor INFO and DEBUG log messages.
    
    356
    -    let reply = await conn.sendCommand(
    
    357
    -      "SETEVENTS " + Array.from(this._eventHandlers.keys()).join(" ")
    
    358
    -    );
    
    359
    -    reply = TorParsers.parseCommandResponse(reply);
    
    360
    -    if (!TorParsers.commandSucceeded(reply)) {
    
    361
    -      logger.error("SETEVENTS failed");
    
    348
    +    try {
    
    349
    +      await conn.setEvents(Array.from(this._eventHandlers.keys()));
    
    350
    +    } catch (e) {
    
    351
    +      logger.error("SETEVENTS failed", e);
    
    362 352
           conn.close();
    
    363 353
           return false;
    
    364 354
         }
    
    ... ... @@ -405,18 +395,16 @@ export const TorMonitorService = {
    405 395
     
    
    406 396
       // Try to become the primary controller (TAKEOWNERSHIP).
    
    407 397
       async _takeTorOwnership(conn) {
    
    408
    -    const takeOwnership = "TAKEOWNERSHIP";
    
    409
    -    let reply = await conn.sendCommand(takeOwnership);
    
    410
    -    reply = TorParsers.parseCommandResponse(reply);
    
    411
    -    if (!TorParsers.commandSucceeded(reply)) {
    
    412
    -      logger.warn("Take ownership failed");
    
    413
    -    } else {
    
    414
    -      const resetConf = "RESETCONF __OwningControllerProcess";
    
    415
    -      reply = await conn.sendCommand(resetConf);
    
    416
    -      reply = TorParsers.parseCommandResponse(reply);
    
    417
    -      if (!TorParsers.commandSucceeded(reply)) {
    
    418
    -        logger.warn("Clear owning controller process failed");
    
    419
    -      }
    
    398
    +    try {
    
    399
    +      conn.takeOwnership();
    
    400
    +    } catch (e) {
    
    401
    +      logger.warn("Take ownership failed", e);
    
    402
    +      return;
    
    403
    +    }
    
    404
    +    try {
    
    405
    +      conn.resetOwningControllerProcess();
    
    406
    +    } catch (e) {
    
    407
    +      logger.warn("Clear owning controller process failed", e);
    
    420 408
         }
    
    421 409
       },
    
    422 410
     
    

  • toolkit/components/tor-launcher/TorProtocolService.sys.mjs
    ... ... @@ -19,16 +19,10 @@ ChromeUtils.defineModuleGetter(
    19 19
       "TorMonitorService",
    
    20 20
       "resource://gre/modules/TorMonitorService.jsm"
    
    21 21
     );
    
    22
    -ChromeUtils.defineModuleGetter(
    
    23
    -  lazy,
    
    24
    -  "configureControlPortModule",
    
    25
    -  "resource://torbutton/modules/tor-control-port.js"
    
    26
    -);
    
    27
    -ChromeUtils.defineModuleGetter(
    
    28
    -  lazy,
    
    29
    -  "controller",
    
    30
    -  "resource://torbutton/modules/tor-control-port.js"
    
    31
    -);
    
    22
    +ChromeUtils.defineESModuleGetters(lazy, {
    
    23
    +  controller: "resource://gre/modules/TorControlPort.sys.mjs",
    
    24
    +  configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
    
    25
    +});
    
    32 26
     
    
    33 27
     const TorTopics = Object.freeze({
    
    34 28
       ProcessExited: "TorProcessExited",
    
    ... ... @@ -285,8 +279,7 @@ export const TorProtocolService = {
    285 279
         });
    
    286 280
       },
    
    287 281
     
    
    288
    -  // TODO: transform the following 4 functions in getters. At the moment they
    
    289
    -  // are also used in torbutton.
    
    282
    +  // TODO: transform the following 4 functions in getters.
    
    290 283
     
    
    291 284
       // Returns Tor password string or null if an error occurs.
    
    292 285
       torGetPassword() {
    
    ... ... @@ -490,8 +483,6 @@ export const TorProtocolService = {
    490 483
           TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo);
    
    491 484
     
    
    492 485
           // Set the global control port info parameters.
    
    493
    -      // These values may be overwritten by torbutton when it initializes, but
    
    494
    -      // torbutton's values *should* be identical.
    
    495 486
           lazy.configureControlPortModule(
    
    496 487
             this._controlIPCFile,
    
    497 488
             this._controlHost,
    
    ... ... @@ -616,8 +607,7 @@ export const TorProtocolService = {
    616 607
       // return it.
    
    617 608
       async _getConnection() {
    
    618 609
         if (!this._controlConnection) {
    
    619
    -      const avoidCache = true;
    
    620
    -      this._controlConnection = await lazy.controller(avoidCache);
    
    610
    +      this._controlConnection = await lazy.controller();
    
    621 611
         }
    
    622 612
         if (this._controlConnection.inUse) {
    
    623 613
           await new Promise((resolve, reject) =>
    

  • toolkit/components/tor-launcher/moz.build
    1 1
     EXTRA_JS_MODULES += [
    
    2 2
         "TorBootstrapRequest.sys.mjs",
    
    3
    +    "TorControlPort.sys.mjs",
    
    3 4
         "TorDomainIsolator.sys.mjs",
    
    4 5
         "TorLauncherUtil.sys.mjs",
    
    5 6
         "TorMonitorService.sys.mjs",
    

  • toolkit/torbutton/chrome/content/torbutton.js deleted
    1
    -// window globals
    
    2
    -var torbutton_init;
    
    3
    -
    
    4
    -(() => {
    
    5
    -  // Bug 1506 P1-P5: This is the main Torbutton overlay file. Much needs to be
    
    6
    -  // preserved here, but in an ideal world, most of this code should perhaps be
    
    7
    -  // moved into an XPCOM service, and much can also be tossed. See also
    
    8
    -  // individual 1506 comments for details.
    
    9
    -
    
    10
    -  // TODO: check for leaks: http://www.mozilla.org/scriptable/avoiding-leaks.html
    
    11
    -  // TODO: Double-check there are no strange exploits to defeat:
    
    12
    -  //       http://kb.mozillazine.org/Links_to_local_pages_don%27t_work
    
    13
    -
    
    14
    -  /* global gBrowser, Services, AppConstants */
    
    15
    -
    
    16
    -  let { torbutton_log } = ChromeUtils.import(
    
    17
    -    "resource://torbutton/modules/utils.js"
    
    18
    -  );
    
    19
    -  let { configureControlPortModule } = ChromeUtils.import(
    
    20
    -    "resource://torbutton/modules/tor-control-port.js"
    
    21
    -  );
    
    22
    -
    
    23
    -  const { TorProtocolService } = ChromeUtils.import(
    
    24
    -    "resource://gre/modules/TorProtocolService.jsm"
    
    25
    -  );
    
    26
    -
    
    27
    -  var m_tb_prefs = Services.prefs;
    
    28
    -
    
    29
    -  // status
    
    30
    -  var m_tb_wasinited = false;
    
    31
    -
    
    32
    -  var m_tb_control_ipc_file = null; // Set if using IPC (UNIX domain socket).
    
    33
    -  var m_tb_control_port = null; // Set if using TCP.
    
    34
    -  var m_tb_control_host = null; // Set if using TCP.
    
    35
    -  var m_tb_control_pass = null;
    
    36
    -
    
    37
    -  // Bug 1506 P2-P4: This code sets some version variables that are irrelevant.
    
    38
    -  // It does read out some important environment variables, though. It is
    
    39
    -  // called once per browser window.. This might belong in a component.
    
    40
    -  torbutton_init = function () {
    
    41
    -    torbutton_log(3, "called init()");
    
    42
    -
    
    43
    -    if (m_tb_wasinited) {
    
    44
    -      return;
    
    45
    -    }
    
    46
    -    m_tb_wasinited = true;
    
    47
    -
    
    48
    -    // Bug 1506 P4: These vars are very important for New Identity
    
    49
    -    if (Services.env.exists("TOR_CONTROL_PASSWD")) {
    
    50
    -      m_tb_control_pass = Services.env.get("TOR_CONTROL_PASSWD");
    
    51
    -    } else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
    
    52
    -      var cookie_path = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
    
    53
    -      try {
    
    54
    -        if ("" != cookie_path) {
    
    55
    -          m_tb_control_pass = torbutton_read_authentication_cookie(cookie_path);
    
    56
    -        }
    
    57
    -      } catch (e) {
    
    58
    -        torbutton_log(4, "unable to read authentication cookie");
    
    59
    -      }
    
    60
    -    } else {
    
    61
    -      try {
    
    62
    -        // Try to get password from Tor Launcher.
    
    63
    -        m_tb_control_pass = TorProtocolService.torGetPassword();
    
    64
    -      } catch (e) {}
    
    65
    -    }
    
    66
    -
    
    67
    -    // Try to get the control port IPC file (an nsIFile) from Tor Launcher,
    
    68
    -    // since Tor Launcher knows how to handle its own preferences and how to
    
    69
    -    // resolve relative paths.
    
    70
    -    try {
    
    71
    -      m_tb_control_ipc_file = TorProtocolService.torGetControlIPCFile();
    
    72
    -    } catch (e) {}
    
    73
    -
    
    74
    -    if (!m_tb_control_ipc_file) {
    
    75
    -      if (Services.env.exists("TOR_CONTROL_PORT")) {
    
    76
    -        m_tb_control_port = Services.env.get("TOR_CONTROL_PORT");
    
    77
    -      } else {
    
    78
    -        try {
    
    79
    -          const kTLControlPortPref = "extensions.torlauncher.control_port";
    
    80
    -          m_tb_control_port = m_tb_prefs.getIntPref(kTLControlPortPref);
    
    81
    -        } catch (e) {
    
    82
    -          // Since we want to disable some features when Tor Launcher is
    
    83
    -          // not installed (e.g., New Identity), we do not set a default
    
    84
    -          // port value here.
    
    85
    -        }
    
    86
    -      }
    
    87
    -
    
    88
    -      if (Services.env.exists("TOR_CONTROL_HOST")) {
    
    89
    -        m_tb_control_host = Services.env.get("TOR_CONTROL_HOST");
    
    90
    -      } else {
    
    91
    -        try {
    
    92
    -          const kTLControlHostPref = "extensions.torlauncher.control_host";
    
    93
    -          m_tb_control_host = m_tb_prefs.getCharPref(kTLControlHostPref);
    
    94
    -        } catch (e) {
    
    95
    -          m_tb_control_host = "127.0.0.1";
    
    96
    -        }
    
    97
    -      }
    
    98
    -    }
    
    99
    -
    
    100
    -    configureControlPortModule(
    
    101
    -      m_tb_control_ipc_file,
    
    102
    -      m_tb_control_host,
    
    103
    -      m_tb_control_port,
    
    104
    -      m_tb_control_pass
    
    105
    -    );
    
    106
    -
    
    107
    -    torbutton_log(3, "init completed");
    
    108
    -  };
    
    109
    -
    
    110
    -  // Bug 1506 P4: Control port interaction. Needed for New Identity.
    
    111
    -  function torbutton_read_authentication_cookie(path) {
    
    112
    -    var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
    
    113
    -    file.initWithPath(path);
    
    114
    -    var fileStream = Cc[
    
    115
    -      "@mozilla.org/network/file-input-stream;1"
    
    116
    -    ].createInstance(Ci.nsIFileInputStream);
    
    117
    -    fileStream.init(file, 1, 0, false);
    
    118
    -    var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
    
    119
    -      Ci.nsIBinaryInputStream
    
    120
    -    );
    
    121
    -    binaryStream.setInputStream(fileStream);
    
    122
    -    var array = binaryStream.readByteArray(fileStream.available());
    
    123
    -    binaryStream.close();
    
    124
    -    fileStream.close();
    
    125
    -    return torbutton_array_to_hexdigits(array);
    
    126
    -  }
    
    127
    -
    
    128
    -  // Bug 1506 P4: Control port interaction. Needed for New Identity.
    
    129
    -  function torbutton_array_to_hexdigits(array) {
    
    130
    -    return array
    
    131
    -      .map(function (c) {
    
    132
    -        return String("0" + c.toString(16)).slice(-2);
    
    133
    -      })
    
    134
    -      .join("");
    
    135
    -  }
    
    136
    -
    
    137
    -  // ---------------------- Event handlers -----------------
    
    138
    -
    
    139
    -  // Bug 1506 P3: This is needed pretty much only for the window resizing.
    
    140
    -  // See comments for individual functions for details
    
    141
    -  function torbutton_new_window(event) {
    
    142
    -    torbutton_log(3, "New window");
    
    143
    -    if (!m_tb_wasinited) {
    
    144
    -      torbutton_init();
    
    145
    -    }
    
    146
    -  }
    
    147
    -  window.addEventListener("load", torbutton_new_window);
    
    148
    -})();

  • toolkit/torbutton/components.conf deleted
    1
    -Classes = [
    
    2
    -    {
    
    3
    -        "cid": "{f36d72c9-9718-4134-b550-e109638331d7}",
    
    4
    -        "contract_ids": [
    
    5
    -            "@torproject.org/torbutton-logger;1"
    
    6
    -        ],
    
    7
    -        "jsm": "resource://torbutton/modules/TorbuttonLogger.jsm",
    
    8
    -        "constructor": "TorbuttonLogger",
    
    9
    -    },
    
    10
    -]

  • toolkit/torbutton/jar.mn
    1 1
     #filter substitution
    
    2 2
     
    
    3 3
     torbutton.jar:
    
    4
    -
    
    5
    -% content torbutton %content/
    
    6
    -
    
    7
    -  content/torbutton.js                   (chrome/content/torbutton.js)
    
    8
    -
    
    9
    -  modules/       (modules/*)
    
    10
    -
    
    11 4
     % resource torbutton %
    
    5
    +% category l10n-registry torbutton resource://torbutton/locale/{locale}/
    
    12 6
     
    
    13 7
     # browser branding
    
    14 8
     % override chrome://branding/locale/brand.dtd chrome://torbutton/locale/brand.dtd
    
    15 9
     % override chrome://branding/locale/brand.properties chrome://torbutton/locale/brand.properties
    
    16
    -% category l10n-registry torbutton resource://torbutton/locale/{locale}/
    
    17 10
     
    
    18 11
     # Strings for the about:tbupdate page
    
    19 12
     % override chrome://browser/locale/aboutTBUpdate.dtd chrome://torbutton/locale/aboutTBUpdate.dtd
    

  • toolkit/torbutton/modules/TorbuttonLogger.jsm deleted
    1
    -// Bug 1506 P1: This is just a handy logger. If you have a better one, toss
    
    2
    -// this in the trash.
    
    3
    -
    
    4
    -/*************************************************************************
    
    5
    - * TBLogger (_javascript_ XPCOM component)
    
    6
    - *
    
    7
    - * Allows loglevel-based logging to different logging mechanisms.
    
    8
    - *
    
    9
    - *************************************************************************/
    
    10
    -
    
    11
    -var EXPORTED_SYMBOLS = ["TorbuttonLogger"];
    
    12
    -
    
    13
    -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
    
    14
    -
    
    15
    -function TorbuttonLogger() {
    
    16
    -  // Register observer
    
    17
    -  Services.prefs.addObserver("extensions.torbutton", this);
    
    18
    -
    
    19
    -  this.loglevel = Services.prefs.getIntPref("extensions.torbutton.loglevel", 4);
    
    20
    -  this.logmethod = Services.prefs.getIntPref(
    
    21
    -    "extensions.torbutton.logmethod",
    
    22
    -    1
    
    23
    -  );
    
    24
    -
    
    25
    -  try {
    
    26
    -    var logMngr = Cc["@mozmonkey.com/debuglogger/manager;1"].getService(
    
    27
    -      Ci.nsIDebugLoggerManager
    
    28
    -    );
    
    29
    -    this._debuglog = logMngr.registerLogger("torbutton");
    
    30
    -  } catch (exErr) {
    
    31
    -    this._debuglog = false;
    
    32
    -  }
    
    33
    -  this._console = Services.console;
    
    34
    -
    
    35
    -  // This JSObject is exported directly to chrome
    
    36
    -  this.wrappedJSObject = this;
    
    37
    -  this.log(3, "Torbutton debug output ready");
    
    38
    -}
    
    39
    -
    
    40
    -/**
    
    41
    - * JS XPCOM component registration goop:
    
    42
    - *
    
    43
    - * Everything below is boring boilerplate and can probably be ignored.
    
    44
    - */
    
    45
    -
    
    46
    -TorbuttonLogger.prototype = {
    
    47
    -  QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]),
    
    48
    -
    
    49
    -  wrappedJSObject: null, // Initialized by constructor
    
    50
    -
    
    51
    -  formatLog(str, level) {
    
    52
    -    const padInt = n => String(n).padStart(2, "0");
    
    53
    -    const logString = { 1: "VERB", 2: "DBUG", 3: "INFO", 4: "NOTE", 5: "WARN" };
    
    54
    -    const d = new Date();
    
    55
    -    const now =
    
    56
    -      padInt(d.getUTCMonth() + 1) +
    
    57
    -      "-" +
    
    58
    -      padInt(d.getUTCDate()) +
    
    59
    -      " " +
    
    60
    -      padInt(d.getUTCHours()) +
    
    61
    -      ":" +
    
    62
    -      padInt(d.getUTCMinutes()) +
    
    63
    -      ":" +
    
    64
    -      padInt(d.getUTCSeconds());
    
    65
    -    return `${now} Torbutton ${logString[level]}: ${str}`;
    
    66
    -  },
    
    67
    -
    
    68
    -  // error console log
    
    69
    -  eclog(level, str) {
    
    70
    -    switch (this.logmethod) {
    
    71
    -      case 0: // stderr
    
    72
    -        if (this.loglevel <= level) {
    
    73
    -          dump(this.formatLog(str, level) + "\n");
    
    74
    -        }
    
    75
    -        break;
    
    76
    -      default:
    
    77
    -        // errorconsole
    
    78
    -        if (this.loglevel <= level) {
    
    79
    -          this._console.logStringMessage(this.formatLog(str, level));
    
    80
    -        }
    
    81
    -        break;
    
    82
    -    }
    
    83
    -  },
    
    84
    -
    
    85
    -  safe_log(level, str, scrub) {
    
    86
    -    if (this.loglevel < 4) {
    
    87
    -      this.eclog(level, str + scrub);
    
    88
    -    } else {
    
    89
    -      this.eclog(level, str + " [scrubbed]");
    
    90
    -    }
    
    91
    -  },
    
    92
    -
    
    93
    -  log(level, str) {
    
    94
    -    switch (this.logmethod) {
    
    95
    -      case 2: // debuglogger
    
    96
    -        if (this._debuglog) {
    
    97
    -          this._debuglog.log(6 - level, this.formatLog(str, level));
    
    98
    -          break;
    
    99
    -        }
    
    100
    -      // fallthrough
    
    101
    -      case 0: // stderr
    
    102
    -        if (this.loglevel <= level) {
    
    103
    -          dump(this.formatLog(str, level) + "\n");
    
    104
    -        }
    
    105
    -        break;
    
    106
    -      case 1: // errorconsole
    
    107
    -        if (this.loglevel <= level) {
    
    108
    -          this._console.logStringMessage(this.formatLog(str, level));
    
    109
    -        }
    
    110
    -        break;
    
    111
    -      default:
    
    112
    -        dump("Bad log method: " + this.logmethod);
    
    113
    -    }
    
    114
    -  },
    
    115
    -
    
    116
    -  // Pref observer interface implementation
    
    117
    -
    
    118
    -  // topic:   what event occurred
    
    119
    -  // subject: what nsIPrefBranch we're observing
    
    120
    -  // data:    which pref has been changed (relative to subject)
    
    121
    -  observe(subject, topic, data) {
    
    122
    -    if (topic != "nsPref:changed") {
    
    123
    -      return;
    
    124
    -    }
    
    125
    -    switch (data) {
    
    126
    -      case "extensions.torbutton.logmethod":
    
    127
    -        this.logmethod = Services.prefs.getIntPref(
    
    128
    -          "extensions.torbutton.logmethod"
    
    129
    -        );
    
    130
    -        if (this.logmethod === 0) {
    
    131
    -          Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
    
    132
    -        } else if (
    
    133
    -          Services.prefs.getIntPref("extensions.torlauncher.logmethod", 3) !== 0
    
    134
    -        ) {
    
    135
    -          // If Tor Launcher is not available or its log method is not 0
    
    136
    -          // then let's reset the dump pref.
    
    137
    -          Services.prefs.setBoolPref("browser.dom.window.dump.enabled", false);
    
    138
    -        }
    
    139
    -        break;
    
    140
    -      case "extensions.torbutton.loglevel":
    
    141
    -        this.loglevel = Services.prefs.getIntPref(
    
    142
    -          "extensions.torbutton.loglevel"
    
    143
    -        );
    
    144
    -        break;
    
    145
    -    }
    
    146
    -  },
    
    147
    -};

  • toolkit/torbutton/modules/tor-control-port.js deleted
    1
    -// A module for TorBrowser that provides an asynchronous controller for
    
    2
    -// Tor, through its ControlPort.
    
    3
    -//
    
    4
    -// This file is written in call stack order (later functions
    
    5
    -// call earlier functions). The file can be processed
    
    6
    -// with docco.js to produce pretty documentation.
    
    7
    -//
    
    8
    -// To import the module, use
    
    9
    -//
    
    10
    -//  let { configureControlPortModule, controller, wait_for_controller } =
    
    11
    -//                Components.utils.import("path/to/tor-control-port.js", {});
    
    12
    -//
    
    13
    -// See the third-to-last function defined in this file:
    
    14
    -//   configureControlPortModule(ipcFile, host, port, password)
    
    15
    -// for usage of the configureControlPortModule function.
    
    16
    -//
    
    17
    -// See the last functions defined in this file:
    
    18
    -//   controller(avoidCache), wait_for_controller(avoidCache)
    
    19
    -// for usage of the controller functions.
    
    20
    -
    
    21
    -/* jshint esnext: true */
    
    22
    -/* jshint -W097 */
    
    23
    -/* global console */
    
    24
    -"use strict";
    
    25
    -
    
    26
    -const { XPCOMUtils } = ChromeUtils.importESModule(
    
    27
    -  "resource://gre/modules/XPCOMUtils.sys.mjs"
    
    28
    -);
    
    29
    -
    
    30
    -ChromeUtils.defineModuleGetter(
    
    31
    -  this,
    
    32
    -  "TorMonitorService",
    
    33
    -  "resource://gre/modules/TorMonitorService.jsm"
    
    34
    -);
    
    35
    -
    
    36
    -XPCOMUtils.defineLazyServiceGetter(
    
    37
    -  this,
    
    38
    -  "logger",
    
    39
    -  "@torproject.org/torbutton-logger;1",
    
    40
    -  "nsISupports"
    
    41
    -);
    
    42
    -
    
    43
    -// tor-launcher observer topics
    
    44
    -const TorTopics = Object.freeze({
    
    45
    -  ProcessIsReady: "TorProcessIsReady",
    
    46
    -});
    
    47
    -
    
    48
    -// __log__.
    
    49
    -// Logging function
    
    50
    -let log = x =>
    
    51
    -  logger.wrappedJSObject.eclog(3, x.trimRight().replace(/\r\n/g, "\n"));
    
    52
    -
    
    53
    -// ### announce this file
    
    54
    -log("Loading tor-control-port.js\n");
    
    55
    -
    
    56
    -class AsyncSocket {
    
    57
    -  constructor(ipcFile, host, port) {
    
    58
    -    let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
    
    59
    -      Ci.nsISocketTransportService
    
    60
    -    );
    
    61
    -    const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
    
    62
    -
    
    63
    -    let socketTransport = ipcFile
    
    64
    -      ? sts.createUnixDomainTransport(ipcFile)
    
    65
    -      : sts.createTransport([], host, port, null, null);
    
    66
    -
    
    67
    -    this.outputStream = socketTransport
    
    68
    -      .openOutputStream(OPEN_UNBUFFERED, 1, 1)
    
    69
    -      .QueryInterface(Ci.nsIAsyncOutputStream);
    
    70
    -    this.outputQueue = [];
    
    71
    -
    
    72
    -    this.inputStream = socketTransport
    
    73
    -      .openInputStream(OPEN_UNBUFFERED, 1, 1)
    
    74
    -      .QueryInterface(Ci.nsIAsyncInputStream);
    
    75
    -    this.scriptableInputStream = Cc[
    
    76
    -      "@mozilla.org/scriptableinputstream;1"
    
    77
    -    ].createInstance(Ci.nsIScriptableInputStream);
    
    78
    -    this.scriptableInputStream.init(this.inputStream);
    
    79
    -    this.inputQueue = [];
    
    80
    -  }
    
    81
    -
    
    82
    -  // asynchronously write string to underlying socket and return number of bytes written
    
    83
    -  async write(str) {
    
    84
    -    return new Promise((resolve, reject) => {
    
    85
    -      // asyncWait next write request
    
    86
    -      const tryAsyncWait = () => {
    
    87
    -        if (this.outputQueue.length) {
    
    88
    -          this.outputStream.asyncWait(
    
    89
    -            this.outputQueue.at(0), // next request
    
    90
    -            0,
    
    91
    -            0,
    
    92
    -            Services.tm.currentThread
    
    93
    -          );
    
    94
    -        }
    
    95
    -      };
    
    96
    -
    
    97
    -      // output stream can only have 1 registered callback at a time, so multiple writes
    
    98
    -      // need to be queued up (see nsIAsyncOutputStream.idl)
    
    99
    -      this.outputQueue.push({
    
    100
    -        // Implement an nsIOutputStreamCallback:
    
    101
    -        onOutputStreamReady: () => {
    
    102
    -          try {
    
    103
    -            let bytesWritten = this.outputStream.write(str, str.length);
    
    104
    -
    
    105
    -            // remove this callback object from queue as it is now completed
    
    106
    -            this.outputQueue.shift();
    
    107
    -
    
    108
    -            // request next wait if there is one
    
    109
    -            tryAsyncWait();
    
    110
    -
    
    111
    -            // finally resolve promise
    
    112
    -            resolve(bytesWritten);
    
    113
    -          } catch (err) {
    
    114
    -            // reject promise on error
    
    115
    -            reject(err);
    
    116
    -          }
    
    117
    -        },
    
    118
    -      });
    
    119
    -
    
    120
    -      // length 1 imples that there is no in-flight asyncWait, so we may immediately
    
    121
    -      // follow through on this write
    
    122
    -      if (this.outputQueue.length == 1) {
    
    123
    -        tryAsyncWait();
    
    124
    -      }
    
    125
    -    });
    
    126
    -  }
    
    127
    -
    
    128
    -  // asynchronously read string from underlying socket and return it
    
    129
    -  async read() {
    
    130
    -    return new Promise((resolve, reject) => {
    
    131
    -      const tryAsyncWait = () => {
    
    132
    -        if (this.inputQueue.length) {
    
    133
    -          this.inputStream.asyncWait(
    
    134
    -            this.inputQueue.at(0), // next input request
    
    135
    -            0,
    
    136
    -            0,
    
    137
    -            Services.tm.currentThread
    
    138
    -          );
    
    139
    -        }
    
    140
    -      };
    
    141
    -
    
    142
    -      this.inputQueue.push({
    
    143
    -        onInputStreamReady: stream => {
    
    144
    -          try {
    
    145
    -            if (!this.scriptableInputStream.available()) {
    
    146
    -              // This means EOF, but not closed yet. However, arriving at EOF
    
    147
    -              // should be an error condition for us, since we are in a socket,
    
    148
    -              // and EOF should mean peer disconnected.
    
    149
    -              // If the stream has been closed, this function itself should
    
    150
    -              // throw.
    
    151
    -              reject(
    
    152
    -                new Error("onInputStreamReady called without available bytes.")
    
    153
    -              );
    
    154
    -              return;
    
    155
    -            }
    
    156
    -
    
    157
    -            // read our string from input stream
    
    158
    -            let str = this.scriptableInputStream.read(
    
    159
    -              this.scriptableInputStream.available()
    
    160
    -            );
    
    161
    -
    
    162
    -            // remove this callback object from queue now that we have read
    
    163
    -            this.inputQueue.shift();
    
    164
    -
    
    165
    -            // request next wait if there is one
    
    166
    -            tryAsyncWait();
    
    167
    -
    
    168
    -            // finally resolve promise
    
    169
    -            resolve(str);
    
    170
    -          } catch (err) {
    
    171
    -            reject(err);
    
    172
    -          }
    
    173
    -        },
    
    174
    -      });
    
    175
    -
    
    176
    -      // length 1 imples that there is no in-flight asyncWait, so we may immediately
    
    177
    -      // follow through on this read
    
    178
    -      if (this.inputQueue.length == 1) {
    
    179
    -        tryAsyncWait();
    
    180
    -      }
    
    181
    -    });
    
    182
    -  }
    
    183
    -
    
    184
    -  close() {
    
    185
    -    this.outputStream.close();
    
    186
    -    this.inputStream.close();
    
    187
    -  }
    
    188
    -}
    
    189
    -
    
    190
    -class ControlSocket {
    
    191
    -  constructor(asyncSocket) {
    
    192
    -    this.socket = asyncSocket;
    
    193
    -    this._isOpen = true;
    
    194
    -    this.pendingData = "";
    
    195
    -    this.pendingLines = [];
    
    196
    -
    
    197
    -    this.mainDispatcher = io.callbackDispatcher();
    
    198
    -    this.notificationDispatcher = io.callbackDispatcher();
    
    199
    -    // mainDispatcher pushes only async notifications (650) to notificationDispatcher
    
    200
    -    this.mainDispatcher.addCallback(
    
    201
    -      /^650/,
    
    202
    -      this._handleNotification.bind(this)
    
    203
    -    );
    
    204
    -    // callback for handling responses and errors
    
    205
    -    this.mainDispatcher.addCallback(
    
    206
    -      /^[245]\d\d/,
    
    207
    -      this._handleCommandReply.bind(this)
    
    208
    -    );
    
    209
    -
    
    210
    -    this.commandQueue = [];
    
    211
    -
    
    212
    -    this._startMessagePump();
    
    213
    -  }
    
    214
    -
    
    215
    -  // blocks until an entire line is read and returns it
    
    216
    -  // immediately returns next line in queue (pendingLines) if present
    
    217
    -  async _readLine() {
    
    218
    -    // keep reading from socket until we have a full line to return
    
    219
    -    while (!this.pendingLines.length) {
    
    220
    -      // read data from our socket and spit on newline tokens
    
    221
    -      this.pendingData += await this.socket.read();
    
    222
    -      let lines = this.pendingData.split("\r\n");
    
    223
    -
    
    224
    -      // the last line will either be empty string, or a partial read of a response/event
    
    225
    -      // so save it off for the next socket read
    
    226
    -      this.pendingData = lines.pop();
    
    227
    -
    
    228
    -      // copy remaining full lines to our pendingLines list
    
    229
    -      this.pendingLines = this.pendingLines.concat(lines);
    
    230
    -    }
    
    231
    -    return this.pendingLines.shift();
    
    232
    -  }
    
    233
    -
    
    234
    -  // blocks until an entire message is ready and returns it
    
    235
    -  async _readMessage() {
    
    236
    -    // whether we are searching for the end of a multi-line values
    
    237
    -    // See control-spec section 3.9
    
    238
    -    let handlingMultlineValue = false;
    
    239
    -    let endOfMessageFound = false;
    
    240
    -    const message = [];
    
    241
    -
    
    242
    -    do {
    
    243
    -      const line = await this._readLine();
    
    244
    -      message.push(line);
    
    245
    -
    
    246
    -      if (handlingMultlineValue) {
    
    247
    -        // look for end of multiline
    
    248
    -        if (line.match(/^\.$/)) {
    
    249
    -          handlingMultlineValue = false;
    
    250
    -        }
    
    251
    -      } else {
    
    252
    -        // 'Multiline values' are possible. We avoid interrupting one by detecting it
    
    253
    -        // and waiting for a terminating "." on its own line.
    
    254
    -        // (See control-spec section 3.9 and https://trac.torproject.org/16990#comment:28
    
    255
    -        // Ensure this is the first line of a new message
    
    256
    -        // eslint-disable-next-line no-lonely-if
    
    257
    -        if (message.length === 1 && line.match(/^\d\d\d\+.+?=$/)) {
    
    258
    -          handlingMultlineValue = true;
    
    259
    -        }
    
    260
    -        // look for end of message (note the space character at end of the regex)
    
    261
    -        else if (line.match(/^\d\d\d /)) {
    
    262
    -          if (message.length == 1) {
    
    263
    -            endOfMessageFound = true;
    
    264
    -          } else {
    
    265
    -            let firstReplyCode = message[0].substring(0, 3);
    
    266
    -            let lastReplyCode = line.substring(0, 3);
    
    267
    -            if (firstReplyCode == lastReplyCode) {
    
    268
    -              endOfMessageFound = true;
    
    269
    -            }
    
    270
    -          }
    
    271
    -        }
    
    272
    -      }
    
    273
    -    } while (!endOfMessageFound);
    
    274
    -
    
    275
    -    // join our lines back together to form one message
    
    276
    -    return message.join("\r\n");
    
    277
    -  }
    
    278
    -
    
    279
    -  async _startMessagePump() {
    
    280
    -    try {
    
    281
    -      while (true) {
    
    282
    -        let message = await this._readMessage();
    
    283
    -        log("controlPort >> " + message);
    
    284
    -        this.mainDispatcher.pushMessage(message);
    
    285
    -      }
    
    286
    -    } catch (err) {
    
    287
    -      this._isOpen = false;
    
    288
    -      for (const cmd of this.commandQueue) {
    
    289
    -        cmd.reject(err);
    
    290
    -      }
    
    291
    -      this.commandQueue = [];
    
    292
    -    }
    
    293
    -  }
    
    294
    -
    
    295
    -  _writeNextCommand() {
    
    296
    -    let cmd = this.commandQueue[0];
    
    297
    -    log("controlPort << " + cmd.commandString);
    
    298
    -    this.socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
    
    299
    -  }
    
    300
    -
    
    301
    -  async sendCommand(commandString) {
    
    302
    -    if (!this.isOpen()) {
    
    303
    -      throw new Error("ControlSocket not open");
    
    304
    -    }
    
    305
    -
    
    306
    -    // this promise is resolved either in _handleCommandReply, or
    
    307
    -    // in _startMessagePump (on stream error)
    
    308
    -    return new Promise((resolve, reject) => {
    
    309
    -      let command = {
    
    310
    -        commandString,
    
    311
    -        resolve,
    
    312
    -        reject,
    
    313
    -      };
    
    314
    -
    
    315
    -      this.commandQueue.push(command);
    
    316
    -      if (this.commandQueue.length == 1) {
    
    317
    -        this._writeNextCommand();
    
    318
    -      }
    
    319
    -    });
    
    320
    -  }
    
    321
    -
    
    322
    -  _handleCommandReply(message) {
    
    323
    -    let cmd = this.commandQueue.shift();
    
    324
    -    if (message.match(/^2/)) {
    
    325
    -      cmd.resolve(message);
    
    326
    -    } else if (message.match(/^[45]/)) {
    
    327
    -      let myErr = new Error(cmd.commandString + " -> " + message);
    
    328
    -      // Add Tor-specific information to the Error object.
    
    329
    -      let idx = message.indexOf(" ");
    
    330
    -      if (idx > 0) {
    
    331
    -        myErr.torStatusCode = message.substring(0, idx);
    
    332
    -        myErr.torMessage = message.substring(idx);
    
    333
    -      } else {
    
    334
    -        myErr.torStatusCode = message;
    
    335
    -      }
    
    336
    -      cmd.reject(myErr);
    
    337
    -    } else {
    
    338
    -      cmd.reject(
    
    339
    -        new Error(
    
    340
    -          `ControlSocket::_handleCommandReply received unexpected message:\n----\n${message}\n----`
    
    341
    -        )
    
    342
    -      );
    
    343
    -    }
    
    344
    -
    
    345
    -    // send next command if one is available
    
    346
    -    if (this.commandQueue.length) {
    
    347
    -      this._writeNextCommand();
    
    348
    -    }
    
    349
    -  }
    
    350
    -
    
    351
    -  _handleNotification(message) {
    
    352
    -    this.notificationDispatcher.pushMessage(message);
    
    353
    -  }
    
    354
    -
    
    355
    -  close() {
    
    356
    -    this.socket.close();
    
    357
    -    this._isOpen = false;
    
    358
    -  }
    
    359
    -
    
    360
    -  addNotificationCallback(regex, callback) {
    
    361
    -    this.notificationDispatcher.addCallback(regex, callback);
    
    362
    -  }
    
    363
    -
    
    364
    -  isOpen() {
    
    365
    -    return this._isOpen;
    
    366
    -  }
    
    367
    -}
    
    368
    -
    
    369
    -// ## io
    
    370
    -// I/O utilities namespace
    
    371
    -
    
    372
    -let io = {};
    
    373
    -
    
    374
    -// __io.callbackDispatcher()__.
    
    375
    -// Returns dispatcher object with three member functions:
    
    376
    -// dispatcher.addCallback(regex, callback), dispatcher.removeCallback(callback),
    
    377
    -// and dispatcher.pushMessage(message).
    
    378
    -// Pass pushMessage to another function that needs a callback with a single string
    
    379
    -// argument. Whenever dispatcher.pushMessage receives a string, the dispatcher will
    
    380
    -// check for any regex matches and pass the string on to the corresponding callback(s).
    
    381
    -io.callbackDispatcher = function () {
    
    382
    -  let callbackPairs = [],
    
    383
    -    removeCallback = function (aCallback) {
    
    384
    -      callbackPairs = callbackPairs.filter(function ([regex, callback]) {
    
    385
    -        return callback !== aCallback;
    
    386
    -      });
    
    387
    -    },
    
    388
    -    addCallback = function (regex, callback) {
    
    389
    -      if (callback) {
    
    390
    -        callbackPairs.push([regex, callback]);
    
    391
    -      }
    
    392
    -      return function () {
    
    393
    -        removeCallback(callback);
    
    394
    -      };
    
    395
    -    },
    
    396
    -    pushMessage = function (message) {
    
    397
    -      for (let [regex, callback] of callbackPairs) {
    
    398
    -        if (message.match(regex)) {
    
    399
    -          callback(message);
    
    400
    -        }
    
    401
    -      }
    
    402
    -    };
    
    403
    -  return {
    
    404
    -    pushMessage,
    
    405
    -    removeCallback,
    
    406
    -    addCallback,
    
    407
    -  };
    
    408
    -};
    
    409
    -
    
    410
    -// __io.controlSocket(ipcFile, host, port, password)__.
    
    411
    -// Instantiates and returns a socket to a tor ControlPort at ipcFile or
    
    412
    -// host:port, authenticating with the given password. Example:
    
    413
    -//
    
    414
    -//     // Open the socket
    
    415
    -//     let socket = await io.controlSocket(undefined, "127.0.0.1", 9151, "MyPassw0rd");
    
    416
    -//     // Send command and receive "250" response reply or error is thrown
    
    417
    -//     await socket.sendCommand(commandText);
    
    418
    -//     // Register or deregister for "650" notifications
    
    419
    -//     // that match regex
    
    420
    -//     socket.addNotificationCallback(regex, callback);
    
    421
    -//     socket.removeNotificationCallback(callback);
    
    422
    -//     // Close the socket permanently
    
    423
    -//     socket.close();
    
    424
    -io.controlSocket = async function (ipcFile, host, port, password) {
    
    425
    -  let socket = new AsyncSocket(ipcFile, host, port);
    
    426
    -  let controlSocket = new ControlSocket(socket);
    
    427
    -
    
    428
    -  // Log in to control port.
    
    429
    -  await controlSocket.sendCommand("authenticate " + (password || ""));
    
    430
    -  // Activate needed events.
    
    431
    -  await controlSocket.sendCommand("setevents stream");
    
    432
    -
    
    433
    -  return controlSocket;
    
    434
    -};
    
    435
    -
    
    436
    -// ## utils
    
    437
    -// A namespace for utility functions
    
    438
    -let utils = {};
    
    439
    -
    
    440
    -// __utils.identity(x)__.
    
    441
    -// Returns its argument unchanged.
    
    442
    -utils.identity = function (x) {
    
    443
    -  return x;
    
    444
    -};
    
    445
    -
    
    446
    -// __utils.isString(x)__.
    
    447
    -// Returns true iff x is a string.
    
    448
    -utils.isString = function (x) {
    
    449
    -  return typeof x === "string" || x instanceof String;
    
    450
    -};
    
    451
    -
    
    452
    -// __utils.capture(string, regex)__.
    
    453
    -// Takes a string and returns an array of capture items, where regex must have a single
    
    454
    -// capturing group and use the suffix /.../g to specify a global search.
    
    455
    -utils.capture = function (string, regex) {
    
    456
    -  let matches = [];
    
    457
    -  // Special trick to use string.replace for capturing multiple matches.
    
    458
    -  string.replace(regex, function (a, captured) {
    
    459
    -    matches.push(captured);
    
    460
    -  });
    
    461
    -  return matches;
    
    462
    -};
    
    463
    -
    
    464
    -// __utils.extractor(regex)__.
    
    465
    -// Returns a function that takes a string and returns an array of regex matches. The
    
    466
    -// regex must use the suffix /.../g to specify a global search.
    
    467
    -utils.extractor = function (regex) {
    
    468
    -  return function (text) {
    
    469
    -    return utils.capture(text, regex);
    
    470
    -  };
    
    471
    -};
    
    472
    -
    
    473
    -// __utils.splitLines(string)__.
    
    474
    -// Splits a string into an array of strings, each corresponding to a line.
    
    475
    -utils.splitLines = function (string) {
    
    476
    -  return string.split(/\r?\n/);
    
    477
    -};
    
    478
    -
    
    479
    -// __utils.splitAtSpaces(string)__.
    
    480
    -// Splits a string into chunks between spaces. Does not split at spaces
    
    481
    -// inside pairs of quotation marks.
    
    482
    -utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
    
    483
    -
    
    484
    -// __utils.splitAtFirst(string, regex)__.
    
    485
    -// Splits a string at the first instance of regex match. If no match is
    
    486
    -// found, returns the whole string.
    
    487
    -utils.splitAtFirst = function (string, regex) {
    
    488
    -  let match = string.match(regex);
    
    489
    -  return match
    
    490
    -    ? [
    
    491
    -        string.substring(0, match.index),
    
    492
    -        string.substring(match.index + match[0].length),
    
    493
    -      ]
    
    494
    -    : string;
    
    495
    -};
    
    496
    -
    
    497
    -// __utils.splitAtEquals(string)__.
    
    498
    -// Splits a string into chunks between equals. Does not split at equals
    
    499
    -// inside pairs of quotation marks.
    
    500
    -utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
    
    501
    -
    
    502
    -// __utils.mergeObjects(arrayOfObjects)__.
    
    503
    -// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
    
    504
    -// Pure function.
    
    505
    -utils.mergeObjects = function (arrayOfObjects) {
    
    506
    -  let result = {};
    
    507
    -  for (let obj of arrayOfObjects) {
    
    508
    -    for (let key in obj) {
    
    509
    -      result[key] = obj[key];
    
    510
    -    }
    
    511
    -  }
    
    512
    -  return result;
    
    513
    -};
    
    514
    -
    
    515
    -// __utils.listMapData(parameterString, listNames)__.
    
    516
    -// Takes a list of parameters separated by spaces, of which the first several are
    
    517
    -// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
    
    518
    -// to the unnamed parameters, and combine them in a map with the named parameters.
    
    519
    -// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
    
    520
    -//
    
    521
    -//     utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
    
    522
    -//                       ["streamID", "event", "circuitID", "IP"])
    
    523
    -//     // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
    
    524
    -//     //      "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
    
    525
    -utils.listMapData = function (parameterString, listNames) {
    
    526
    -  // Split out the space-delimited parameters.
    
    527
    -  let parameters = utils.splitAtSpaces(parameterString),
    
    528
    -    dataMap = {};
    
    529
    -  // Assign listNames to the first n = listNames.length parameters.
    
    530
    -  for (let i = 0; i < listNames.length; ++i) {
    
    531
    -    dataMap[listNames[i]] = parameters[i];
    
    532
    -  }
    
    533
    -  // Read key-value pairs and copy these to the dataMap.
    
    534
    -  for (let i = listNames.length; i < parameters.length; ++i) {
    
    535
    -    let [key, value] = utils.splitAtEquals(parameters[i]);
    
    536
    -    if (key && value) {
    
    537
    -      dataMap[key] = value;
    
    538
    -    }
    
    539
    -  }
    
    540
    -  return dataMap;
    
    541
    -};
    
    542
    -
    
    543
    -// __utils.rejectPromise(errorMessage)__.
    
    544
    -// Returns a rejected promise with the given error message.
    
    545
    -utils.rejectPromise = errorMessage => Promise.reject(new Error(errorMessage));
    
    546
    -
    
    547
    -// ## info
    
    548
    -// A namespace for functions related to tor's GETINFO and GETCONF command.
    
    549
    -let info = {};
    
    550
    -
    
    551
    -// __info.keyValueStringsFromMessage(messageText)__.
    
    552
    -// Takes a message (text) response to GETINFO or GETCONF and provides
    
    553
    -// a series of key-value strings, which are either multiline (with a `250+` prefix):
    
    554
    -//
    
    555
    -//     250+config/defaults=
    
    556
    -//     AccountingMax "0 bytes"
    
    557
    -//     AllowDotExit "0"
    
    558
    -//     .
    
    559
    -//
    
    560
    -// or single-line (with a `250-` or `250 ` prefix):
    
    561
    -//
    
    562
    -//     250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
    
    563
    -info.keyValueStringsFromMessage = utils.extractor(
    
    564
    -  /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
    
    565
    -);
    
    566
    -
    
    567
    -// __info.applyPerLine(transformFunction)__.
    
    568
    -// Returns a function that splits text into lines,
    
    569
    -// and applies transformFunction to each line.
    
    570
    -info.applyPerLine = function (transformFunction) {
    
    571
    -  return function (text) {
    
    572
    -    return utils.splitLines(text.trim()).map(transformFunction);
    
    573
    -  };
    
    574
    -};
    
    575
    -
    
    576
    -// __info.routerStatusParser(valueString)__.
    
    577
    -// Parses a router status entry as, described in
    
    578
    -// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
    
    579
    -// (search for "router status entry")
    
    580
    -info.routerStatusParser = function (valueString) {
    
    581
    -  let lines = utils.splitLines(valueString),
    
    582
    -    objects = [];
    
    583
    -  for (let line of lines) {
    
    584
    -    // Drop first character and grab data following it.
    
    585
    -    let myData = line.substring(2),
    
    586
    -      // Accumulate more maps with data, depending on the first character in the line.
    
    587
    -      dataFun = {
    
    588
    -        r: data =>
    
    589
    -          utils.listMapData(data, [
    
    590
    -            "nickname",
    
    591
    -            "identity",
    
    592
    -            "digest",
    
    593
    -            "publicationDate",
    
    594
    -            "publicationTime",
    
    595
    -            "IP",
    
    596
    -            "ORPort",
    
    597
    -            "DirPort",
    
    598
    -          ]),
    
    599
    -        a: data => ({ IPv6: data }),
    
    600
    -        s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
    
    601
    -        v: data => ({ version: data }),
    
    602
    -        w: data => utils.listMapData(data, []),
    
    603
    -        p: data => ({ portList: data.split(",") }),
    
    604
    -      }[line.charAt(0)];
    
    605
    -    if (dataFun !== undefined) {
    
    606
    -      objects.push(dataFun(myData));
    
    607
    -    }
    
    608
    -  }
    
    609
    -  return utils.mergeObjects(objects);
    
    610
    -};
    
    611
    -
    
    612
    -// __info.circuitStatusParser(line)__.
    
    613
    -// Parse the output of a circuit status line.
    
    614
    -info.circuitStatusParser = function (line) {
    
    615
    -  let data = utils.listMapData(line, ["id", "status", "circuit"]),
    
    616
    -    circuit = data.circuit;
    
    617
    -  // Parse out the individual circuit IDs and names.
    
    618
    -  if (circuit) {
    
    619
    -    data.circuit = circuit.split(",").map(function (x) {
    
    620
    -      return x.split(/~|=/);
    
    621
    -    });
    
    622
    -  }
    
    623
    -  return data;
    
    624
    -};
    
    625
    -
    
    626
    -// __info.streamStatusParser(line)__.
    
    627
    -// Parse the output of a stream status line.
    
    628
    -info.streamStatusParser = function (text) {
    
    629
    -  return utils.listMapData(text, [
    
    630
    -    "StreamID",
    
    631
    -    "StreamStatus",
    
    632
    -    "CircuitID",
    
    633
    -    "Target",
    
    634
    -  ]);
    
    635
    -};
    
    636
    -
    
    637
    -// TODO: fix this parsing logic to handle bridgeLine correctly
    
    638
    -// fingerprint/id is an optional parameter
    
    639
    -// __info.bridgeParser(bridgeLine)__.
    
    640
    -// Takes a single line from a `getconf bridge` result and returns
    
    641
    -// a map containing the bridge's type, address, and ID.
    
    642
    -info.bridgeParser = function (bridgeLine) {
    
    643
    -  let result = {},
    
    644
    -    tokens = bridgeLine.split(/\s+/);
    
    645
    -  // First check if we have a "vanilla" bridge:
    
    646
    -  if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
    
    647
    -    result.type = "vanilla";
    
    648
    -    [result.address, result.ID] = tokens;
    
    649
    -    // Several bridge types have a similar format:
    
    650
    -  } else {
    
    651
    -    result.type = tokens[0];
    
    652
    -    if (
    
    653
    -      [
    
    654
    -        "flashproxy",
    
    655
    -        "fte",
    
    656
    -        "meek",
    
    657
    -        "meek_lite",
    
    658
    -        "obfs3",
    
    659
    -        "obfs4",
    
    660
    -        "scramblesuit",
    
    661
    -        "snowflake",
    
    662
    -      ].includes(result.type)
    
    663
    -    ) {
    
    664
    -      [result.address, result.ID] = tokens.slice(1);
    
    665
    -    }
    
    666
    -  }
    
    667
    -  return result.type ? result : null;
    
    668
    -};
    
    669
    -
    
    670
    -// __info.parsers__.
    
    671
    -// A map of GETINFO and GETCONF keys to parsing function, which convert
    
    672
    -// result strings to _javascript_ data.
    
    673
    -info.parsers = {
    
    674
    -  "ns/id/": info.routerStatusParser,
    
    675
    -  "ip-to-country/": utils.identity,
    
    676
    -  "circuit-status": info.applyPerLine(info.circuitStatusParser),
    
    677
    -  bridge: info.bridgeParser,
    
    678
    -  // Currently unused parsers:
    
    679
    -  //  "ns/name/" : info.routerStatusParser,
    
    680
    -  //  "stream-status" : info.applyPerLine(info.streamStatusParser),
    
    681
    -  //  "version" : utils.identity,
    
    682
    -  //  "config-file" : utils.identity,
    
    683
    -};
    
    684
    -
    
    685
    -// __info.getParser(key)__.
    
    686
    -// Takes a key and determines the parser function that should be used to
    
    687
    -// convert its corresponding valueString to _javascript_ data.
    
    688
    -info.getParser = function (key) {
    
    689
    -  return (
    
    690
    -    info.parsers[key] ||
    
    691
    -    info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
    
    692
    -  );
    
    693
    -};
    
    694
    -
    
    695
    -// __info.stringToValue(string)__.
    
    696
    -// Converts a key-value string as from GETINFO or GETCONF to a value.
    
    697
    -info.stringToValue = function (string) {
    
    698
    -  // key should look something like `250+circuit-status=` or `250-circuit-status=...`
    
    699
    -  // or `250 circuit-status=...`
    
    700
    -  let matchForKey = string.match(/^250[ +-](.+?)=/),
    
    701
    -    key = matchForKey ? matchForKey[1] : null;
    
    702
    -  if (key === null) {
    
    703
    -    return null;
    
    704
    -  }
    
    705
    -  // matchResult finds a single-line result for `250-` or `250 `,
    
    706
    -  // or a multi-line one for `250+`.
    
    707
    -  let matchResult =
    
    708
    -      string.match(/^250[ -].+?=(.*)$/) ||
    
    709
    -      string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
    
    710
    -    // Retrieve the captured group (the text of the value in the key-value pair)
    
    711
    -    valueString = matchResult ? matchResult[1] : null,
    
    712
    -    // Get the parser function for the key found.
    
    713
    -    parse = info.getParser(key.toLowerCase());
    
    714
    -  if (parse === undefined) {
    
    715
    -    throw new Error("No parser found for '" + key + "'");
    
    716
    -  }
    
    717
    -  // Return value produced by the parser.
    
    718
    -  return parse(valueString);
    
    719
    -};
    
    720
    -
    
    721
    -// __info.getMultipleResponseValues(message)__.
    
    722
    -// Process multiple responses to a GETINFO or GETCONF request.
    
    723
    -info.getMultipleResponseValues = function (message) {
    
    724
    -  return info
    
    725
    -    .keyValueStringsFromMessage(message)
    
    726
    -    .map(info.stringToValue)
    
    727
    -    .filter(utils.identity);
    
    728
    -};
    
    729
    -
    
    730
    -// __info.getInfo(controlSocket, key)__.
    
    731
    -// Sends GETINFO for a single key. Returns a promise with the result.
    
    732
    -info.getInfo = function (aControlSocket, key) {
    
    733
    -  if (!utils.isString(key)) {
    
    734
    -    return utils.rejectPromise("key argument should be a string");
    
    735
    -  }
    
    736
    -  return aControlSocket
    
    737
    -    .sendCommand("getinfo " + key)
    
    738
    -    .then(response => info.getMultipleResponseValues(response)[0]);
    
    739
    -};
    
    740
    -
    
    741
    -// __info.getConf(aControlSocket, key)__.
    
    742
    -// Sends GETCONF for a single key. Returns a promise with the result.
    
    743
    -info.getConf = function (aControlSocket, key) {
    
    744
    -  // GETCONF with a single argument returns results with
    
    745
    -  // one or more lines that look like `250[- ]key=value`.
    
    746
    -  // Any GETCONF lines that contain a single keyword only are currently dropped.
    
    747
    -  // So we can use similar parsing to that for getInfo.
    
    748
    -  if (!utils.isString(key)) {
    
    749
    -    return utils.rejectPromise("key argument should be a string");
    
    750
    -  }
    
    751
    -  return aControlSocket
    
    752
    -    .sendCommand("getconf " + key)
    
    753
    -    .then(info.getMultipleResponseValues);
    
    754
    -};
    
    755
    -
    
    756
    -// ## onionAuth
    
    757
    -// A namespace for functions related to tor's ONION_CLIENT_AUTH_* commands.
    
    758
    -let onionAuth = {};
    
    759
    -
    
    760
    -onionAuth.keyInfoStringsFromMessage = utils.extractor(/^250-CLIENT\s+(.+)$/gim);
    
    761
    -
    
    762
    -onionAuth.keyInfoObjectsFromMessage = function (message) {
    
    763
    -  let keyInfoStrings = onionAuth.keyInfoStringsFromMessage(message);
    
    764
    -  return keyInfoStrings.map(infoStr =>
    
    765
    -    utils.listMapData(infoStr, ["hsAddress", "typeAndKey"])
    
    766
    -  );
    
    767
    -};
    
    768
    -
    
    769
    -// __onionAuth.viewKeys()__.
    
    770
    -// Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private keys.
    
    771
    -// Returns a promise that is fulfilled with an array of key info objects which
    
    772
    -// contain the following properties:
    
    773
    -//   hsAddress
    
    774
    -//   typeAndKey
    
    775
    -//   Flags (e.g., "Permanent")
    
    776
    -onionAuth.viewKeys = function (aControlSocket) {
    
    777
    -  let cmd = "onion_client_auth_view";
    
    778
    -  return aControlSocket
    
    779
    -    .sendCommand(cmd)
    
    780
    -    .then(onionAuth.keyInfoObjectsFromMessage);
    
    781
    -};
    
    782
    -
    
    783
    -// __onionAuth.add(controlSocket, hsAddress, b64PrivateKey, isPermanent)__.
    
    784
    -// Sends a ONION_CLIENT_AUTH_ADD command to add a private key to the
    
    785
    -// Tor configuration.
    
    786
    -onionAuth.add = function (
    
    787
    -  aControlSocket,
    
    788
    -  hsAddress,
    
    789
    -  b64PrivateKey,
    
    790
    -  isPermanent
    
    791
    -) {
    
    792
    -  if (!utils.isString(hsAddress)) {
    
    793
    -    return utils.rejectPromise("hsAddress argument should be a string");
    
    794
    -  }
    
    795
    -
    
    796
    -  if (!utils.isString(b64PrivateKey)) {
    
    797
    -    return utils.rejectPromise("b64PrivateKey argument should be a string");
    
    798
    -  }
    
    799
    -
    
    800
    -  const keyType = "x25519";
    
    801
    -  let cmd = `onion_client_auth_add ${hsAddress} ${keyType}:${b64PrivateKey}`;
    
    802
    -  if (isPermanent) {
    
    803
    -    cmd += " Flags=Permanent";
    
    804
    -  }
    
    805
    -  return aControlSocket.sendCommand(cmd);
    
    806
    -};
    
    807
    -
    
    808
    -// __onionAuth.remove(controlSocket, hsAddress)__.
    
    809
    -// Sends a ONION_CLIENT_AUTH_REMOVE command to remove a private key from the
    
    810
    -// Tor configuration.
    
    811
    -onionAuth.remove = function (aControlSocket, hsAddress) {
    
    812
    -  if (!utils.isString(hsAddress)) {
    
    813
    -    return utils.rejectPromise("hsAddress argument should be a string");
    
    814
    -  }
    
    815
    -
    
    816
    -  let cmd = `onion_client_auth_remove ${hsAddress}`;
    
    817
    -  return aControlSocket.sendCommand(cmd);
    
    818
    -};
    
    819
    -
    
    820
    -// ## event
    
    821
    -// Handlers for events
    
    822
    -
    
    823
    -let event = {};
    
    824
    -
    
    825
    -// __event.parsers__.
    
    826
    -// A map of EVENT keys to parsing functions, which convert result strings to _javascript_
    
    827
    -// data.
    
    828
    -event.parsers = {
    
    829
    -  stream: info.streamStatusParser,
    
    830
    -  // Currently unused:
    
    831
    -  // "circ" : info.circuitStatusParser,
    
    832
    -};
    
    833
    -
    
    834
    -// __event.messageToData(type, message)__.
    
    835
    -// Extract the data from an event. Note, at present
    
    836
    -// we only extract streams that look like `"650" SP...`
    
    837
    -event.messageToData = function (type, message) {
    
    838
    -  let dataText = message.match(/^650 \S+?\s(.*)/m)[1];
    
    839
    -  return dataText && type.toLowerCase() in event.parsers
    
    840
    -    ? event.parsers[type.toLowerCase()](dataText)
    
    841
    -    : null;
    
    842
    -};
    
    843
    -
    
    844
    -// __event.watchEvent(controlSocket, type, filter, onData)__.
    
    845
    -// Watches for a particular type of event. If filter(data) returns true, the event's
    
    846
    -// data is passed to the onData callback. Returns a zero arg function that
    
    847
    -// stops watching the event. Note: we only observe `"650" SP...` events
    
    848
    -// currently (no `650+...` or `650-...` events).
    
    849
    -event.watchEvent = function (controlSocket, type, filter, onData, raw = false) {
    
    850
    -  controlSocket.addNotificationCallback(
    
    851
    -    new RegExp("^650 " + type),
    
    852
    -    function (message) {
    
    853
    -      let data = event.messageToData(type, message);
    
    854
    -      if (filter === null || filter(data)) {
    
    855
    -        if (raw || !data) {
    
    856
    -          onData(message);
    
    857
    -          return;
    
    858
    -        }
    
    859
    -        onData(data);
    
    860
    -      }
    
    861
    -    }
    
    862
    -  );
    
    863
    -};
    
    864
    -
    
    865
    -// ## tor
    
    866
    -// Things related to the main controller.
    
    867
    -let tor = {};
    
    868
    -
    
    869
    -// __tor.controllerCache__.
    
    870
    -// A map from "unix:socketpath" or "host:port" to controller objects. Prevents
    
    871
    -// redundant instantiation of control sockets.
    
    872
    -tor.controllerCache = new Map();
    
    873
    -
    
    874
    -// __tor.controller(ipcFile, host, port, password)__.
    
    875
    -// Creates a tor controller at the given ipcFile or host and port, with the
    
    876
    -// given password.
    
    877
    -tor.controller = async function (ipcFile, host, port, password) {
    
    878
    -  let socket = await io.controlSocket(ipcFile, host, port, password);
    
    879
    -  return {
    
    880
    -    getInfo: key => info.getInfo(socket, key),
    
    881
    -    getConf: key => info.getConf(socket, key),
    
    882
    -    onionAuthViewKeys: () => onionAuth.viewKeys(socket),
    
    883
    -    onionAuthAdd: (hsAddress, b64PrivateKey, isPermanent) =>
    
    884
    -      onionAuth.add(socket, hsAddress, b64PrivateKey, isPermanent),
    
    885
    -    onionAuthRemove: hsAddress => onionAuth.remove(socket, hsAddress),
    
    886
    -    watchEvent: (type, filter, onData, raw = false) => {
    
    887
    -      event.watchEvent(socket, type, filter, onData, raw);
    
    888
    -    },
    
    889
    -    isOpen: () => socket.isOpen(),
    
    890
    -    close: () => {
    
    891
    -      socket.close();
    
    892
    -    },
    
    893
    -    sendCommand: cmd => socket.sendCommand(cmd),
    
    894
    -  };
    
    895
    -};
    
    896
    -
    
    897
    -// ## Export
    
    898
    -
    
    899
    -let controlPortInfo = {};
    
    900
    -
    
    901
    -// __configureControlPortModule(ipcFile, host, port, password)__.
    
    902
    -// Sets Tor control port connection parameters to be used in future calls to
    
    903
    -// the controller() function. Example:
    
    904
    -//     configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
    
    905
    -var configureControlPortModule = function (ipcFile, host, port, password) {
    
    906
    -  controlPortInfo.ipcFile = ipcFile;
    
    907
    -  controlPortInfo.host = host;
    
    908
    -  controlPortInfo.port = port || 9151;
    
    909
    -  controlPortInfo.password = password;
    
    910
    -};
    
    911
    -
    
    912
    -// __controller(avoidCache)__.
    
    913
    -// Instantiates and returns a controller object that is connected and
    
    914
    -// authenticated to a Tor ControlPort using the connection parameters
    
    915
    -// provided in the most recent call to configureControlPortModule(), if
    
    916
    -// the controller doesn't yet exist. Otherwise returns the existing
    
    917
    -// controller to the given ipcFile or host:port. Throws on error.
    
    918
    -//
    
    919
    -// Example:
    
    920
    -//
    
    921
    -//     // Get a new controller
    
    922
    -//     const avoidCache = true;
    
    923
    -//     let c = controller(avoidCache);
    
    924
    -//     // Send command and receive `250` reply or error message in a promise:
    
    925
    -//     let replyPromise = c.getInfo("ip-to-country/16.16.16.16");
    
    926
    -//     // Close the controller permanently
    
    927
    -//     c.close();
    
    928
    -var controller = async function (avoidCache) {
    
    929
    -  if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
    
    930
    -    throw new Error("Please call configureControlPortModule first");
    
    931
    -  }
    
    932
    -
    
    933
    -  const dest = controlPortInfo.ipcFile
    
    934
    -    ? `unix:${controlPortInfo.ipcFile.path}`
    
    935
    -    : `${controlPortInfo.host}:${controlPortInfo.port}`;
    
    936
    -
    
    937
    -  // constructor shorthand
    
    938
    -  const newTorController = async () => {
    
    939
    -    return tor.controller(
    
    940
    -      controlPortInfo.ipcFile,
    
    941
    -      controlPortInfo.host,
    
    942
    -      controlPortInfo.port,
    
    943
    -      controlPortInfo.password
    
    944
    -    );
    
    945
    -  };
    
    946
    -
    
    947
    -  // avoid cache so always return a new controller
    
    948
    -  if (avoidCache) {
    
    949
    -    return newTorController();
    
    950
    -  }
    
    951
    -
    
    952
    -  // first check our cache and see if we already have one
    
    953
    -  let cachedController = tor.controllerCache.get(dest);
    
    954
    -  if (cachedController && cachedController.isOpen()) {
    
    955
    -    return cachedController;
    
    956
    -  }
    
    957
    -
    
    958
    -  // create a new one and store in the map
    
    959
    -  cachedController = await newTorController();
    
    960
    -  // overwrite the close() function to prevent consumers from closing a shared/cached controller
    
    961
    -  cachedController.close = () => {
    
    962
    -    throw new Error("May not close cached Tor Controller as it may be in use");
    
    963
    -  };
    
    964
    -
    
    965
    -  tor.controllerCache.set(dest, cachedController);
    
    966
    -  return cachedController;
    
    967
    -};
    
    968
    -
    
    969
    -// __wait_for_controller(avoidCache)
    
    970
    -// Same as controller() function, but explicitly waits until there is a tor daemon
    
    971
    -// to connect to (either launched by tor-launcher, or if we have an existing system
    
    972
    -// tor daemon)
    
    973
    -var wait_for_controller = function (avoidCache) {
    
    974
    -  // if tor process is running (either ours or system) immediately return controller
    
    975
    -  if (!TorMonitorService.ownsTorDaemon || TorMonitorService.isRunning) {
    
    976
    -    return controller(avoidCache);
    
    977
    -  }
    
    978
    -
    
    979
    -  // otherwise we must wait for tor to finish launching before resolving
    
    980
    -  return new Promise((resolve, reject) => {
    
    981
    -    let observer = {
    
    982
    -      observe: async (subject, topic, data) => {
    
    983
    -        if (topic === TorTopics.ProcessIsReady) {
    
    984
    -          try {
    
    985
    -            resolve(await controller(avoidCache));
    
    986
    -          } catch (err) {
    
    987
    -            reject(err);
    
    988
    -          }
    
    989
    -          Services.obs.removeObserver(observer, TorTopics.ProcessIsReady);
    
    990
    -        }
    
    991
    -      },
    
    992
    -    };
    
    993
    -    Services.obs.addObserver(observer, TorTopics.ProcessIsReady);
    
    994
    -  });
    
    995
    -};
    
    996
    -
    
    997
    -// Export functions for external use.
    
    998
    -var EXPORTED_SYMBOLS = [
    
    999
    -  "configureControlPortModule",
    
    1000
    -  "controller",
    
    1001
    -  "wait_for_controller",
    
    1002
    -];

  • toolkit/torbutton/modules/utils.js deleted
    1
    -// # Utils.js
    
    2
    -// Various helpful utility functions.
    
    3
    -
    
    4
    -// ### Import Mozilla Services
    
    5
    -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
    
    6
    -
    
    7
    -// ## Pref utils
    
    8
    -
    
    9
    -// __prefs__. A shortcut to Mozilla Services.prefs.
    
    10
    -let prefs = Services.prefs;
    
    11
    -
    
    12
    -// __getPrefValue(prefName)__
    
    13
    -// Returns the current value of a preference, regardless of its type.
    
    14
    -var getPrefValue = function (prefName) {
    
    15
    -  switch (prefs.getPrefType(prefName)) {
    
    16
    -    case prefs.PREF_BOOL:
    
    17
    -      return prefs.getBoolPref(prefName);
    
    18
    -    case prefs.PREF_INT:
    
    19
    -      return prefs.getIntPref(prefName);
    
    20
    -    case prefs.PREF_STRING:
    
    21
    -      return prefs.getCharPref(prefName);
    
    22
    -    default:
    
    23
    -      return null;
    
    24
    -  }
    
    25
    -};
    
    26
    -
    
    27
    -// __bindPref(prefName, prefHandler, init)__
    
    28
    -// Applies prefHandler whenever the value of the pref changes.
    
    29
    -// If init is true, applies prefHandler to the current value.
    
    30
    -// Returns a zero-arg function that unbinds the pref.
    
    31
    -var bindPref = function (prefName, prefHandler, init = false) {
    
    32
    -  let update = () => {
    
    33
    -      prefHandler(getPrefValue(prefName));
    
    34
    -    },
    
    35
    -    observer = {
    
    36
    -      observe(subject, topic, data) {
    
    37
    -        if (data === prefName) {
    
    38
    -          update();
    
    39
    -        }
    
    40
    -      },
    
    41
    -    };
    
    42
    -  prefs.addObserver(prefName, observer);
    
    43
    -  if (init) {
    
    44
    -    update();
    
    45
    -  }
    
    46
    -  return () => {
    
    47
    -    prefs.removeObserver(prefName, observer);
    
    48
    -  };
    
    49
    -};
    
    50
    -
    
    51
    -// __bindPrefAndInit(prefName, prefHandler)__
    
    52
    -// Applies prefHandler to the current value of pref specified by prefName.
    
    53
    -// Re-applies prefHandler whenever the value of the pref changes.
    
    54
    -// Returns a zero-arg function that unbinds the pref.
    
    55
    -var bindPrefAndInit = (prefName, prefHandler) =>
    
    56
    -  bindPref(prefName, prefHandler, true);
    
    57
    -
    
    58
    -// ## Observers
    
    59
    -
    
    60
    -// __observe(topic, callback)__.
    
    61
    -// Observe the given topic. When notification of that topic
    
    62
    -// occurs, calls callback(subject, data). Returns a zero-arg
    
    63
    -// function that stops observing.
    
    64
    -var observe = function (topic, callback) {
    
    65
    -  let observer = {
    
    66
    -    observe(aSubject, aTopic, aData) {
    
    67
    -      if (topic === aTopic) {
    
    68
    -        callback(aSubject, aData);
    
    69
    -      }
    
    70
    -    },
    
    71
    -  };
    
    72
    -  Services.obs.addObserver(observer, topic);
    
    73
    -  return () => Services.obs.removeObserver(observer, topic);
    
    74
    -};
    
    75
    -
    
    76
    -// ## Environment variables
    
    77
    -
    
    78
    -// __getEnv(name)__.
    
    79
    -// Reads the environment variable of the given name.
    
    80
    -var getEnv = function (name) {
    
    81
    -  return Services.env.exists(name) ? Services.env.get(name) : undefined;
    
    82
    -};
    
    83
    -
    
    84
    -// __getLocale
    
    85
    -// Returns the app locale to be used in tor-related urls.
    
    86
    -var getLocale = function () {
    
    87
    -  const locale = Services.locale.appLocaleAsBCP47;
    
    88
    -  if (locale === "ja-JP-macos") {
    
    89
    -    // We don't want to distinguish the mac locale.
    
    90
    -    return "ja";
    
    91
    -  }
    
    92
    -  return locale;
    
    93
    -};
    
    94
    -
    
    95
    -// ## Windows
    
    96
    -
    
    97
    -// __dialogsByName__.
    
    98
    -// Map of window names to dialogs.
    
    99
    -let dialogsByName = {};
    
    100
    -
    
    101
    -// __showDialog(parent, url, name, features, arg1, arg2, ...)__.
    
    102
    -// Like window.openDialog, but if the window is already
    
    103
    -// open, just focuses it instead of opening a new one.
    
    104
    -var showDialog = function (parent, url, name, features) {
    
    105
    -  let existingDialog = dialogsByName[name];
    
    106
    -  if (existingDialog && !existingDialog.closed) {
    
    107
    -    existingDialog.focus();
    
    108
    -    return existingDialog;
    
    109
    -  }
    
    110
    -  let newDialog = parent.openDialog.apply(parent, Array.slice(arguments, 1));
    
    111
    -  dialogsByName[name] = newDialog;
    
    112
    -  return newDialog;
    
    113
    -};
    
    114
    -
    
    115
    -// ## Tor control protocol utility functions
    
    116
    -
    
    117
    -let _torControl = {
    
    118
    -  // Unescape Tor Control string aStr (removing surrounding "" and \ escapes).
    
    119
    -  // Based on Vidalia's src/common/stringutil.cpp:string_unescape().
    
    120
    -  // Returns the unescaped string. Throws upon failure.
    
    121
    -  // Within Tor Launcher, the file components/tl-protocol.js also contains a
    
    122
    -  // copy of _strUnescape().
    
    123
    -  _strUnescape(aStr) {
    
    124
    -    if (!aStr) {
    
    125
    -      return aStr;
    
    126
    -    }
    
    127
    -
    
    128
    -    var len = aStr.length;
    
    129
    -    if (len < 2 || '"' != aStr.charAt(0) || '"' != aStr.charAt(len - 1)) {
    
    130
    -      return aStr;
    
    131
    -    }
    
    132
    -
    
    133
    -    const kHexRE = /[0-9A-Fa-f]{2}/;
    
    134
    -    const kOctalRE = /[0-7]{3}/;
    
    135
    -    var rv = "";
    
    136
    -    var i = 1;
    
    137
    -    var lastCharIndex = len - 2;
    
    138
    -    while (i <= lastCharIndex) {
    
    139
    -      var c = aStr.charAt(i);
    
    140
    -      if ("\\" == c) {
    
    141
    -        if (++i > lastCharIndex) {
    
    142
    -          throw new Error("missing character after \\");
    
    143
    -        }
    
    144
    -
    
    145
    -        c = aStr.charAt(i);
    
    146
    -        if ("n" == c) {
    
    147
    -          rv += "\n";
    
    148
    -        } else if ("r" == c) {
    
    149
    -          rv += "\r";
    
    150
    -        } else if ("t" == c) {
    
    151
    -          rv += "\t";
    
    152
    -        } else if ("x" == c) {
    
    153
    -          if (i + 2 > lastCharIndex) {
    
    154
    -            throw new Error("not enough hex characters");
    
    155
    -          }
    
    156
    -
    
    157
    -          let s = aStr.substr(i + 1, 2);
    
    158
    -          if (!kHexRE.test(s)) {
    
    159
    -            throw new Error("invalid hex characters");
    
    160
    -          }
    
    161
    -
    
    162
    -          let val = parseInt(s, 16);
    
    163
    -          rv += String.fromCharCode(val);
    
    164
    -          i += 3;
    
    165
    -        } else if (this._isDigit(c)) {
    
    166
    -          let s = aStr.substr(i, 3);
    
    167
    -          if (i + 2 > lastCharIndex) {
    
    168
    -            throw new Error("not enough octal characters");
    
    169
    -          }
    
    170
    -
    
    171
    -          if (!kOctalRE.test(s)) {
    
    172
    -            throw new Error("invalid octal characters");
    
    173
    -          }
    
    174
    -
    
    175
    -          let val = parseInt(s, 8);
    
    176
    -          rv += String.fromCharCode(val);
    
    177
    -          i += 3;
    
    178
    -        } // "\\" and others
    
    179
    -        else {
    
    180
    -          rv += c;
    
    181
    -          ++i;
    
    182
    -        }
    
    183
    -      } else if ('"' == c) {
    
    184
    -        throw new Error('unescaped " within string');
    
    185
    -      } else {
    
    186
    -        rv += c;
    
    187
    -        ++i;
    
    188
    -      }
    
    189
    -    }
    
    190
    -
    
    191
    -    // Convert from UTF-8 to Unicode. TODO: is UTF-8 always used in protocol?
    
    192
    -    return decodeURIComponent(escape(rv));
    
    193
    -  }, // _strUnescape()
    
    194
    -
    
    195
    -  // Within Tor Launcher, the file components/tl-protocol.js also contains a
    
    196
    -  // copy of _isDigit().
    
    197
    -  _isDigit(aChar) {
    
    198
    -    const kRE = /^\d$/;
    
    199
    -    return aChar && kRE.test(aChar);
    
    200
    -  },
    
    201
    -}; // _torControl
    
    202
    -
    
    203
    -// __unescapeTorString(str, resultObj)__.
    
    204
    -// Unescape Tor Control string str (removing surrounding "" and \ escapes).
    
    205
    -// Returns the unescaped string. Throws upon failure.
    
    206
    -var unescapeTorString = function (str) {
    
    207
    -  return _torControl._strUnescape(str);
    
    208
    -};
    
    209
    -
    
    210
    -var m_tb_torlog = Cc["@torproject.org/torbutton-logger;1"].getService(
    
    211
    -  Ci.nsISupports
    
    212
    -).wrappedJSObject;
    
    213
    -
    
    214
    -var m_tb_string_bundle = torbutton_get_stringbundle();
    
    215
    -
    
    216
    -function torbutton_safelog(nLevel, sMsg, scrub) {
    
    217
    -  m_tb_torlog.safe_log(nLevel, sMsg, scrub);
    
    218
    -  return true;
    
    219
    -}
    
    220
    -
    
    221
    -function torbutton_log(nLevel, sMsg) {
    
    222
    -  m_tb_torlog.log(nLevel, sMsg);
    
    223
    -
    
    224
    -  // So we can use it in boolean expressions to determine where the
    
    225
    -  // short-circuit is..
    
    226
    -  return true;
    
    227
    -}
    
    228
    -
    
    229
    -// load localization strings
    
    230
    -function torbutton_get_stringbundle() {
    
    231
    -  var o_stringbundle = false;
    
    232
    -
    
    233
    -  try {
    
    234
    -    var oBundle = Services.strings;
    
    235
    -    o_stringbundle = oBundle.createBundle(
    
    236
    -      "chrome://torbutton/locale/torbutton.properties"
    
    237
    -    );
    
    238
    -  } catch (err) {
    
    239
    -    o_stringbundle = false;
    
    240
    -  }
    
    241
    -  if (!o_stringbundle) {
    
    242
    -    torbutton_log(5, "ERROR (init): failed to find torbutton-bundle");
    
    243
    -  }
    
    244
    -
    
    245
    -  return o_stringbundle;
    
    246
    -}
    
    247
    -
    
    248
    -function torbutton_get_property_string(propertyname) {
    
    249
    -  try {
    
    250
    -    if (!m_tb_string_bundle) {
    
    251
    -      m_tb_string_bundle = torbutton_get_stringbundle();
    
    252
    -    }
    
    253
    -
    
    254
    -    return m_tb_string_bundle.GetStringFromName(propertyname);
    
    255
    -  } catch (e) {
    
    256
    -    torbutton_log(4, "Unlocalized string " + propertyname);
    
    257
    -  }
    
    258
    -
    
    259
    -  return propertyname;
    
    260
    -}
    
    261
    -
    
    262
    -// Export utility functions for external use.
    
    263
    -let EXPORTED_SYMBOLS = [
    
    264
    -  "bindPref",
    
    265
    -  "bindPrefAndInit",
    
    266
    -  "getEnv",
    
    267
    -  "getLocale",
    
    268
    -  "getPrefValue",
    
    269
    -  "observe",
    
    270
    -  "showDialog",
    
    271
    -  "show_torbrowser_manual",
    
    272
    -  "unescapeTorString",
    
    273
    -  "torbutton_safelog",
    
    274
    -  "torbutton_log",
    
    275
    -  "torbutton_get_property_string",
    
    276
    -];

  • toolkit/torbutton/moz.build
    ... ... @@ -3,8 +3,4 @@
    3 3
     # This Source Code Form is subject to the terms of the Mozilla Public
    
    4 4
     # License, v. 2.0. If a copy of the MPL was not distributed with this
    
    5 5
     # file, You can obtain one at http://mozilla.org/MPL/2.0/.
    
    6
    -JAR_MANIFESTS += ['jar.mn']
    
    7
    -
    
    8
    -XPCOM_MANIFESTS += [
    
    9
    -    "components.conf",
    
    10
    -]
    6
    +JAR_MANIFESTS += ["jar.mn"]

  • tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
    ... ... @@ -90,11 +90,7 @@ function getGlobalScriptIncludes(scriptPath) {
    90 90
               "browser/components/screenshots/content/"
    
    91 91
             )
    
    92 92
             .replace("chrome://browser/content/", "browser/base/content/")
    
    93
    -        .replace("chrome://global/content/", "toolkit/content/")
    
    94
    -        .replace(
    
    95
    -          "chrome://torbutton/content/",
    
    96
    -          "toolkit/torbutton/chrome/content/"
    
    97
    -        );
    
    93
    +        .replace("chrome://global/content/", "toolkit/content/");
    
    98 94
     
    
    99 95
           for (let mapping of Object.getOwnPropertyNames(MAPPINGS)) {
    
    100 96
             if (sourceFile.includes(mapping)) {
    

  • _______________________________________________
    tor-commits mailing list
    tor-commits@xxxxxxxxxxxxxxxxxxxx
    https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits