NetworkAgent.js 6.56 KB
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule NetworkAgent
 * @flow
 */
'use strict';

const InspectorAgent = require('InspectorAgent');
const JSInspector = require('JSInspector');
const Map = require('Map');
const XMLHttpRequest = require('XMLHttpRequest');

import type EventSender from 'InspectorAgent';

type RequestId = string;

type LoaderId = string;
type FrameId = string;
type Timestamp = number;

type Headers = Object;

// We don't currently care about this
type ResourceTiming = null;

type ResourceType =
  'Document' |
  'Stylesheet' |
  'Image' |
  'Media' |
  'Font' |
  'Script' |
  'TextTrack' |
  'XHR' |
  'Fetch' |
  'EventSource' |
  'WebSocket' |
  'Manifest' |
  'Other';

type SecurityState =
  'unknown' |
  'neutral' |
  'insecure' |
  'warning' |
  'secure' |
  'info';
type BlockedReason =
  'csp' |
  'mixed-content' |
  'origin' |
  'inspector' |
  'subresource-filter' |
  'other';

type StackTrace = null;

type Initiator = {
  type: 'script' | 'other',
  stackTrace?: StackTrace,
  url?: string,
  lineNumber?: number
}

type ResourcePriority = 'VeryLow' | 'Low' | 'Medium' | 'High' | 'VeryHigh';

type Request = {
  url: string,
  method: string,
  headers: Headers,
  postData?: string,
  mixedContentType?: 'blockable' | 'optionally-blockable' | 'none',
  initialPriority: ResourcePriority,
};

type Response = {
  url: string,
  status: number,
  statusText: string,
  headers: Headers,
  headersText?: string,
  mimeType: string,
  requestHeaders?: Headers,
  requestHeadersText?: string,
  connectionReused: boolean,
  connectionId: number,
  fromDiskCache?: boolean,
  encodedDataLength: number,
  timing?: ResourceTiming,
  securityState: SecurityState,
};

type RequestWillBeSentEvent = {
  requestId: RequestId,
  frameId: FrameId,
  loaderId: LoaderId,
  documentURL: string,
  request: Request,
  timestamp: Timestamp,
  initiator: Initiator,
  redirectResponse?: Response,
  // This is supposed to be optional but the inspector crashes without it,
  // see https://bugs.chromium.org/p/chromium/issues/detail?id=653138
  type: ResourceType,
};

type ResponseReceivedEvent = {
  requestId: RequestId,
  frameId: FrameId,
  loaderId: LoaderId,
  timestamp: Timestamp,
  type: ResourceType,
  response: Response,
};

type DataReceived = {
  requestId: RequestId,
  timestamp: Timestamp,
  dataLength: number,
  encodedDataLength: number,
};

type LoadingFinishedEvent = {
  requestId: RequestId,
  timestamp: Timestamp,
  encodedDataLength: number,
};

type LoadingFailedEvent = {
  requestId: RequestId,
  timestamp: Timestamp,
  type: ResourceType,
  errorText: string,
  canceled?: boolean,
  blockedReason?: BlockedReason,
};

class Interceptor {
  _agent: NetworkAgent;
  _requests: Map<string, string>;

  constructor(agent: NetworkAgent) {
    this._agent = agent;
    this._requests = new Map();
  }

  getData(requestId: string): ?string {
    return this._requests.get(requestId);
  }

  requestSent(
    id: number,
    url: string,
    method: string,
    headers: Object) {
    const requestId = String(id);
    this._requests.set(requestId, '');

    const request: Request = {
      url,
      method,
      headers,
      initialPriority: 'Medium',
    };
    const event: RequestWillBeSentEvent = {
      requestId,
      documentURL: '',
      frameId: '1',
      loaderId: '1',
      request,
      timestamp: JSInspector.getTimestamp(),
      initiator: {
        // TODO(blom): Get stack trace
        // If type is 'script' the inspector will try to execute
        // `stack.callFrames[0]`
        type: 'other',
      },
      type: 'Other',
    };
    this._agent.sendEvent('requestWillBeSent', event);
  }

  responseReceived(
    id: number,
    url: string,
    status: number,
    headers: Object) {
    const requestId = String(id);
    const response: Response = {
      url,
      status,
      statusText: String(status),
      headers,
      // TODO(blom) refined headers, can we get this?
      requestHeaders: {},
      mimeType: this._getMimeType(headers),
      connectionReused: false,
      connectionId: -1,
      encodedDataLength: 0,
      securityState: 'unknown',
    };

    const event: ResponseReceivedEvent = {
      requestId,
      frameId: '1',
      loaderId: '1',
      timestamp: JSInspector.getTimestamp(),
      type: 'Other',
      response,
    };
    this._agent.sendEvent('responseReceived', event);
  }

  dataReceived(
    id: number,
    data: string) {
    const requestId = String(id);
    const existingData = this._requests.get(requestId) || '';
    this._requests.set(requestId, existingData.concat(data));
    const event: DataReceived = {
      requestId,
      timestamp: JSInspector.getTimestamp(),
      dataLength: data.length,
      encodedDataLength: data.length,
    };
    this._agent.sendEvent('dataReceived', event);
  }

  loadingFinished(
    id: number,
    encodedDataLength: number) {
    const event: LoadingFinishedEvent = {
      requestId: String(id),
      timestamp: JSInspector.getTimestamp(),
      encodedDataLength: encodedDataLength,
    };
    this._agent.sendEvent('loadingFinished', event);
  }

  loadingFailed(
      id: number,
      error: string) {
    const event: LoadingFailedEvent = {
      requestId: String(id),
      timestamp: JSInspector.getTimestamp(),
      type: 'Other',
      errorText: error,
    };
    this._agent.sendEvent('loadingFailed', event);
  }

  _getMimeType(headers: Object): string {
    const contentType = headers['Content-Type'] || '';
    return contentType.split(';')[0];
  }
}

type EnableArgs = {
  maxResourceBufferSize?: number,
  maxTotalBufferSize?: number
};

class NetworkAgent extends InspectorAgent {
  static DOMAIN = 'Network';

  _sendEvent: EventSender;
  _interceptor: ?Interceptor;

  enable({ maxResourceBufferSize, maxTotalBufferSize }: EnableArgs) {
    this._interceptor = new Interceptor(this);
    XMLHttpRequest.setInterceptor(this._interceptor);
  }

  disable() {
    XMLHttpRequest.setInterceptor(null);
    this._interceptor = null;
  }

  getResponseBody({requestId}: {requestId: RequestId})
      : {body: ?string, base64Encoded: boolean} {
    return {body: this.interceptor().getData(requestId), base64Encoded: false};
  }

  interceptor(): Interceptor {
    if (this._interceptor) {
      return this._interceptor;
    } else {
      throw Error('_interceptor can not be null');
    }

  }
}

module.exports = NetworkAgent;