/* 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/. */

"use strict";

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const CC = Components.Constructor;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");

const InputStreamPump = CC(
        "@mozilla.org/network/input-stream-pump;1", "nsIInputStreamPump", "init"),
      AsyncStreamCopier = CC(
        "@mozilla.org/network/async-stream-copier;1", "nsIAsyncStreamCopier", "init"),
      ScriptableInputStream = CC(
        "@mozilla.org/scriptableinputstream;1", "nsIScriptableInputStream", "init"),
      BinaryInputStream = CC(
        "@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"),
      StringInputStream = CC(
        '@mozilla.org/io/string-input-stream;1', 'nsIStringInputStream'),
      ArrayBufferInputStream = CC(
        '@mozilla.org/io/arraybuffer-input-stream;1', 'nsIArrayBufferInputStream'),
      MultiplexInputStream = CC(
        '@mozilla.org/io/multiplex-input-stream;1', 'nsIMultiplexInputStream');
const TCPServerSocket = CC(
        "@mozilla.org/tcp-server-socket;1", "nsITCPServerSocketInternal", "init");

const kCONNECTING = 'connecting';
const kOPEN = 'open';
const kCLOSING = 'closing';
const kCLOSED = 'closed';
const kRESUME_ERROR = 'Calling resume() on a connection that was not suspended.';

const BUFFER_SIZE = 65536;
const NETWORK_STATS_THRESHOLD = 65536;

// XXX we have no TCPError implementation right now because it's really hard to
// do on b2g18.  On mozilla-central we want a proper TCPError that ideally
// sub-classes DOMError.  Bug 867872 has been filed to implement this and
// contains a documented TCPError.webidl that maps all the error codes we use in
// this file to slightly more readable explanations.
function createTCPError(aWindow, aErrorName, aErrorType) {
  return new (aWindow ? aWindow.DOMError : DOMError)(aErrorName);
}


/*
 * Debug logging function
 */

let debug = false;
function LOG(msg) {
  if (debug)
    dump("TCPSocket: " + msg + "\n");
}

/*
 * nsITCPSocketEvent object
 */

function TCPSocketEvent(type, sock, data) {
  this._type = type;
  this._target = sock;
  this._data = data;
}

// When this API moves to WebIDL and these __exposedProps__ go away, remove
// this call here and remove the API from XPConnect.
Cu.skipCOWCallableChecks();

TCPSocketEvent.prototype = {
  __exposedProps__: {
    type: 'r',
    target: 'r',
    data: 'r',
    // Promise::ResolveInternal tries to check if the thing being resolved is
    // itself a promise through the presence of "then".  Accordingly, we list
    // it as an exposed property, although we return undefined for it.
    // Bug 882123 covers making TCPSocket be a proper event target with proper
    // events.
    then: 'r'
  },
  get type() {
    return this._type;
  },
  get target() {
    return this._target;
  },
  get data() {
    return this._data;
  },
  get then() {
    return undefined;
  }
}

/*
 * nsIDOMTCPSocket object
 */

function TCPSocket() {
  this._readyState = kCLOSED;

  this._onopen = null;
  this._ondrain = null;
  this._ondata = null;
  this._onerror = null;
  this._onclose = null;

  this._binaryType = "string";

  this._host = "";
  this._port = 0;
  this._ssl = false;

  this.useWin = null;
}

