/// /// /// /// import logging = require('../../../third_party/uproxy-lib/logging/logging'); import handler = require('../../../third_party/uproxy-lib/handler/queue'); import net = require('./net.types'); var log :logging.Log = new logging.Log('tcp'); // Indicates how a socket (server or client) terminated. export enum SocketCloseKind { WE_CLOSED_IT, REMOTELY_CLOSED, NEVER_CONNECTED, UNKOWN } export interface ConnectionInfo { bound ?:net.Endpoint; remote ?:net.Endpoint; } // Maximum per-server number of TCP connections. var DEFAULT_MAX_CONNECTIONS = 1048576; // Public only for unit tests. export function endpointOfSocketInfo(info:freedom_TcpSocket.SocketInfo) : ConnectionInfo { var retval :ConnectionInfo = {}; if (typeof info.localAddress == 'string' && typeof info.localPort == 'number') { retval.bound = { address: info.localAddress, port: info.localPort }; } if (typeof info.peerAddress == 'string' && typeof info.peerPort == 'number') { retval.remote = { address: info.peerAddress, port: info.peerPort }; } return retval; } // Closes a socket, along with its freedomjs interface object. function destroyFreedomSocket_(socket:freedom_TcpSocket.Socket) : Promise { // Note: // freedom['core.tcpsocket'].close != freedom['core.tcpsocket']().close // The former destroys the freedom interface & communication channels. // The latter is a method on the constructed interface object that is on // the instance of the freedomjs TCP socket API. var destroy = () => { freedom['core.tcpsocket'].close(socket); }; return socket.close().then(destroy, (e:Error) => { destroy(); return e; }); } // Promise and handler queue-based TCP server with freedomjs sockets. // TODO: protection against multiple calls to methods such as listen export class Server { // Count of sockets created to date. private static numCreations_ :number = 0; // Unique ID, for logging purposes. private id_ :string; private socket_ :freedom_TcpSocket.Socket; // Active connections to the server. // TODO: index by connectionId rather than socketID private connections_ :{[socketId:number] : Connection} = {}; public connectionsQueue :handler.Queue = new handler.Queue(); private fulfillListening_ :(endpoint:net.Endpoint) => void; private rejectListening_ :(e:Error) => void; private onceListening_ = new Promise((F, R) => { this.fulfillListening_ = F; this.rejectListening_ = R; }); private fulfillShutdown_ :(kind:SocketCloseKind) => void; private onceShutdown_ = new Promise((F, R) => { this.fulfillShutdown_ = F; }); constructor(private endpoint_ :net.Endpoint, private maxConnections_ :number = DEFAULT_MAX_CONNECTIONS) { this.id_ = 'S' + (Server.numCreations_++); this.onceListening_.catch((e:Error) => { this.fulfillShutdown_(SocketCloseKind.NEVER_CONNECTED); }); this.socket_ = freedom['core.tcpsocket'](); this.socket_.on('onConnection', this.onConnectionHandler_); this.socket_.on('onDisconnect', this.onDisconnectHandler_); } // Invoked when the socket terminates. private onDisconnectHandler_ = (info:freedom_TcpSocket.DisconnectInfo) : void => { log.debug('%1: disconnected: %2', this.id_, JSON.stringify(info)); if (info.errcode === 'SUCCESS') { this.fulfillShutdown_(SocketCloseKind.WE_CLOSED_IT); } else { // TODO: investigate which other values occur this.fulfillShutdown_(SocketCloseKind.UNKOWN); } } // Listens for connections, returning onceListening. // Should only be called once. public listen = () : Promise => { this.socket_.listen(this.endpoint_.address, this.endpoint_.port).then(this.socket_.getInfo).then( (info:freedom_TcpSocket.SocketInfo) => { this.endpoint_ = { address: info.localAddress, port: info.localPort }; this.fulfillListening_(this.endpoint_); }).catch((e:Error) => { this.rejectListening_(new Error('failed to listen: ' + e.message)); }); return this.onceListening_; } // Invoked each time a new connection is established with the server. private onConnectionHandler_ = ( acceptValue:freedom_TcpSocket.ConnectInfo) : void => { log.debug('%1: new connection', this.id_); var socketId = acceptValue.socket; if (this.connectionsCount() >= this.maxConnections_) { log.warn('%1: hit maximum connections count, dropping new connection', this.id_); destroyFreedomSocket_(freedom['core.tcpsocket'](socketId)); return; } var connection = new Connection({ existingSocketId: socketId }); this.connections_[socketId] = connection; var discard = () => { delete this.connections_[socketId]; log.debug('%1: discarded connection (%2 remaining)', this.id_, this.connectionsCount()); }; connection.onceClosed.then(discard, (e:Error) => { log.error('%1: connection %2 rejected on close: %3', this.id_, socketId, e.message); discard(); }); this.connectionsQueue.handle(connection); } // Closes the server socket then closes all active connections. // Equivalent to calling stopListening followed by closeAll. public shutdown = () : Promise => { log.debug('%1: shutdown', this.id_); // This order is important: make sure no new connections happen while // we're trying to close all the connections. return this.stopListening().then(this.closeAll); } // Closes the server socket, preventing any new connections. // Does not affect active connections to the server. public stopListening = () : Promise => { log.debug('%1: closing socket, no new connections will be accepted', this.id_); return destroyFreedomSocket_(this.socket_); } // Closes all active connections. public closeAll = () : Promise => { log.debug('%1: closing all connections', this.id_); var promises :Promise[] = []; for (var socketId in this.connections_) { var connection = this.connections_[socketId]; promises.push(connection.close()); } return Promise.all(promises).then((unused:any) => {}); } // Returns all active connections. public connections = () : Connection[] => { var connections : Connection[] = []; for (var socketId in this.connections_) { connections.push(this.connections_[socketId]); } return connections; } // Returns the number of the active connections. public connectionsCount = () => { return Object.keys(this.connections_).length; } // Returns a promise which fulfills once the socket is accepting // connections and rejects if there is any error creating the socket // or listening for connections. public onceListening = () : Promise => { return this.onceListening_; } // Returns a promise which fulfills once the socket has stopped // accepting new connections, or the call to listen has failed. public onceShutdown = () : Promise => { return this.onceShutdown_; } public toString = () : string => { return 'TCP server ' + this.id_ + ': ' + JSON.stringify(this.endpoint_) + ', ' + this.connectionsCount() + ' connections: ' + this.connections().join(', '); } } // Tcp.Connection - Manages up a single TCP connection. export class Connection { // Unique identifier for each connection. private static globalConnectionId_ :number = 0; // Promise for when this connection is closed. public onceConnected :Promise; public onceClosed :Promise; // Queue of data to be handled, and the capacity to set a handler and // handle the data. public dataFromSocketQueue :handler.Queue; public dataToSocketQueue :handler.Queue; // Public unique connectionId. public connectionId :string; // isClosed() === state_ === Connection.State.CLOSED iff onceClosed // has been rejected or fulfilled. We use isClosed to ensure that we only // fulfill/reject the onceDisconnectd once. private state_ :Connection.State; // The underlying Freedom TCP socket. private connectionSocket_ :freedom_TcpSocket.Socket; // A private function called to invoke fullfil onceClosed. private fulfillClosed_ :(reason:SocketCloseKind)=>void; // A TCP connection for a given socket. constructor(connectionKind:Connection.Kind, private startPaused_?:boolean) { this.connectionId = 'N' + Connection.globalConnectionId_++; this.dataFromSocketQueue = new handler.Queue(); this.dataToSocketQueue = new handler.Queue(); if(Object.keys(connectionKind).length !== 1) { //log.error(this.connectionId + ': Bad New Tcp Connection Kind:' + // JSON.stringify(connectionKind)); this.state_ = Connection.State.ERROR; this.onceConnected = Promise.reject(new Error( this.connectionId + 'Bad New Tcp Connection Kind:' + JSON.stringify(connectionKind))); this.onceClosed = Promise.resolve(SocketCloseKind.NEVER_CONNECTED); return; } if(connectionKind.existingSocketId) { // If we already have an open socket; i.e. from a previous tcp listen. // So we get a handler to the old freedom socket. this.connectionSocket_ = freedom['core.tcpsocket'](connectionKind.existingSocketId); this.onceConnected = this.connectionSocket_.getInfo().then(endpointOfSocketInfo); this.state_ = Connection.State.CONNECTED; this.connectionId = this.connectionId + '.A' + connectionKind.existingSocketId; } else if (connectionKind.endpoint) { // Create a new tcp socket to the given endpoint. this.connectionSocket_ = freedom['core.tcpsocket'](); // We don't declare ourselves connected until we know the IP address to // which we have connected. To speed this process up, we immediately // pause the socket as soon as it's connected, so that CPU time is not // wasted sending events that we can't pass on until getInfo returns. this.onceConnected = this.connectionSocket_ .connect(connectionKind.endpoint.address, connectionKind.endpoint.port) .then(this.pause) .then(this.connectionSocket_.getInfo) .then((info:freedom_TcpSocket.SocketInfo) => { if (!this.startPaused_) { this.resume(); } return endpointOfSocketInfo(info); }) this.state_ = Connection.State.CONNECTING; this.onceConnected .then(() => { // We need this guard because the getInfo call is async and a // close may happen affter the freedom socket connects and the // getInfo completes. if(this.state_ !== Connection.State.CLOSED) { this.state_ = Connection.State.CONNECTED; } }); } else { throw(new Error(this.connectionId + ': Should be impossible connectionKind' + JSON.stringify(connectionKind))); } // Use the dataFromSocketQueue handler for data from the socket. this.connectionSocket_.on('onData', (readInfo:freedom_TcpSocket.ReadInfo) : void => { this.dataFromSocketQueue.handle(readInfo.data); }); this.onceClosed = new Promise((F, R) => { this.fulfillClosed_ = F; }); // Once we are connected, we start sending data to the underlying socket. // |dataToSocketQueue| allows a class using this connection to start // queuing data to be send to the socket. this.onceConnected.then(() => { this.dataToSocketQueue.setHandler(this.connectionSocket_.write); }); this.onceConnected.catch((e:Error) => { this.fulfillClosed_(SocketCloseKind.NEVER_CONNECTED); }); this.connectionSocket_.on('onDisconnect', this.onDisconnectHandler_); } // Receive returns a promise for exactly the next |ArrayBuffer| of data. public receiveNext = () : Promise => { return new Promise((F,R) => { this.dataFromSocketQueue.setSyncNextHandler(F).catch(R); }); } // Invoked when the socket is closed for any reason. // Fulfills onceClosed. private onDisconnectHandler_ = (info:freedom_TcpSocket.DisconnectInfo) : void => { log.debug('%1: onDisconnect: %2', [ this.connectionId, JSON.stringify(info)]); if (this.state_ === Connection.State.CLOSED) { log.warn('%1: Got onDisconnect in closed state', [this.connectionId]); return; } this.state_ = Connection.State.CLOSED; this.dataToSocketQueue.stopHandling(); this.dataToSocketQueue.clear(); // CONSIDER: can this happen after a onceConnected promise rejection? if so, // do we want to preserve the SocketCloseKind.NEVER_CONNECTED result for // onceClosed? destroyFreedomSocket_(this.connectionSocket_).then(() => { if (info.errcode === 'SUCCESS') { this.fulfillClosed_(SocketCloseKind.WE_CLOSED_IT); } else if (info.errcode === 'CONNECTION_CLOSED') { this.fulfillClosed_(SocketCloseKind.REMOTELY_CLOSED); } else { this.fulfillClosed_(SocketCloseKind.UNKOWN); } }); } public pause = () => { this.connectionSocket_.pause(); } public resume = () => { this.connectionSocket_.resume(); } // This is called to close the underlying socket. This fulfills the // disconnect Promise `onceDisconnected`. public close = () : Promise => { log.debug('%1: close', [this.connectionId]); if (this.state_ === Connection.State.CLOSED) { log.debug('%1: close called when already closed', [ this.connectionId]); } else { this.connectionSocket_.close(); } // The onDisconnect handler (which should only // be invoked once) actually stops handling, fulfills // onceClosed, etc. return this.onceClosed; } // Boolean function to check if this connection is closed; public isClosed = () : boolean => { return this.state_ === Connection.State.CLOSED; }; public getState = () : Connection.State => { return this.state_; }; /** * Sends a message that is pre-formatted as an arrayBuffer. */ public send = (msg :ArrayBuffer) : Promise => { return this.dataToSocketQueue.handle(msg); } public toString = () => { return 'Tcp.Connection(' + this.connectionId + ':' + Connection.State[this.state_] + ')'; } } // class Tcp.Connection // Static stuff for the Connection class. export module Connection { // Exactly one of the arguments must be specified. export interface Kind { // To wrap up a connection for an existing socket existingSocketId ?:number; // TO create a new TCP connection to this target address and port. endpoint ?:net.Endpoint; } // Describes the state of a connection. export enum State { ERROR, // Cannot change state. CONNECTING, // Can change to ERROR or CONNECTED. CONNECTED, // Can change to ERROR or CLOSED. CLOSED // Cannot change state. } } // module Connection