Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  NetworkObserver.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CommonUtils: "resource://services-common/utils.sys.mjs",
  EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",

  ChannelEventSinkFactory:
    "chrome://remote/content/cdp/observers/ChannelEventSink.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gActivityDistributor",
  "@mozilla.org/network/http-activity-distributor;1",
  "nsIHttpActivityDistributor"
);

const CC = Components.Constructor;

ChromeUtils.defineLazyGetter(lazy, "BinaryInputStream", () => {
  return CC(
    "@mozilla.org/binaryinputstream;1",
    "nsIBinaryInputStream",
    "setInputStream"
  );
});

ChromeUtils.defineLazyGetter(lazy, "BinaryOutputStream", () => {
  return CC(
    "@mozilla.org/binaryoutputstream;1",
    "nsIBinaryOutputStream",
    "setOutputStream"
  );
});

ChromeUtils.defineLazyGetter(lazy, "StorageStream", () => {
  return CC("@mozilla.org/storagestream;1", "nsIStorageStream", "init");
});

// Cap response storage with 100Mb per tracked tab.
const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024;

export class NetworkObserver {
  constructor() {
    lazy.EventEmitter.decorate(this);
    this._browserSessionCount = new Map();
    lazy.gActivityDistributor.addObserver(this);
    lazy.ChannelEventSinkFactory.getService().registerCollector(this);

    this._redirectMap = new Map();

    // Request interception state.
    this._browserSuspendedChannels = new Map();
    this._extraHTTPHeaders = new Map();
    this._browserResponseStorages = new Map();

    this._onRequest = this._onRequest.bind(this);
    this._onExamineResponse = this._onResponse.bind(
      this,
      false /* fromCache */
    );
    this._onCachedResponse = this._onResponse.bind(this, true /* fromCache */);
  }

  dispose() {
    lazy.gActivityDistributor.removeObserver(this);
    lazy.ChannelEventSinkFactory.getService().unregisterCollector(this);

    Services.obs.removeObserver(this._onRequest, "http-on-modify-request");
    Services.obs.removeObserver(
      this._onExamineResponse,
      "http-on-examine-response"
    );
    Services.obs.removeObserver(
      this._onCachedResponse,
      "http-on-examine-cached-response"
    );
    Services.obs.removeObserver(
      this._onCachedResponse,
      "http-on-examine-merged-response"
    );
  }

  setExtraHTTPHeaders(browser, headers) {
    if (!headers) {
      this._extraHTTPHeaders.delete(browser);
    } else {
      this._extraHTTPHeaders.set(browser, headers);
    }
  }

  enableRequestInterception(browser) {
    if (!this._browserSuspendedChannels.has(browser)) {
      this._browserSuspendedChannels.set(browser, new Map());
    }
  }

  disableRequestInterception(browser) {
    const suspendedChannels = this._browserSuspendedChannels.get(browser);
    if (!suspendedChannels) {
      return;
    }
    this._browserSuspendedChannels.delete(browser);
    for (const channel of suspendedChannels.values()) {
      channel.resume();
    }
  }

  resumeSuspendedRequest(browser, requestId, headers) {
    const suspendedChannels = this._browserSuspendedChannels.get(browser);
    if (!suspendedChannels) {
      throw new Error(`Request interception is not enabled`);
    }
    const httpChannel = suspendedChannels.get(requestId);
    if (!httpChannel) {
      throw new Error(`Cannot find request "${requestId}"`);
    }
    if (headers) {
      // 1. Clear all previous headers.
      for (const header of requestHeaders(httpChannel)) {
        httpChannel.setRequestHeader(header.name, "", false /* merge */);
      }
      // 2. Set new headers.
      for (const header of headers) {
        httpChannel.setRequestHeader(
          header.name,
          header.value,
          false /* merge */
        );
      }
    }
    suspendedChannels.delete(requestId);
    httpChannel.resume();
  }

  getResponseBody(browser, requestId) {
    const responseStorage = this._browserResponseStorages.get(browser);
    if (!responseStorage) {
      throw new Error("Responses are not tracked for the given browser");
    }
    return responseStorage.getBase64EncodedResponse(requestId);
  }

  abortSuspendedRequest(browser, aRequestId) {
    const suspendedChannels = this._browserSuspendedChannels.get(browser);
    if (!suspendedChannels) {
      throw new Error(`Request interception is not enabled`);
    }
    const httpChannel = suspendedChannels.get(aRequestId);
    if (!httpChannel) {
      throw new Error(`Cannot find request "${aRequestId}"`);
    }
    suspendedChannels.delete(aRequestId);
    httpChannel.cancel(Cr.NS_ERROR_FAILURE);
    httpChannel.resume();
    this.emit("requestfailed", httpChannel, {
      requestId: requestId(httpChannel),
      errorCode: getNetworkErrorStatusText(httpChannel.status),
    });
  }

