tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Connection.ts (5642B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
      8 
      9 import {CallbackRegistry} from '../common/CallbackRegistry.js';
     10 import type {ConnectionTransport} from '../common/ConnectionTransport.js';
     11 import {debug} from '../common/Debug.js';
     12 import type {EventsWithWildcard} from '../common/EventEmitter.js';
     13 import {EventEmitter} from '../common/EventEmitter.js';
     14 import {debugError} from '../common/util.js';
     15 import {assert} from '../util/assert.js';
     16 
     17 import {BidiCdpSession} from './CDPSession.js';
     18 import type {
     19  BidiEvents,
     20  Commands as BidiCommands,
     21  Connection,
     22 } from './core/Connection.js';
     23 
     24 const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
     25 const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
     26 
     27 /**
     28 * @internal
     29 */
     30 export interface Commands extends BidiCommands {
     31  'goog:cdp.sendCommand': {
     32    params: Bidi.Cdp.SendCommandParameters;
     33    returnType: Bidi.Cdp.SendCommandResult;
     34  };
     35  'goog:cdp.getSession': {
     36    params: Bidi.Cdp.GetSessionParameters;
     37    returnType: Bidi.Cdp.GetSessionResult;
     38  };
     39  'goog:cdp.resolveRealm': {
     40    params: Bidi.Cdp.ResolveRealmParameters;
     41    returnType: Bidi.Cdp.ResolveRealmResult;
     42  };
     43 }
     44 
     45 /**
     46 * @internal
     47 */
     48 export class BidiConnection
     49  extends EventEmitter<BidiEvents>
     50  implements Connection
     51 {
     52  #url: string;
     53  #transport: ConnectionTransport;
     54  #delay: number;
     55  #timeout = 0;
     56  #closed = false;
     57  #callbacks = new CallbackRegistry();
     58  #emitters: Array<EventEmitter<any>> = [];
     59 
     60  constructor(
     61    url: string,
     62    transport: ConnectionTransport,
     63    delay = 0,
     64    timeout?: number,
     65  ) {
     66    super();
     67    this.#url = url;
     68    this.#delay = delay;
     69    this.#timeout = timeout ?? 180_000;
     70 
     71    this.#transport = transport;
     72    this.#transport.onmessage = this.onMessage.bind(this);
     73    this.#transport.onclose = this.unbind.bind(this);
     74  }
     75 
     76  get closed(): boolean {
     77    return this.#closed;
     78  }
     79 
     80  get url(): string {
     81    return this.#url;
     82  }
     83 
     84  pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
     85    this.#emitters.push(emitter);
     86  }
     87 
     88  override emit<Key extends keyof EventsWithWildcard<BidiEvents>>(
     89    type: Key,
     90    event: EventsWithWildcard<BidiEvents>[Key],
     91  ): boolean {
     92    for (const emitter of this.#emitters) {
     93      emitter.emit(type, event);
     94    }
     95    return super.emit(type, event);
     96  }
     97 
     98  send<T extends keyof Commands>(
     99    method: T,
    100    params: Commands[T]['params'],
    101    timeout?: number,
    102  ): Promise<{result: Commands[T]['returnType']}> {
    103    assert(!this.#closed, 'Protocol error: Connection closed.');
    104 
    105    return this.#callbacks.create(method, timeout ?? this.#timeout, id => {
    106      const stringifiedMessage = JSON.stringify({
    107        id,
    108        method,
    109        params,
    110      } as Bidi.Command);
    111      debugProtocolSend(stringifiedMessage);
    112      this.#transport.send(stringifiedMessage);
    113    }) as Promise<{result: Commands[T]['returnType']}>;
    114  }
    115 
    116  /**
    117   * @internal
    118   */
    119  protected async onMessage(message: string): Promise<void> {
    120    if (this.#delay) {
    121      await new Promise(f => {
    122        return setTimeout(f, this.#delay);
    123      });
    124    }
    125    debugProtocolReceive(message);
    126    const object: Bidi.ChromiumBidi.Message = JSON.parse(message);
    127    if ('type' in object) {
    128      switch (object.type) {
    129        case 'success':
    130          this.#callbacks.resolve(object.id, object);
    131          return;
    132        case 'error':
    133          if (object.id === null) {
    134            break;
    135          }
    136          this.#callbacks.reject(
    137            object.id,
    138            createProtocolError(object),
    139            `${object.error}: ${object.message}`,
    140          );
    141          return;
    142        case 'event':
    143          if (isCdpEvent(object)) {
    144            BidiCdpSession.sessions
    145              .get(object.params.session)
    146              ?.emit(object.params.event, object.params.params);
    147            return;
    148          }
    149          // SAFETY: We know the method and parameter still match here.
    150          this.emit(
    151            object.method,
    152            object.params as BidiEvents[keyof BidiEvents],
    153          );
    154          return;
    155      }
    156    }
    157    // Even if the response in not in BiDi protocol format but `id` is provided, reject
    158    // the callback. This can happen if the endpoint supports CDP instead of BiDi.
    159    if ('id' in object) {
    160      this.#callbacks.reject(
    161        (object as {id: number}).id,
    162        `Protocol Error. Message is not in BiDi protocol format: '${message}'`,
    163        object.message,
    164      );
    165    }
    166    debugError(object);
    167  }
    168 
    169  /**
    170   * Unbinds the connection, but keeps the transport open. Useful when the transport will
    171   * be reused by other connection e.g. with different protocol.
    172   * @internal
    173   */
    174  unbind(): void {
    175    if (this.#closed) {
    176      return;
    177    }
    178    this.#closed = true;
    179    // Both may still be invoked and produce errors
    180    this.#transport.onmessage = () => {};
    181    this.#transport.onclose = () => {};
    182 
    183    this.#callbacks.clear();
    184  }
    185 
    186  /**
    187   * Unbinds the connection and closes the transport.
    188   */
    189  dispose(): void {
    190    this.unbind();
    191    this.#transport.close();
    192  }
    193 
    194  getPendingProtocolErrors(): Error[] {
    195    return this.#callbacks.getPendingProtocolErrors();
    196  }
    197 }
    198 
    199 /**
    200 * @internal
    201 */
    202 function createProtocolError(object: Bidi.ErrorResponse): string {
    203  let message = `${object.error} ${object.message}`;
    204  if (object.stacktrace) {
    205    message += ` ${object.stacktrace}`;
    206  }
    207  return message;
    208 }
    209 
    210 function isCdpEvent(event: Bidi.ChromiumBidi.Event): event is Bidi.Cdp.Event {
    211  return event.method.startsWith('goog:cdp.');
    212 }