TCPSocket.prototype = {
  __exposedProps__: {
    open: 'r',
    host: 'r',
    port: 'r',
    ssl: 'r',
    bufferedAmount: 'r',
    suspend: 'r',
    resume: 'r',
    close: 'r',
    send: 'r',
    readyState: 'r',
    binaryType: 'r',
    listen: 'r',
    onopen: 'rw',
    ondrain: 'rw',
    ondata: 'rw',
    onerror: 'rw',
    onclose: 'rw'
  },
  // The binary type, "string" or "arraybuffer"
  _binaryType: null,

  // Internal
  _hasPrivileges: null,

  // Raw socket streams
  _transport: null,
  _socketInputStream: null,
  _socketOutputStream: null,

  // Input stream machinery
  _inputStreamPump: null,
  _inputStreamScriptable: null,
  _inputStreamBinary: null,

  // Output stream machinery
  _multiplexStream: null,
  _multiplexStreamCopier: null,

  _asyncCopierActive: false,
  _waitingForDrain: false,
  _suspendCount: 0,

  // Reported parent process buffer
  _bufferedAmount: 0,

  // IPC socket actor
  _socketBridge: null,

  // StartTLS
  _waitingForStartTLS: false,
  _pendingDataAfterStartTLS: [],

  // Used to notify when update bufferedAmount is updated.
  _onUpdateBufferedAmount: null,
  _trackingNumber: 0,

//@line 190 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"

  // Public accessors.
  get readyState() {
    return this._readyState;
  },
  get binaryType() {
    return this._binaryType;
  },
  get host() {
    return this._host;
  },
  get port() {
    return this._port;
  },
  get ssl() {
    return this._ssl;
  },
  get bufferedAmount() {
    if (this._inChild) {
      return this._bufferedAmount;
    }
    return this._multiplexStream.available();
  },
  get onopen() {
    return this._onopen;
  },
  set onopen(f) {
    this._onopen = f;
  },
  get ondrain() {
    return this._ondrain;
  },
  set ondrain(f) {
    this._ondrain = f;
  },
  get ondata() {
    return this._ondata;
  },
  set ondata(f) {
    this._ondata = f;
  },
  get onerror() {
    return this._onerror;
  },
  set onerror(f) {
    this._onerror = f;
  },
  get onclose() {
    return this._onclose;
  },
  set onclose(f) {
    this._onclose = f;
  },

  _activateTLS: function() {
    let securityInfo = this._transport.securityInfo
          .QueryInterface(Ci.nsISSLSocketControl);
    securityInfo.StartTLS();
  },

  // Helper methods.
  _createTransport: function ts_createTransport(host, port, sslMode) {
    let options;
    if (sslMode === 'ssl') {
      options = ['ssl'];
    } else {
      options = ['starttls'];
    }
    return Cc["@mozilla.org/network/socket-transport-service;1"]
             .getService(Ci.nsISocketTransportService)
             .createTransport(options, 1, host, port, null);
  },

  _sendBufferedAmount: function ts_sendBufferedAmount() {
    if (this._onUpdateBufferedAmount) {
      this._onUpdateBufferedAmount(this.bufferedAmount, this._trackingNumber);
    }
  },

  _ensureCopying: function ts_ensureCopying() {
    let self = this;
    if (this._asyncCopierActive) {
      return;
    }
    this._asyncCopierActive = true;
    this._multiplexStreamCopier.asyncCopy({
      onStartRequest: function ts_output_onStartRequest() {
      },
      onStopRequest: function ts_output_onStopRequest(request, context, status) {
        self._asyncCopierActive = false;
        self._multiplexStream.removeStream(0);
        self._sendBufferedAmount();

        if (!Components.isSuccessCode(status)) {
          // Note that we can/will get an error here as well as in the
          // onStopRequest for inbound data.
          self._maybeReportErrorAndCloseIfOpen(status);
          return;
        }

        if (self._multiplexStream.count) {
          self._ensureCopying();
        } else {
          // If we are waiting for initiating starttls, we can begin to
          // activate tls now.
          if (self._waitingForStartTLS && self._readyState == kOPEN) {
            self._activateTLS();
            self._waitingForStartTLS = false;
            // If we have pending data, we should send them, or fire
            // a drain event if we are waiting for it.
            if (self._pendingDataAfterStartTLS.length > 0) {
              while (self._pendingDataAfterStartTLS.length)
                self._multiplexStream.appendStream(self._pendingDataAfterStartTLS.shift());
              self._ensureCopying();
              return;
            }
          }

          // If we have a callback to update bufferedAmount, we let child to
          // decide whether ondrain should be dispatched.
          if (self._waitingForDrain && !self._onUpdateBufferedAmount) {
            self._waitingForDrain = false;
            self.callListener("drain");
          }
          if (self._readyState === kCLOSING) {
            self._socketOutputStream.close();
            self._readyState = kCLOSED;
            self.callListener("close");
          }
        }
      }
    }, null);
  },

  _initStream: function ts_initStream(binaryType) {
    this._binaryType = binaryType;
    this._socketInputStream = this._transport.openInputStream(0, 0, 0);
    this._socketOutputStream = this._transport.openOutputStream(
      Ci.nsITransport.OPEN_UNBUFFERED, 0, 0);

    // If the other side is not listening, we will
    // get an onInputStreamReady callback where available
    // raises to indicate the connection was refused.
    this._socketInputStream.asyncWait(
      this, this._socketInputStream.WAIT_CLOSURE_ONLY, 0, Services.tm.currentThread);

    if (this._binaryType === "arraybuffer") {
      this._inputStreamBinary = new BinaryInputStream(this._socketInputStream);
    } else {
      this._inputStreamScriptable = new ScriptableInputStream(this._socketInputStream);
    }

    this._multiplexStream = new MultiplexInputStream();

    this._multiplexStreamCopier = new AsyncStreamCopier(
      this._multiplexStream,
      this._socketOutputStream,
      // (nsSocketTransport uses gSocketTransportService)
      Cc["@mozilla.org/network/socket-transport-service;1"]
        .getService(Ci.nsIEventTarget),
      /* source buffered */ true, /* sink buffered */ false,
      BUFFER_SIZE, /* close source*/ false, /* close sink */ false);
  },

//@line 385 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"

  callListener: function ts_callListener(type, data) {
    if (!this["on" + type])
      return;

    this["on" + type].call(null, new TCPSocketEvent(type, this, data || ""));
  },

  /* nsITCPSocketInternal methods */
  callListenerError: function ts_callListenerError(type, name) {
    // XXX we're not really using TCPError at this time, so there's only a name
    // attribute to pass.
    this.callListener(type, createTCPError(this.useWin, name));
  },

  callListenerData: function ts_callListenerString(type, data) {
    this.callListener(type, data);
  },

  callListenerArrayBuffer: function ts_callListenerArrayBuffer(type, data) {
    this.callListener(type, data);
  },

  callListenerVoid: function ts_callListenerVoid(type) {
    this.callListener(type);
  },

  /**
   * This method is expected to be called by TCPSocketChild to update child's
   * readyState.
   */
  updateReadyState: function ts_updateReadyState(readyState) {
    if (!this._inChild) {
      LOG("Calling updateReadyState in parent, which should only be called " +
          "in child");
      return;
    }
    this._readyState = readyState;
  },

  updateBufferedAmount: function ts_updateBufferedAmount(bufferedAmount, trackingNumber) {
    if (trackingNumber != this._trackingNumber) {
      LOG("updateBufferedAmount is called but trackingNumber is not matched " +
          "parent's trackingNumber: " + trackingNumber + ", child's trackingNumber: " +
          this._trackingNumber);
      return;
    }
    this._bufferedAmount = bufferedAmount;
    if (bufferedAmount == 0) {
      if (this._waitingForDrain) {
        this._waitingForDrain = false;
        this.callListener("drain");
      }
    } else {
      LOG("bufferedAmount is updated but haven't reaches zero. bufferedAmount: " +
          bufferedAmount);
    }
  },

  createAcceptedParent: function ts_createAcceptedParent(transport, binaryType, windowObject) {
    let that = new TCPSocket();
    that._transport = transport;
    that._initStream(binaryType);

    // ReadyState is kOpen since accepted transport stream has already been connected
    that._readyState = kOPEN;
    that._inputStreamPump = new InputStreamPump(that._socketInputStream, -1, -1, 0, 0, false);
    that._inputStreamPump.asyncRead(that, null);

    // Grab host/port from SocketTransport.
    that._host = transport.host;
    that._port = transport.port;
    that.useWin = windowObject;

    return that;
  },

  createAcceptedChild: function ts_createAcceptedChild(socketChild, binaryType, windowObject) {
    let that = new TCPSocket();

    that._binaryType = binaryType;
    that._inChild = true;
    that._readyState = kOPEN;
    socketChild.setSocketAndWindow(that, windowObject);
    that._socketBridge = socketChild;
    that._host = socketChild.host;
    that._port = socketChild.port;
    that.useWin = windowObject;

    return that;
  },

  setAppId: function ts_setAppId(appId) {
//@line 481 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"
    // Do nothing because _appId only exists on Gonk-specific platform.
//@line 483 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"
  },

  setInBrowser: function ts_setInBrowser(inBrowser) {
//@line 489 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"
    // Do nothing.
//@line 491 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"
  },

  setOnUpdateBufferedAmountHandler: function(aFunction) {
    if (typeof(aFunction) == 'function') {
      this._onUpdateBufferedAmount = aFunction;
    } else {
      throw new Error("only function can be passed to " +
                      "setOnUpdateBufferedAmountHandler");
    }
  },

  /**
   * Handle the requst of sending data and update trackingNumber from
   * child.
   * This function is expected to be called by TCPSocketChild.
   */
  onRecvSendFromChild: function(data, byteOffset, byteLength, trackingNumber) {
    this._trackingNumber = trackingNumber;
    this.send(data, byteOffset, byteLength);
  },

  /* end nsITCPSocketInternal methods */

  initWindowless: function ts_initWindowless() {
    try {
      return Services.prefs.getBoolPref("dom.mozTCPSocket.enabled");
    } catch (e) {
      // no pref means return false
      return false;
    }
  },

  init: function ts_init(aWindow) {
    if (!this.initWindowless())
      return null;

    let principal = aWindow.document.nodePrincipal;
    let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
                   .getService(Ci.nsIScriptSecurityManager);

    let perm = principal == secMan.getSystemPrincipal()
                 ? Ci.nsIPermissionManager.ALLOW_ACTION
                 : Services.perms.testExactPermissionFromPrincipal(principal, "tcp-socket");

    this._hasPrivileges = perm == Ci.nsIPermissionManager.ALLOW_ACTION;

    let util = aWindow.QueryInterface(
      Ci.nsIInterfaceRequestor
    ).getInterface(Ci.nsIDOMWindowUtils);

    this.useWin = XPCNativeWrapper.unwrap(aWindow);
    this.innerWindowID = util.currentInnerWindowID;
    LOG("window init: " + this.innerWindowID);
  },

  observe: function(aSubject, aTopic, aData) {
    if (aTopic == "inner-window-destroyed") {
      let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
      if (wId == this.innerWindowID) {
        LOG("inner-window-destroyed: " + this.innerWindowID);

        // This window is now dead, so we want to clear the callbacks
        // so that we don't get a "can't access dead object" when the
        // underlying stream goes to tell us that we are closed
        this.onopen = null;
        this.ondrain = null;
        this.ondata = null;
        this.onerror = null;
        this.onclose = null;

        this.useWin = null;

        // Clean up our socket
        this.close();
      }
    }
  },

  // nsIDOMTCPSocket
  open: function ts_open(host, port, options) {
    this._inChild = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
                       .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
    LOG("content process: " + (this._inChild ? "true" : "false"));

    // in the testing case, init won't be called and
    // hasPrivileges will be null. We want to proceed to test.
    if (this._hasPrivileges !== true && this._hasPrivileges !== null) {
      throw new Error("TCPSocket does not have permission in this context.\n");
    }
    let that = new TCPSocket();

    that.useWin = this.useWin;
    that.innerWindowID = this.innerWindowID;
    that._inChild = this._inChild;

    LOG("window init: " + that.innerWindowID);
    Services.obs.addObserver(that, "inner-window-destroyed", true);

    LOG("startup called");
    LOG("Host info: " + host + ":" + port);

    that._readyState = kCONNECTING;
    that._host = host;
    that._port = port;
    if (options !== undefined) {
      if (options.useSecureTransport) {
          that._ssl = 'ssl';
      } else {
          that._ssl = false;
      }
      that._binaryType = options.binaryType || that._binaryType;
    }

    LOG("SSL: " + that.ssl);

    if (this._inChild) {
      that._socketBridge = Cc["@mozilla.org/tcp-socket-child;1"]
                             .createInstance(Ci.nsITCPSocketChild);
      that._socketBridge.sendOpen(that, host, port, !!that._ssl,
                                  that._binaryType, this.useWin, this.useWin || this);
      return that;
    }

    let transport = that._transport = this._createTransport(host, port, that._ssl);
    transport.setEventSink(that, Services.tm.currentThread);
    that._initStream(that._binaryType);

//@line 627 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"

    return that;
  },

  upgradeToSecure: function ts_upgradeToSecure() {
    if (this._readyState !== kOPEN) {
      throw new Error("Socket not open.");
    }
    if (this._ssl == 'ssl') {
      // Already SSL
      return;
    }

    this._ssl = 'ssl';

    if (this._inChild) {
      this._socketBridge.sendStartTLS();
      return;
    }

    if (this._multiplexStream.count == 0) {
      this._activateTLS();
    } else {
      this._waitingForStartTLS = true;
    }
  },

  listen: function ts_listen(localPort, options, backlog) {
    // in the testing case, init won't be called and
    // hasPrivileges will be null. We want to proceed to test.
    if (this._hasPrivileges !== true && this._hasPrivileges !== null) {
      throw new Error("TCPSocket does not have permission in this context.\n");
    }
    let that = new TCPServerSocket(this.useWin);

    options = options || { binaryType : this.binaryType };
    backlog = backlog || -1;
    that.listen(localPort, options, backlog);
    return that;
  },

  close: function ts_close() {
    if (this._readyState === kCLOSED || this._readyState === kCLOSING)
      return;

    LOG("close called");
    this._readyState = kCLOSING;

    if (this._inChild) {
      this._socketBridge.sendClose();
      return;
    }

    if (!this._multiplexStream.count) {
      this._socketOutputStream.close();
    }
    this._socketInputStream.close();
  },

  send: function ts_send(data, byteOffset, byteLength) {
    if (this._readyState !== kOPEN) {
      throw new Error("Socket not open.");
    }

    if (this._binaryType === "arraybuffer") {
      byteLength = byteLength || data.byteLength;
    }

    if (this._inChild) {
      this._socketBridge.sendSend(data, byteOffset, byteLength, ++this._trackingNumber);
    }

    let length = this._binaryType === "arraybuffer" ? byteLength : data.length;
    let newBufferedAmount = this.bufferedAmount + length;
    let bufferFull = newBufferedAmount >= BUFFER_SIZE;

    if (bufferFull) {
      // If we buffered more than some arbitrary amount of data,
      // (65535 right now) we should tell the caller so they can
      // wait until ondrain is called if they so desire. Once all the
      // buffered data has been written to the socket, ondrain is
      // called.
      this._waitingForDrain = true;
    }

    if (this._inChild) {
      // In child, we just add buffer length to our bufferedAmount and let
      // parent to update our bufferedAmount when data have been sent.
      this._bufferedAmount = newBufferedAmount;
      return !bufferFull;
    }

    let new_stream;
    if (this._binaryType === "arraybuffer") {
      new_stream = new ArrayBufferInputStream();
      new_stream.setData(data, byteOffset, byteLength);
    } else {
      new_stream = new StringInputStream();
      new_stream.setData(data, length);
    }

    if (this._waitingForStartTLS) {
      // When we are waiting for starttls, new_stream is added to pendingData
      // and will be appended to multiplexStream after tls had been set up.
      this._pendingDataAfterStartTLS.push(new_stream);
    } else {
      this._multiplexStream.appendStream(new_stream);
    }

    this._ensureCopying();

//@line 743 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"

    return !bufferFull;
  },

  suspend: function ts_suspend() {
    if (this._inChild) {
      this._socketBridge.sendSuspend();
      return;
    }

    if (this._inputStreamPump) {
      this._inputStreamPump.suspend();
    } else {
      ++this._suspendCount;
    }
  },

  resume: function ts_resume() {
    if (this._inChild) {
      this._socketBridge.sendResume();
      return;
    }

    if (this._inputStreamPump) {
      this._inputStreamPump.resume();
    } else if (this._suspendCount < 1) {
      throw new Error(kRESUME_ERROR);
    } else {
      --this._suspendCount;
    }
  },

  _maybeReportErrorAndCloseIfOpen: function(status) {
//@line 781 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"

    // If we're closed, we've already reported the error or just don't need to
    // report the error.
    if (this._readyState === kCLOSED)
      return;
    this._readyState = kCLOSED;

    if (!Components.isSuccessCode(status)) {
      // Convert the status code to an appropriate error message.  Raw constants
      // are used inline in all cases for consistency.  Some error codes are
      // available in Components.results, some aren't.  Network error codes are
      // effectively stable, NSS error codes are officially not, but we have no
      // symbolic way to dynamically resolve them anyways (other than an ability
      // to determine the error class.)
      let errName, errType;
      // security module? (and this is an error)
      if ((status & 0xff0000) === 0x5a0000) {
        const nsINSSErrorsService = Ci.nsINSSErrorsService;
        let nssErrorsService = Cc['@mozilla.org/nss_errors_service;1']
                                 .getService(nsINSSErrorsService);
        let errorClass;
        // getErrorClass will throw a generic NS_ERROR_FAILURE if the error code is
        // somehow not in the set of covered errors.
        try {
          errorClass = nssErrorsService.getErrorClass(status);
        }
        catch (ex) {
          errorClass = 'SecurityProtocol';
        }
        switch (errorClass) {
          case nsINSSErrorsService.ERROR_CLASS_SSL_PROTOCOL:
            errType = 'SecurityProtocol';
            break;
          case nsINSSErrorsService.ERROR_CLASS_BAD_CERT:
            errType = 'SecurityCertificate';
            break;
          // no default is required; the platform impl automatically defaults to
          // ERROR_CLASS_SSL_PROTOCOL.
        }

        // NSS_SEC errors (happen below the base value because of negative vals)
        if ((status & 0xffff) <
            Math.abs(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.
          let nssErr = Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE) -
                         (status & 0xffff);
          switch (nssErr) {
            case 11: // SEC_ERROR_EXPIRED_CERTIFICATE, sec(11)
              errName = 'SecurityExpiredCertificateError';
              break;
            case 12: // SEC_ERROR_REVOKED_CERTIFICATE, sec(12)
              errName = 'SecurityRevokedCertificateError';
              break;
            // per bsmith, we will be unable to tell these errors apart very soon,
            // so it makes sense to just folder them all together already.
            case 13: // SEC_ERROR_UNKNOWN_ISSUER, sec(13)
            case 20: // SEC_ERROR_UNTRUSTED_ISSUER, sec(20)
            case 21: // SEC_ERROR_UNTRUSTED_CERT, sec(21)
            case 36: // SEC_ERROR_CA_CERT_INVALID, sec(36)
              errName = 'SecurityUntrustedCertificateIssuerError';
              break;
            case 90: // SEC_ERROR_INADEQUATE_KEY_USAGE, sec(90)
              errName = 'SecurityInadequateKeyUsageError';
              break;
            case 176: // SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, sec(176)
              errName = 'SecurityCertificateSignatureAlgorithmDisabledError';
              break;
            default:
              errName = 'SecurityError';
              break;
          }
        }
        // NSS_SSL errors
        else {
          let sslErr = Math.abs(nsINSSErrorsService.NSS_SSL_ERROR_BASE) -
                         (status & 0xffff);
          switch (sslErr) {
            case 3: // SSL_ERROR_NO_CERTIFICATE, ssl(3)
              errName = 'SecurityNoCertificateError';
              break;
            case 4: // SSL_ERROR_BAD_CERTIFICATE, ssl(4)
              errName = 'SecurityBadCertificateError';
              break;
            case 8: // SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE, ssl(8)
              errName = 'SecurityUnsupportedCertificateTypeError';
              break;
            case 9: // SSL_ERROR_UNSUPPORTED_VERSION, ssl(9)
              errName = 'SecurityUnsupportedTLSVersionError';
              break;
            case 12: // SSL_ERROR_BAD_CERT_DOMAIN, ssl(12)
              errName = 'SecurityCertificateDomainMismatchError';
              break;
            default:
              errName = 'SecurityError';
              break;
          }
        }
      }
      // must be network
      else {
        errType = 'Network';
        switch (status) {
          // connect to host:port failed
          case 0x804B000C: // NS_ERROR_CONNECTION_REFUSED, network(13)
            errName = 'ConnectionRefusedError';
            break;
          // network timeout error
          case 0x804B000E: // NS_ERROR_NET_TIMEOUT, network(14)
            errName = 'NetworkTimeoutError';
            break;
          // hostname lookup failed
          case 0x804B001E: // NS_ERROR_UNKNOWN_HOST, network(30)
            errName = 'DomainNotFoundError';
            break;
          case 0x804B0047: // NS_ERROR_NET_INTERRUPT, network(71)
            errName = 'NetworkInterruptError';
            break;
          default:
            errName = 'NetworkError';
            break;
        }
      }
      let err = createTCPError(this.useWin, errName, errType);
      this.callListener("error", err);
    }
    this.callListener("close");
  },

  // nsITransportEventSink (Triggered by transport.setEventSink)
  onTransportStatus: function ts_onTransportStatus(
    transport, status, progress, max) {
    if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
      this._readyState = kOPEN;
      this.callListener("open");

      this._inputStreamPump = new InputStreamPump(
        this._socketInputStream, -1, -1, 0, 0, false
      );

      while (this._suspendCount--) {
        this._inputStreamPump.suspend();
      }

      this._inputStreamPump.asyncRead(this, null);
    }
  },

  // nsIAsyncInputStream (Triggered by _socketInputStream.asyncWait)
  // Only used for detecting connection refused
  onInputStreamReady: function ts_onInputStreamReady(input) {
    try {
      input.available();
    } catch (e) {
      // NS_ERROR_CONNECTION_REFUSED
      this._maybeReportErrorAndCloseIfOpen(0x804B000C);
    }
  },

  // nsIRequestObserver (Triggered by _inputStreamPump.asyncRead)
  onStartRequest: function ts_onStartRequest(request, context) {
  },

  // nsIRequestObserver (Triggered by _inputStreamPump.asyncRead)
  onStopRequest: function ts_onStopRequest(request, context, status) {
    let buffered_output = this._multiplexStream.count !== 0;

    this._inputStreamPump = null;

    let statusIsError = !Components.isSuccessCode(status);

    if (buffered_output && !statusIsError) {
      // If we have some buffered output still, and status is not an
      // error, the other side has done a half-close, but we don't
      // want to be in the close state until we are done sending
      // everything that was buffered. We also don't want to call onclose
      // yet.
      return;
    }

    // We call this even if there is no error.
    this._maybeReportErrorAndCloseIfOpen(status);
  },

  // nsIStreamListener (Triggered by _inputStreamPump.asyncRead)
  onDataAvailable: function ts_onDataAvailable(request, context, inputStream, offset, count) {
    if (this._binaryType === "arraybuffer") {
      let buffer = new (this.useWin ? this.useWin.ArrayBuffer : ArrayBuffer)(count);
      this._inputStreamBinary.readArrayBuffer(count, buffer);
      this.callListener("data", buffer);
    } else {
      this.callListener("data", this._inputStreamScriptable.read(count));
    }

//@line 980 "/home/daniel/trees/bg-ng/dom/network/TCPSocket.js"
  },

  classID: Components.ID("{cda91b22-6472-11e1-aa11-834fec09cd0a}"),

  classInfo: XPCOMUtils.generateCI({
    classID: Components.ID("{cda91b22-6472-11e1-aa11-834fec09cd0a}"),
    contractID: "@mozilla.org/tcp-socket;1",
    classDescription: "Client TCP Socket",
    interfaces: [
      Ci.nsIDOMTCPSocket,
    ],
    flags: Ci.nsIClassInfo.DOM_OBJECT,
  }),

  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIDOMTCPSocket,
    Ci.nsITCPSocketInternal,
    Ci.nsIDOMGlobalPropertyInitializer,
    Ci.nsIObserver,
    Ci.nsISupportsWeakReference
  ])
}

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TCPSocket]);