  _onChannelRedirect(oldChannel, newChannel) {
    // We can be called with any nsIChannel, but are interested only in HTTP channels
    try {
      oldChannel.QueryInterface(Ci.nsIHttpChannel);
      newChannel.QueryInterface(Ci.nsIHttpChannel);
    } catch (ex) {
      return;
    }

    const httpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
    const loadContext = getLoadContext(httpChannel);
    if (
      !loadContext ||
      !this._browserSessionCount.has(loadContext.topFrameElement)
    ) {
      return;
    }
    this._redirectMap.set(newChannel, oldChannel);
  }

  _onRequest(channel) {
    const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
    const loadContext = getLoadContext(httpChannel);
    const browser = loadContext?.topFrameElement;
    if (!loadContext || !this._browserSessionCount.has(browser)) {
      return;
    }

    const extraHeaders = this._extraHTTPHeaders.get(browser);
    if (extraHeaders) {
      for (const header of extraHeaders) {
        httpChannel.setRequestHeader(
          header.name,
          header.value,
          false /* merge */
        );
      }
    }
    const causeType = httpChannel.loadInfo
      ? httpChannel.loadInfo.externalContentPolicyType
      : Ci.nsIContentPolicy.TYPE_OTHER;

    const suspendedChannels = this._browserSuspendedChannels.get(browser);
    if (suspendedChannels) {
      httpChannel.suspend();
      suspendedChannels.set(requestId(httpChannel), httpChannel);
    }

    const oldChannel = this._redirectMap.get(httpChannel);
    this._redirectMap.delete(httpChannel);

    // Install response body hooks.
    new ResponseBodyListener(this, browser, httpChannel);

    this.emit("request", httpChannel, {
      url: httpChannel.URI.spec,
      suspended: suspendedChannels ? true : undefined,
      requestId: requestId(httpChannel),
      redirectedFrom: oldChannel ? requestId(oldChannel) : undefined,
      postData: readRequestPostData(httpChannel),
      headers: requestHeaders(httpChannel),
      method: httpChannel.requestMethod,
      isNavigationRequest: httpChannel.isMainDocumentChannel,
      cause: causeType,
      causeString: causeTypeToString(causeType),
      frameId: this.frameId(httpChannel),
      // clients expect loaderId == requestId for document navigation
      loaderId: [
        Ci.nsIContentPolicy.TYPE_DOCUMENT,
        Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
      ].includes(causeType)
        ? requestId(httpChannel)
        : undefined,
    });
  }

  _onResponse(fromCache, httpChannel) {
    const loadContext = getLoadContext(httpChannel);
    if (
      !loadContext ||
      !this._browserSessionCount.has(loadContext.topFrameElement)
    ) {
      return;
    }
    httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
    const causeType = httpChannel.loadInfo
      ? httpChannel.loadInfo.externalContentPolicyType
      : Ci.nsIContentPolicy.TYPE_OTHER;
    let remoteIPAddress;
    let remotePort;
    try {
      remoteIPAddress = httpChannel.remoteAddress;
      remotePort = httpChannel.remotePort;
    } catch (e) {
      // remoteAddress is not defined for cached requests.
    }

    this.emit("response", httpChannel, {
      requestId: requestId(httpChannel),
      securityDetails: getSecurityDetails(httpChannel),
      fromCache,
      headers: responseHeaders(httpChannel),
      requestHeaders: requestHeaders(httpChannel),
      remoteIPAddress,
      remotePort,
      status: httpChannel.responseStatus,
      statusText: httpChannel.responseStatusText,
      cause: causeType,
      causeString: causeTypeToString(causeType),
      frameId: this.frameId(httpChannel),
      // clients expect loaderId == requestId for document navigation
      loaderId: [
        Ci.nsIContentPolicy.TYPE_DOCUMENT,
        Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
      ].includes(causeType)
        ? requestId(httpChannel)
        : undefined,
    });
  }

  _onResponseFinished(browser, httpChannel, body) {
    const responseStorage = this._browserResponseStorages.get(browser);
    if (!responseStorage) {
      return;
    }
    responseStorage.addResponseBody(httpChannel, body);
    this.emit("requestfinished", httpChannel, {
      requestId: requestId(httpChannel),
      errorCode: getNetworkErrorStatusText(httpChannel.status),
    });
  }

  isActive(browser) {
    return !!this._browserSessionCount.get(browser);
  }

