tor-browser

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

BidiOverCdp.ts (5410B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js';
      8 import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
      9 import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';
     10 
     11 import type {CDPEvents, CDPSession} from '../api/CDPSession.js';
     12 import type {Connection as CdpConnection} from '../cdp/Connection.js';
     13 import {debug} from '../common/Debug.js';
     14 import {TargetCloseError} from '../common/Errors.js';
     15 import type {Handler} from '../common/EventEmitter.js';
     16 
     17 import {BidiConnection} from './Connection.js';
     18 
     19 const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
     20  debug(`bidi:${prefix}`)(args);
     21 };
     22 
     23 /**
     24 * @internal
     25 */
     26 export async function connectBidiOverCdp(
     27  cdp: CdpConnection,
     28 ): Promise<BidiConnection> {
     29  const transportBiDi = new NoOpTransport();
     30  const cdpConnectionAdapter = new CdpConnectionAdapter(cdp);
     31  const pptrTransport = {
     32    send(message: string): void {
     33      // Forwards a BiDi command sent by Puppeteer to the input of the BidiServer.
     34      transportBiDi.emitMessage(JSON.parse(message));
     35    },
     36    close(): void {
     37      bidiServer.close();
     38      cdpConnectionAdapter.close();
     39      cdp.dispose();
     40    },
     41    onmessage(_message: string): void {
     42      // The method is overridden by the Connection.
     43    },
     44  };
     45  transportBiDi.on('bidiResponse', (message: object) => {
     46    // Forwards a BiDi event sent by BidiServer to Puppeteer.
     47    pptrTransport.onmessage(JSON.stringify(message));
     48  });
     49  const pptrBiDiConnection = new BidiConnection(
     50    cdp.url(),
     51    pptrTransport,
     52    cdp.delay,
     53    cdp.timeout,
     54  );
     55  const bidiServer = await BidiMapper.BidiServer.createAndStart(
     56    transportBiDi,
     57    cdpConnectionAdapter,
     58    cdpConnectionAdapter.browserClient(),
     59    /* selfTargetId= */ '',
     60    undefined,
     61    bidiServerLogger,
     62  );
     63  return pptrBiDiConnection;
     64 }
     65 
     66 /**
     67 * Manages CDPSessions for BidiServer.
     68 * @internal
     69 */
     70 class CdpConnectionAdapter {
     71  #cdp: CdpConnection;
     72  #adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>();
     73  #browserCdpConnection: CDPClientAdapter<CdpConnection>;
     74 
     75  constructor(cdp: CdpConnection) {
     76    this.#cdp = cdp;
     77    this.#browserCdpConnection = new CDPClientAdapter(cdp);
     78  }
     79 
     80  browserClient(): CDPClientAdapter<CdpConnection> {
     81    return this.#browserCdpConnection;
     82  }
     83 
     84  getCdpClient(id: string) {
     85    const session = this.#cdp.session(id);
     86    if (!session) {
     87      throw new Error(`Unknown CDP session with id ${id}`);
     88    }
     89    if (!this.#adapters.has(session)) {
     90      const adapter = new CDPClientAdapter(
     91        session,
     92        id,
     93        this.#browserCdpConnection,
     94      );
     95      this.#adapters.set(session, adapter);
     96      return adapter;
     97    }
     98    return this.#adapters.get(session)!;
     99  }
    100 
    101  close() {
    102    this.#browserCdpConnection.close();
    103    for (const adapter of this.#adapters.values()) {
    104      adapter.close();
    105    }
    106  }
    107 }
    108 
    109 /**
    110 * Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that
    111 * BidiServer needs.
    112 *
    113 * @internal
    114 */
    115 class CDPClientAdapter<T extends CDPSession | CdpConnection>
    116  extends BidiMapper.EventEmitter<CDPEvents>
    117  implements BidiMapper.CdpClient
    118 {
    119  #closed = false;
    120  #client: T;
    121  sessionId: string | undefined = undefined;
    122  #browserClient?: BidiMapper.CdpClient;
    123 
    124  constructor(
    125    client: T,
    126    sessionId?: string,
    127    browserClient?: BidiMapper.CdpClient,
    128  ) {
    129    super();
    130    this.#client = client;
    131    this.sessionId = sessionId;
    132    this.#browserClient = browserClient;
    133    this.#client.on('*', this.#forwardMessage as Handler<any>);
    134  }
    135 
    136  browserClient(): BidiMapper.CdpClient {
    137    return this.#browserClient!;
    138  }
    139 
    140  #forwardMessage = <T extends keyof CDPEvents>(
    141    method: T,
    142    event: CDPEvents[T],
    143  ) => {
    144    this.emit(method, event);
    145  };
    146 
    147  async sendCommand<T extends keyof ProtocolMapping.Commands>(
    148    method: T,
    149    ...params: ProtocolMapping.Commands[T]['paramsType']
    150  ): Promise<ProtocolMapping.Commands[T]['returnType']> {
    151    if (this.#closed) {
    152      return;
    153    }
    154    try {
    155      return await this.#client.send(method, ...params);
    156    } catch (err) {
    157      if (this.#closed) {
    158        return;
    159      }
    160      throw err;
    161    }
    162  }
    163 
    164  close() {
    165    this.#client.off('*', this.#forwardMessage as Handler<any>);
    166    this.#closed = true;
    167  }
    168 
    169  isCloseError(error: unknown): boolean {
    170    return error instanceof TargetCloseError;
    171  }
    172 }
    173 
    174 /**
    175 * This transport is given to the BiDi server instance and allows Puppeteer
    176 * to send and receive commands to the BiDiServer.
    177 * @internal
    178 */
    179 class NoOpTransport
    180  extends BidiMapper.EventEmitter<{
    181    bidiResponse: Bidi.ChromiumBidi.Message;
    182  }>
    183  implements BidiMapper.BidiTransport
    184 {
    185  #onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void =
    186    async (_m: Bidi.ChromiumBidi.Command): Promise<void> => {
    187      return;
    188    };
    189 
    190  emitMessage(message: Bidi.ChromiumBidi.Command) {
    191    void this.#onMessage(message);
    192  }
    193 
    194  setOnMessage(
    195    onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void,
    196  ): void {
    197    this.#onMessage = onMessage;
    198  }
    199 
    200  async sendMessage(message: Bidi.ChromiumBidi.Message): Promise<void> {
    201    this.emit('bidiResponse', message);
    202  }
    203 
    204  close() {
    205    this.#onMessage = async (_m: Bidi.ChromiumBidi.Command): Promise<void> => {
    206      return;
    207    };
    208  }
    209 }