tor-browser

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

BrowserContext.ts (10391B)


      1 /**
      2 * @license
      3 * Copyright 2022 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
      8 
      9 import type {Permission} from '../api/Browser.js';
     10 import {WEB_PERMISSION_TO_PROTOCOL_PERMISSION} from '../api/Browser.js';
     11 import type {BrowserContextEvents} from '../api/BrowserContext.js';
     12 import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
     13 import {PageEvent, type Page} from '../api/Page.js';
     14 import type {Target} from '../api/Target.js';
     15 import type {Cookie, CookieData} from '../common/Cookie.js';
     16 import {EventEmitter} from '../common/EventEmitter.js';
     17 import {debugError} from '../common/util.js';
     18 import type {Viewport} from '../common/Viewport.js';
     19 import {assert} from '../util/assert.js';
     20 import {bubble} from '../util/decorators.js';
     21 
     22 import type {BidiBrowser} from './Browser.js';
     23 import type {BrowsingContext} from './core/BrowsingContext.js';
     24 import {UserContext} from './core/UserContext.js';
     25 import type {BidiFrame} from './Frame.js';
     26 import {
     27  BidiPage,
     28  bidiToPuppeteerCookie,
     29  cdpSpecificCookiePropertiesFromPuppeteerToBidi,
     30  convertCookiesExpiryCdpToBiDi,
     31  convertCookiesPartitionKeyFromPuppeteerToBiDi,
     32  convertCookiesSameSiteCdpToBiDi,
     33 } from './Page.js';
     34 import {BidiWorkerTarget} from './Target.js';
     35 import {BidiFrameTarget, BidiPageTarget} from './Target.js';
     36 import type {BidiWebWorker} from './WebWorker.js';
     37 
     38 /**
     39 * @internal
     40 */
     41 export interface BidiBrowserContextOptions {
     42  defaultViewport: Viewport | null;
     43 }
     44 
     45 /**
     46 * @internal
     47 */
     48 export class BidiBrowserContext extends BrowserContext {
     49  static from(
     50    browser: BidiBrowser,
     51    userContext: UserContext,
     52    options: BidiBrowserContextOptions,
     53  ): BidiBrowserContext {
     54    const context = new BidiBrowserContext(browser, userContext, options);
     55    context.#initialize();
     56    return context;
     57  }
     58 
     59  @bubble()
     60  accessor trustedEmitter = new EventEmitter<BrowserContextEvents>();
     61 
     62  readonly #browser: BidiBrowser;
     63  readonly #defaultViewport: Viewport | null;
     64  // This is public because of cookies.
     65  readonly userContext: UserContext;
     66  readonly #pages = new WeakMap<BrowsingContext, BidiPage>();
     67  readonly #targets = new Map<
     68    BidiPage,
     69    [
     70      BidiPageTarget,
     71      Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>,
     72    ]
     73  >();
     74 
     75  #overrides: Array<{origin: string; permission: Permission}> = [];
     76 
     77  private constructor(
     78    browser: BidiBrowser,
     79    userContext: UserContext,
     80    options: BidiBrowserContextOptions,
     81  ) {
     82    super();
     83    this.#browser = browser;
     84    this.userContext = userContext;
     85    this.#defaultViewport = options.defaultViewport;
     86  }
     87 
     88  #initialize() {
     89    // Create targets for existing browsing contexts.
     90    for (const browsingContext of this.userContext.browsingContexts) {
     91      this.#createPage(browsingContext);
     92    }
     93 
     94    this.userContext.on('browsingcontext', ({browsingContext}) => {
     95      const page = this.#createPage(browsingContext);
     96 
     97      // We need to wait for the DOMContentLoaded as the
     98      // browsingContext still may be navigating from the about:blank
     99      if (browsingContext.originalOpener) {
    100        for (const context of this.userContext.browsingContexts) {
    101          if (context.id === browsingContext.originalOpener) {
    102            this.#pages
    103              .get(context)!
    104              .trustedEmitter.emit(PageEvent.Popup, page);
    105          }
    106        }
    107      }
    108    });
    109    this.userContext.on('closed', () => {
    110      this.trustedEmitter.removeAllListeners();
    111    });
    112  }
    113 
    114  #createPage(browsingContext: BrowsingContext): BidiPage {
    115    const page = BidiPage.from(this, browsingContext);
    116    this.#pages.set(browsingContext, page);
    117    page.trustedEmitter.on(PageEvent.Close, () => {
    118      this.#pages.delete(browsingContext);
    119    });
    120 
    121    // -- Target stuff starts here --
    122    const pageTarget = new BidiPageTarget(page);
    123    const pageTargets = new Map();
    124    this.#targets.set(page, [pageTarget, pageTargets]);
    125 
    126    page.trustedEmitter.on(PageEvent.FrameAttached, frame => {
    127      const bidiFrame = frame as BidiFrame;
    128      const target = new BidiFrameTarget(bidiFrame);
    129      pageTargets.set(bidiFrame, target);
    130      this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
    131    });
    132    page.trustedEmitter.on(PageEvent.FrameNavigated, frame => {
    133      const bidiFrame = frame as BidiFrame;
    134      const target = pageTargets.get(bidiFrame);
    135      // If there is no target, then this is the page's frame.
    136      if (target === undefined) {
    137        this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget);
    138      } else {
    139        this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, target);
    140      }
    141    });
    142    page.trustedEmitter.on(PageEvent.FrameDetached, frame => {
    143      const bidiFrame = frame as BidiFrame;
    144      const target = pageTargets.get(bidiFrame);
    145      if (target === undefined) {
    146        return;
    147      }
    148      pageTargets.delete(bidiFrame);
    149      this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
    150    });
    151 
    152    page.trustedEmitter.on(PageEvent.WorkerCreated, worker => {
    153      const bidiWorker = worker as BidiWebWorker;
    154      const target = new BidiWorkerTarget(bidiWorker);
    155      pageTargets.set(bidiWorker, target);
    156      this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
    157    });
    158    page.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => {
    159      const bidiWorker = worker as BidiWebWorker;
    160      const target = pageTargets.get(bidiWorker);
    161      if (target === undefined) {
    162        return;
    163      }
    164      pageTargets.delete(worker);
    165      this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
    166    });
    167 
    168    page.trustedEmitter.on(PageEvent.Close, () => {
    169      this.#targets.delete(page);
    170      this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
    171    });
    172    this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, pageTarget);
    173    // -- Target stuff ends here --
    174 
    175    return page;
    176  }
    177 
    178  override targets(): Target[] {
    179    return [...this.#targets.values()].flatMap(([target, frames]) => {
    180      return [target, ...frames.values()];
    181    });
    182  }
    183 
    184  override async newPage(): Promise<Page> {
    185    using _guard = await this.waitForScreenshotOperations();
    186 
    187    const context = await this.userContext.createBrowsingContext(
    188      Bidi.BrowsingContext.CreateType.Tab,
    189    );
    190    const page = this.#pages.get(context)!;
    191    if (!page) {
    192      throw new Error('Page is not found');
    193    }
    194    if (this.#defaultViewport) {
    195      try {
    196        await page.setViewport(this.#defaultViewport);
    197      } catch {
    198        // No support for setViewport in Firefox.
    199      }
    200    }
    201 
    202    return page;
    203  }
    204 
    205  override async close(): Promise<void> {
    206    assert(
    207      this.userContext.id !== UserContext.DEFAULT,
    208      'Default BrowserContext cannot be closed!',
    209    );
    210 
    211    try {
    212      await this.userContext.remove();
    213    } catch (error) {
    214      debugError(error);
    215    }
    216 
    217    this.#targets.clear();
    218  }
    219 
    220  override browser(): BidiBrowser {
    221    return this.#browser;
    222  }
    223 
    224  override async pages(): Promise<BidiPage[]> {
    225    return [...this.userContext.browsingContexts].map(context => {
    226      return this.#pages.get(context)!;
    227    });
    228  }
    229 
    230  override async overridePermissions(
    231    origin: string,
    232    permissions: Permission[],
    233  ): Promise<void> {
    234    const permissionsSet = new Set(
    235      permissions.map(permission => {
    236        const protocolPermission =
    237          WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
    238        if (!protocolPermission) {
    239          throw new Error('Unknown permission: ' + permission);
    240        }
    241        return permission;
    242      }),
    243    );
    244    await Promise.all(
    245      Array.from(WEB_PERMISSION_TO_PROTOCOL_PERMISSION.keys()).map(
    246        permission => {
    247          const result = this.userContext.setPermissions(
    248            origin,
    249            {
    250              name: permission,
    251            },
    252            permissionsSet.has(permission)
    253              ? Bidi.Permissions.PermissionState.Granted
    254              : Bidi.Permissions.PermissionState.Denied,
    255          );
    256          this.#overrides.push({origin, permission});
    257          // TODO: some permissions are outdated and setting them to denied does
    258          // not work.
    259          if (!permissionsSet.has(permission)) {
    260            return result.catch(debugError);
    261          }
    262          return result;
    263        },
    264      ),
    265    );
    266  }
    267 
    268  override async clearPermissionOverrides(): Promise<void> {
    269    const promises = this.#overrides.map(({permission, origin}) => {
    270      return this.userContext
    271        .setPermissions(
    272          origin,
    273          {
    274            name: permission,
    275          },
    276          Bidi.Permissions.PermissionState.Prompt,
    277        )
    278        .catch(debugError);
    279    });
    280    this.#overrides = [];
    281    await Promise.all(promises);
    282  }
    283 
    284  override get id(): string | undefined {
    285    if (this.userContext.id === UserContext.DEFAULT) {
    286      return undefined;
    287    }
    288    return this.userContext.id;
    289  }
    290 
    291  override async cookies(): Promise<Cookie[]> {
    292    const cookies = await this.userContext.getCookies();
    293    return cookies.map(cookie => {
    294      return bidiToPuppeteerCookie(cookie, true);
    295    });
    296  }
    297 
    298  override async setCookie(...cookies: CookieData[]): Promise<void> {
    299    await Promise.all(
    300      cookies.map(async cookie => {
    301        const bidiCookie: Bidi.Storage.PartialCookie = {
    302          domain: cookie.domain,
    303          name: cookie.name,
    304          value: {
    305            type: 'string',
    306            value: cookie.value,
    307          },
    308          ...(cookie.path !== undefined ? {path: cookie.path} : {}),
    309          ...(cookie.httpOnly !== undefined ? {httpOnly: cookie.httpOnly} : {}),
    310          ...(cookie.secure !== undefined ? {secure: cookie.secure} : {}),
    311          ...(cookie.sameSite !== undefined
    312            ? {sameSite: convertCookiesSameSiteCdpToBiDi(cookie.sameSite)}
    313            : {}),
    314          ...{expiry: convertCookiesExpiryCdpToBiDi(cookie.expires)},
    315          // Chrome-specific properties.
    316          ...cdpSpecificCookiePropertiesFromPuppeteerToBidi(
    317            cookie,
    318            'sameParty',
    319            'sourceScheme',
    320            'priority',
    321            'url',
    322          ),
    323        };
    324        return await this.userContext.setCookie(
    325          bidiCookie,
    326          convertCookiesPartitionKeyFromPuppeteerToBiDi(cookie.partitionKey),
    327        );
    328      }),
    329    );
    330  }
    331 }