  startTrackingBrowserNetwork(browser) {
    const value = this._browserSessionCount.get(browser) || 0;
    this._browserSessionCount.set(browser, value + 1);
    if (value === 0) {
      Services.obs.addObserver(this._onRequest, "http-on-modify-request");
      Services.obs.addObserver(
        this._onExamineResponse,
        "http-on-examine-response"
      );
      Services.obs.addObserver(
        this._onCachedResponse,
        "http-on-examine-cached-response"
      );
      Services.obs.addObserver(
        this._onCachedResponse,
        "http-on-examine-merged-response"
      );
      this._browserResponseStorages.set(
        browser,
        new ResponseStorage(
          MAX_RESPONSE_STORAGE_SIZE,
          MAX_RESPONSE_STORAGE_SIZE / 10
        )
      );
    }
    return () => this.stopTrackingBrowserNetwork(browser);
  }

  stopTrackingBrowserNetwork(browser) {
    const value = this._browserSessionCount.get(browser);
    if (value) {
      this._browserSessionCount.set(browser, value - 1);
    } else {
      this._browserSessionCount.delete(browser);
      this._browserResponseStorages.delete(browser);
      this.dispose();
    }
  }

  /**
   * Returns the frameId of the current httpChannel.
   */
  frameId(httpChannel) {
    const loadInfo = httpChannel.loadInfo;
    return loadInfo.frameBrowsingContext?.id || loadInfo.browsingContext.id;
  }
}

