tor-browser

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

HTTPRequest.ts (9008B)


      1 /**
      2 * @license
      3 * Copyright 2020 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
      7 import type {Protocol} from 'devtools-protocol';
      8 
      9 import type {CDPSession} from '../api/CDPSession.js';
     10 import type {
     11  ContinueRequestOverrides,
     12  ResponseForRequest,
     13 } from '../api/HTTPRequest.js';
     14 import {
     15  HTTPRequest,
     16  STATUS_TEXTS,
     17  type ResourceType,
     18  handleError,
     19 } from '../api/HTTPRequest.js';
     20 import {PageEvent} from '../api/Page.js';
     21 import {UnsupportedOperation} from '../common/Errors.js';
     22 import {stringToBase64} from '../util/encoding.js';
     23 
     24 import type {Request} from './core/Request.js';
     25 import type {BidiFrame} from './Frame.js';
     26 import {BidiHTTPResponse} from './HTTPResponse.js';
     27 
     28 export const requests = new WeakMap<Request, BidiHTTPRequest>();
     29 
     30 /**
     31 * @internal
     32 */
     33 export class BidiHTTPRequest extends HTTPRequest {
     34  static from(
     35    bidiRequest: Request,
     36    frame: BidiFrame,
     37    redirect?: BidiHTTPRequest,
     38  ): BidiHTTPRequest {
     39    const request = new BidiHTTPRequest(bidiRequest, frame, redirect);
     40    request.#initialize();
     41    return request;
     42  }
     43 
     44  #redirectChain: BidiHTTPRequest[];
     45  #response: BidiHTTPResponse | null = null;
     46  override readonly id: string;
     47  readonly #frame: BidiFrame;
     48  readonly #request: Request;
     49 
     50  private constructor(
     51    request: Request,
     52    frame: BidiFrame,
     53    redirect?: BidiHTTPRequest,
     54  ) {
     55    super();
     56    requests.set(request, this);
     57 
     58    this.interception.enabled = request.isBlocked;
     59 
     60    this.#request = request;
     61    this.#frame = frame;
     62    this.#redirectChain = redirect ? redirect.#redirectChain : [];
     63    this.id = request.id;
     64  }
     65 
     66  override get client(): CDPSession {
     67    return this.#frame.client;
     68  }
     69 
     70  #initialize() {
     71    this.#request.on('redirect', request => {
     72      const httpRequest = BidiHTTPRequest.from(request, this.#frame, this);
     73      this.#redirectChain.push(this);
     74 
     75      request.once('success', () => {
     76        this.#frame
     77          .page()
     78          .trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
     79      });
     80 
     81      request.once('error', () => {
     82        this.#frame
     83          .page()
     84          .trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
     85      });
     86      void httpRequest.finalizeInterceptions();
     87    });
     88    this.#request.once('success', data => {
     89      this.#response = BidiHTTPResponse.from(
     90        data,
     91        this,
     92        this.#frame.page().browser().cdpSupported,
     93      );
     94    });
     95    this.#request.on('authenticate', this.#handleAuthentication);
     96 
     97    this.#frame.page().trustedEmitter.emit(PageEvent.Request, this);
     98 
     99    if (this.#hasInternalHeaderOverwrite) {
    100      this.interception.handlers.push(async () => {
    101        await this.continue(
    102          {
    103            headers: this.headers(),
    104          },
    105          0,
    106        );
    107      });
    108    }
    109  }
    110 
    111  override url(): string {
    112    return this.#request.url;
    113  }
    114 
    115  override resourceType(): ResourceType {
    116    if (!this.#frame.page().browser().cdpSupported) {
    117      throw new UnsupportedOperation();
    118    }
    119    return (
    120      this.#request.resourceType || 'other'
    121    ).toLowerCase() as ResourceType;
    122  }
    123 
    124  override method(): string {
    125    return this.#request.method;
    126  }
    127 
    128  override postData(): string | undefined {
    129    if (!this.#frame.page().browser().cdpSupported) {
    130      throw new UnsupportedOperation();
    131    }
    132    return this.#request.postData;
    133  }
    134 
    135  override hasPostData(): boolean {
    136    if (!this.#frame.page().browser().cdpSupported) {
    137      throw new UnsupportedOperation();
    138    }
    139    return this.#request.hasPostData;
    140  }
    141 
    142  override async fetchPostData(): Promise<string | undefined> {
    143    throw new UnsupportedOperation();
    144  }
    145 
    146  get #hasInternalHeaderOverwrite(): boolean {
    147    return Boolean(
    148      Object.keys(this.#extraHTTPHeaders).length ||
    149        Object.keys(this.#userAgentHeaders).length,
    150    );
    151  }
    152 
    153  get #extraHTTPHeaders(): Record<string, string> {
    154    return this.#frame?.page()._extraHTTPHeaders ?? {};
    155  }
    156 
    157  get #userAgentHeaders(): Record<string, string> {
    158    return this.#frame?.page()._userAgentHeaders ?? {};
    159  }
    160 
    161  override headers(): Record<string, string> {
    162    const headers: Record<string, string> = {};
    163    for (const header of this.#request.headers) {
    164      headers[header.name.toLowerCase()] = header.value.value;
    165    }
    166    return {
    167      ...headers,
    168      ...this.#extraHTTPHeaders,
    169      ...this.#userAgentHeaders,
    170    };
    171  }
    172 
    173  override response(): BidiHTTPResponse | null {
    174    return this.#response;
    175  }
    176 
    177  override failure(): {errorText: string} | null {
    178    if (this.#request.error === undefined) {
    179      return null;
    180    }
    181    return {errorText: this.#request.error};
    182  }
    183 
    184  override isNavigationRequest(): boolean {
    185    return this.#request.navigation !== undefined;
    186  }
    187 
    188  override initiator(): Protocol.Network.Initiator | undefined {
    189    return {
    190      ...this.#request.initiator,
    191      type: this.#request.initiator?.type ?? 'other',
    192    };
    193  }
    194 
    195  override redirectChain(): BidiHTTPRequest[] {
    196    return this.#redirectChain.slice();
    197  }
    198 
    199  override frame(): BidiFrame {
    200    return this.#frame;
    201  }
    202 
    203  override async continue(
    204    overrides?: ContinueRequestOverrides,
    205    priority?: number | undefined,
    206  ): Promise<void> {
    207    return await super.continue(
    208      {
    209        headers: this.#hasInternalHeaderOverwrite ? this.headers() : undefined,
    210        ...overrides,
    211      },
    212      priority,
    213    );
    214  }
    215 
    216  override async _continue(
    217    overrides: ContinueRequestOverrides = {},
    218  ): Promise<void> {
    219    const headers: Bidi.Network.Header[] = getBidiHeaders(overrides.headers);
    220    this.interception.handled = true;
    221 
    222    return await this.#request
    223      .continueRequest({
    224        url: overrides.url,
    225        method: overrides.method,
    226        body: overrides.postData
    227          ? {
    228              type: 'base64',
    229              value: stringToBase64(overrides.postData),
    230            }
    231          : undefined,
    232        headers: headers.length > 0 ? headers : undefined,
    233      })
    234      .catch(error => {
    235        this.interception.handled = false;
    236        return handleError(error);
    237      });
    238  }
    239 
    240  override async _abort(): Promise<void> {
    241    this.interception.handled = true;
    242    return await this.#request.failRequest().catch(error => {
    243      this.interception.handled = false;
    244      throw error;
    245    });
    246  }
    247 
    248  override async _respond(
    249    response: Partial<ResponseForRequest>,
    250    _priority?: number,
    251  ): Promise<void> {
    252    this.interception.handled = true;
    253 
    254    let parsedBody:
    255      | {
    256          contentLength: number;
    257          base64: string;
    258        }
    259      | undefined;
    260    if (response.body) {
    261      parsedBody = HTTPRequest.getResponse(response.body);
    262    }
    263 
    264    const headers: Bidi.Network.Header[] = getBidiHeaders(response.headers);
    265    const hasContentLength = headers.some(header => {
    266      return header.name === 'content-length';
    267    });
    268 
    269    if (response.contentType) {
    270      headers.push({
    271        name: 'content-type',
    272        value: {
    273          type: 'string',
    274          value: response.contentType,
    275        },
    276      });
    277    }
    278 
    279    if (parsedBody?.contentLength && !hasContentLength) {
    280      headers.push({
    281        name: 'content-length',
    282        value: {
    283          type: 'string',
    284          value: String(parsedBody.contentLength),
    285        },
    286      });
    287    }
    288    const status = response.status || 200;
    289 
    290    return await this.#request
    291      .provideResponse({
    292        statusCode: status,
    293        headers: headers.length > 0 ? headers : undefined,
    294        reasonPhrase: STATUS_TEXTS[status],
    295        body: parsedBody?.base64
    296          ? {
    297              type: 'base64',
    298              value: parsedBody?.base64,
    299            }
    300          : undefined,
    301      })
    302      .catch(error => {
    303        this.interception.handled = false;
    304        throw error;
    305      });
    306  }
    307 
    308  #authenticationHandled = false;
    309  #handleAuthentication = async () => {
    310    if (!this.#frame) {
    311      return;
    312    }
    313    const credentials = this.#frame.page()._credentials;
    314    if (credentials && !this.#authenticationHandled) {
    315      this.#authenticationHandled = true;
    316      void this.#request.continueWithAuth({
    317        action: 'provideCredentials',
    318        credentials: {
    319          type: 'password',
    320          username: credentials.username,
    321          password: credentials.password,
    322        },
    323      });
    324    } else {
    325      void this.#request.continueWithAuth({
    326        action: 'cancel',
    327      });
    328    }
    329  };
    330 
    331  timing(): Bidi.Network.FetchTimingInfo {
    332    return this.#request.timing();
    333  }
    334 }
    335 
    336 function getBidiHeaders(rawHeaders?: Record<string, unknown>) {
    337  const headers: Bidi.Network.Header[] = [];
    338  for (const [name, value] of Object.entries(rawHeaders ?? [])) {
    339    if (!Object.is(value, undefined)) {
    340      const values = Array.isArray(value) ? value : [value];
    341 
    342      for (const value of values) {
    343        headers.push({
    344          name: name.toLowerCase(),
    345          value: {
    346            type: 'string',
    347            value: String(value),
    348          },
    349        });
    350      }
    351    }
    352  }
    353 
    354  return headers;
    355 }