const protocolVersionNames = {
  [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: "TLS 1",
  [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: "TLS 1.1",
  [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: "TLS 1.2",
  [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: "TLS 1.3",
};

function getSecurityDetails(httpChannel) {
  const securityInfo = httpChannel.securityInfo;
  if (!securityInfo) {
    return null;
  }
  if (!securityInfo.serverCert) {
    return null;
  }
  return {
    protocol: protocolVersionNames[securityInfo.protocolVersion] || "<unknown>",
    subjectName: securityInfo.serverCert.commonName,
    issuer: securityInfo.serverCert.issuerCommonName,
    // Convert to seconds.
    validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000,
    validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000,
  };
}

function readRequestPostData(httpChannel) {
  if (!(httpChannel instanceof Ci.nsIUploadChannel)) {
    return undefined;
  }
  const iStream = httpChannel.uploadStream;
  if (!iStream) {
    return undefined;
  }
  const isSeekableStream = iStream instanceof Ci.nsISeekableStream;

  let prevOffset;
  if (isSeekableStream) {
    prevOffset = iStream.tell();
    iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
  }

  // Read data from the stream.
  let text;
  try {
    text = lazy.NetUtil.readInputStreamToString(iStream, iStream.available());
    const converter = Cc[
      "@mozilla.org/intl/scriptableunicodeconverter"
    ].createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";
    text = converter.ConvertToUnicode(text);
  } catch (err) {
    text = undefined;
  }

  // Seek locks the file, so seek to the beginning only if necko hasn"t
  // read it yet, since necko doesn"t seek to 0 before reading (at lest
  // not till 459384 is fixed).
  if (isSeekableStream && prevOffset == 0) {
    iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
  }
  return text;
}

function getLoadContext(httpChannel) {
  let loadContext = null;
  try {
    if (httpChannel.notificationCallbacks) {
      loadContext = httpChannel.notificationCallbacks.getInterface(
        Ci.nsILoadContext
      );
    }
  } catch (e) {}
  try {
    if (!loadContext && httpChannel.loadGroup) {
      loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(
        Ci.nsILoadContext
      );
    }
  } catch (e) {}
  return loadContext;
}

function requestId(httpChannel) {
  return String(httpChannel.channelId);
}

function requestHeaders(httpChannel) {
  const headers = [];
  httpChannel.visitRequestHeaders({
    visitHeader: (name, value) => headers.push({ name, value }),
  });
  return headers;
}

function responseHeaders(httpChannel) {
  const headers = [];
  httpChannel.visitResponseHeaders({
    visitHeader: (name, value) => headers.push({ name, value }),
  });
  return headers;
}

function causeTypeToString(causeType) {
  for (let key in Ci.nsIContentPolicy) {
    if (Ci.nsIContentPolicy[key] === causeType) {
      return key;
    }
  }
  return "TYPE_OTHER";
}

class ResponseStorage {
  constructor(maxTotalSize, maxResponseSize) {
    this._totalSize = 0;
    this._maxResponseSize = maxResponseSize;
    this._maxTotalSize = maxTotalSize;
    this._responses = new Map();
  }

  addResponseBody(httpChannel, body) {
    if (body.length > this._maxResponseSize) {
      this._responses.set(requestId, {
        evicted: true,
        body: "",
      });
      return;
    }
    let encodings = [];
    if (
      httpChannel instanceof Ci.nsIEncodedChannel &&
      httpChannel.contentEncodings &&
      !httpChannel.applyConversion
    ) {
      const encodingHeader = httpChannel.getResponseHeader("Content-Encoding");
      encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
    }
    this._responses.set(requestId(httpChannel), { body, encodings });
    this._totalSize += body.length;
    if (this._totalSize > this._maxTotalSize) {
      for (let [, response] of this._responses) {
        this._totalSize -= response.body.length;
        response.body = "";
        response.evicted = true;
        if (this._totalSize < this._maxTotalSize) {
          break;
        }
      }
    }
  }

  getBase64EncodedResponse(requestId) {
    const response = this._responses.get(requestId);
    if (!response) {
      throw new Error(`Request "${requestId}" is not found`);
    }
    if (response.evicted) {
      return { base64body: "", evicted: true };
    }
    let result = response.body;
    if (response.encodings && response.encodings.length) {
      for (const encoding of response.encodings) {
        result = lazy.CommonUtils.convertString(
          result,
          encoding,
          "uncompressed"
        );
      }
    }
    return { base64body: btoa(result) };
  }
}

class ResponseBodyListener {
  constructor(networkObserver, browser, httpChannel) {
    this._networkObserver = networkObserver;
    this._browser = browser;
    this._httpChannel = httpChannel;
    this._chunks = [];
    this.QueryInterface = ChromeUtils.generateQI(["nsIStreamListener"]);
    httpChannel.QueryInterface(Ci.nsITraceableChannel);
    this.originalListener = httpChannel.setNewListener(this);
  }

  onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
    const iStream = new lazy.BinaryInputStream(aInputStream);
    const sStream = new lazy.StorageStream(8192, aCount, null);
    const oStream = new lazy.BinaryOutputStream(sStream.getOutputStream(0));

    // Copy received data as they come.
    const data = iStream.readBytes(aCount);
    this._chunks.push(data);

    oStream.writeBytes(data, aCount);
    this.originalListener.onDataAvailable(
      aRequest,
      sStream.newInputStream(0),
      aOffset,
      aCount
    );
  }

  onStartRequest(aRequest) {
    this.originalListener.onStartRequest(aRequest);
  }

  onStopRequest(aRequest, aStatusCode) {
    this.originalListener.onStopRequest(aRequest, aStatusCode);
    const body = this._chunks.join("");
    delete this._chunks;
    this._networkObserver._onResponseFinished(
      this._browser,
      this._httpChannel,
      body
    );
  }
}

function getNetworkErrorStatusText(status) {
  if (!status) {
    return null;
  }
  for (const key of Object.keys(Cr)) {
    if (Cr[key] === status) {
      return key;
    }
  }
  // Security module. The following is taken from
  // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL
  if ((status & 0xff0000) === 0x5a0000) {
    // NSS_SEC errors (happen below the base value because of negative vals)
    if (
      (status & 0xffff) <
      Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)
    ) {
      // The bases are actually negative, so in our positive numeric space, we
      // need to subtract the base off our value.
      const nssErr =
        Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
      switch (nssErr) {
        case 11:
          return "SEC_ERROR_EXPIRED_CERTIFICATE";
        case 12:
          return "SEC_ERROR_REVOKED_CERTIFICATE";
        case 13:
          return "SEC_ERROR_UNKNOWN_ISSUER";
        case 20:
          return "SEC_ERROR_UNTRUSTED_ISSUER";
        case 21:
          return "SEC_ERROR_UNTRUSTED_CERT";
        case 36:
          return "SEC_ERROR_CA_CERT_INVALID";
        case 90:
          return "SEC_ERROR_INADEQUATE_KEY_USAGE";
        case 176:
          return "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED";
        default:
          return "SEC_ERROR_UNKNOWN";
      }
    }
    const sslErr =
      Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
    switch (sslErr) {
      case 3:
        return "SSL_ERROR_NO_CERTIFICATE";
      case 4:
        return "SSL_ERROR_BAD_CERTIFICATE";
      case 8:
        return "SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE";
      case 9:
        return "SSL_ERROR_UNSUPPORTED_VERSION";
      case 12:
        return "SSL_ERROR_BAD_CERT_DOMAIN";
      default:
        return "SSL_ERROR_UNKNOWN";
    }
  }
  return "<unknown error>";
}

[ Dauer der Verarbeitung: 0.37 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge