///
///
///
import arraybuffers = require('../../../third_party/uproxy-lib/arraybuffers/arraybuffers');
import peerconnection = require('../../../third_party/uproxy-lib/webrtc/peerconnection');
import signals = require('../../../third_party/uproxy-lib/webrtc/signals');
import handler = require('../../../third_party/uproxy-lib/handler/queue');
import churn = require('../churn/churn');
import net = require('../net/net.types');
import tcp = require('../net/tcp');
import socks = require('../socks-common/socks-headers');
import Pool = require('../pool/pool');
import logging = require('../../../third_party/uproxy-lib/logging/logging');
// SocksToRtc passes socks requests over WebRTC datachannels.
module SocksToRtc {
var log :logging.Log = new logging.Log('SocksToRtc');
// The |SocksToRtc| class runs a SOCKS5 proxy server which passes requests
// remotely through WebRTC peer connections.
// TODO: rename this 'Server'.
// TODO: Extract common code for this and SocksToRtc:
// https://github.com/uProxy/uproxy/issues/977
export class SocksToRtc {
// Call this to initiate shutdown.
private fulfillStopping_ :() => void;
private onceStopping_ = new Promise((F, R) => {
this.fulfillStopping_ = F;
});
// Fulfills once the SOCKS server has terminated and the TCP server
// and peerconnection have been shutdown.
// This can happen in response to:
// - startup failure
// - TCP server or peerconnection failure
// - manual invocation of stop()
// Should never reject.
private onceStopped_ :Promise;
// The two Queues below only count bytes transferred between the SOCKS
// client and the remote host(s) the client wants to connect to. WebRTC
// overhead (DTLS headers, ICE initiation, etc.) is not included (because
// WebRTC does not provide easy access to that data) nor is SOCKS
// protocol-related data (because it's sent via string messages).
// All Sessions created in one instance of SocksToRtc will share and
// push numbers to the same queues (belonging to that instance of SocksToRtc).
// Queue of the number of bytes received from the peer. Handler is typically
// defined in the class that creates an instance of SocksToRtc.
private bytesReceivedFromPeer_ :handler.Queue =
new handler.Queue();
// Queue of the number of bytes sent to the peer. Handler is typically
// defined in the class that creates an instance of SocktsToRtc.
private bytesSentToPeer_ :handler.Queue =
new handler.Queue();
// Tcp server that is listening for SOCKS connections.
private tcpServer_ :tcp.Server;
// The connection to the peer that is acting as the endpoint for the proxy
// connection.
private peerConnection_
:peerconnection.PeerConnection;
// This pool manages the PeerConnection's datachannels.
private pool_ : Pool;
// Event listener registration function. When running in freedom, this is
// not defined, and the corresponding functionality is inserted by freedom
// on the consumer side.
public on : (t:string, f:(m:any) => void) => void;
// Database of event listeners for fallback implementation of |on|.
private listeners_ : { [s:string]: (m:any) => void };
// CONSIDER: Remove |on| and |listeners_| once all users of this class use
// it via freedom, or determine a better long-term plan for supporting
// events compatibly with and without freedom
// (https://github.com/uProxy/uproxy/issues/733).
// Note: The optional |dispatchEvent_| is for when this class is loaded as a
// freedom module.
constructor(private dispatchEvent_?:(t:string, m:Object) => void) {
if (!this.dispatchEvent_) {
// CONSIDER: Remove this code once all users of this class move to
// freedom. See https://github.com/uProxy/uproxy/issues/733 for
// possible solutions.
this.listeners_ = {};
this.on = this.fallbackOn_;
this.dispatchEvent_ = this.fallbackDispatchEvent_;
}
}
// Handles creation of a TCP server and peerconnection. Returns the endpoint
// it ended up listening on (if |localSocksServerEndpoint| has port set to
// 0, then a dynamic port is allocated and this port is returned within the
// promise's endpoint). NOTE: Users of this class MUST add on-event
// listeners before calling this method.
public startFromConfig = (
localSocksServerEndpoint:net.Endpoint,
pcConfig:freedom_RTCPeerConnection.RTCConfiguration,
obfuscate?:boolean) : Promise => {
var pc :freedom_RTCPeerConnection.RTCPeerConnection =
freedom['core.rtcpeerconnection'](pcConfig);
return this.start(
new tcp.Server(localSocksServerEndpoint),
obfuscate ?
new churn.Connection(pc, 'SocksToRtc') :
new peerconnection.PeerConnectionClass(pc));
}
// Starts the SOCKS server with the supplied TCP server and peerconnection.
// Returns a promise that resolves when the server is ready to use. This
// method is public only for testing purposes.
public start = (
tcpServer:tcp.Server,
// TODO(iislucas): are the types correct here? Does an obfuscated
// channel have a different signalling type?
peerconnection:peerconnection.PeerConnection)
: Promise => {
if (this.tcpServer_) {
throw new Error('already configured');
}
this.tcpServer_ = tcpServer;
this.tcpServer_.connectionsQueue
.setSyncHandler(this.makeTcpToRtcSession_);
this.peerConnection_ = peerconnection;
this.pool_ = new Pool(this.peerConnection_, 'SocksToRtc');
this.peerConnection_.signalForPeerQueue.setSyncHandler(
this.dispatchEvent_.bind(this, 'signalForPeer'));
this.bytesSentToPeer_.setSyncHandler(
this.dispatchEvent_.bind(this, 'bytesSentToPeer'));
this.bytesReceivedFromPeer_.setSyncHandler(
this.dispatchEvent_.bind(this, 'bytesReceivedFromPeer'));
// Start and listen for notifications.
peerconnection.negotiateConnection();
var onceReady :Promise =
Promise.all([
tcpServer.listen(),
peerconnection.onceConnected
])
.then((answers:any[]) => {
return tcpServer.onceListening();
});
// Shutdown if startup fails or when the server socket or
// peerconnection terminates.
onceReady.catch(this.fulfillStopping_);
this.tcpServer_.onceShutdown().then((kind:tcp.SocketCloseKind) => {
log.info('server socket closed: %1', tcp.SocketCloseKind[kind]);
}).then(this.fulfillStopping_);
this.peerConnection_.onceDisconnected
.then(() => {
log.info('peerconnection terminated');
}, (e:Error) => {
log.error('peerconnection terminated with error: %1', [e.message]);
})
.then(this.fulfillStopping_);
this.onceStopped_ = this.onceStopping_.then(this.stopResources_);
this.onceStopped_.then(this.dispatchEvent_.bind(this, 'stopped'));
var rejectOnStopping = new Promise((F, R) => {
this.onceStopping_.then(R);
});
return Promise.race([onceReady, rejectOnStopping]);
}
// Initiates shutdown of the TCP server and peerconnection.
// Returns onceStopped.
public stop = () : Promise => {
log.info('stop requested');
this.fulfillStopping_();
return this.onceStopped_;
}
// An implementation of dispatchEvent to use if none has been provided
// (i.e. when this class is not being used as a freedom // module).
// For simplicity, only one listener per message type is supported.
private fallbackDispatchEvent_ = (t:string, msg:any) : void => {
var listener = this.listeners_[t];
if (listener) {
listener(msg);
}
}
// Fallback implementation of |on|.
private fallbackOn_ = (t:string, f:(m:any) => void) : void => {
this.listeners_[t] = f;
}
// Shuts down the TCP server and peerconnection if they haven't already
// shut down, fulfilling once both have terminated. Since neither
// object's close() methods should ever reject, this should never reject.
// TODO: close all sessions before fulfilling
private stopResources_ = () : Promise => {
log.debug('freeing resources');
// PeerConnection.close() returns void, implying that the shutdown is
// effectively immediate. However, we wrap it in a promise to ensure
// that any exception is sent to the Promise.catch, rather than
// propagating synchronously up the stack.
return Promise.all([]>[
new Promise((F, R) => { this.peerConnection_.close(); F(); }),
this.tcpServer_.shutdown()
]).then((discard:any) => {});
}
// Invoked when a SOCKS client establishes a connection with the TCP server.
// Note that Session closes the TCP connection and datachannel on any error.
private makeTcpToRtcSession_ = (tcpConnection:tcp.Connection) : void => {
this.pool_.openDataChannel()
.then((channel:peerconnection.DataChannel) => {
var channelLabel = channel.getLabel();
log.info('associating channel %1 with new SOCKS client', channelLabel);
var session = new Session();
session.start(
tcpConnection,
channel,
this.bytesSentToPeer_,
this.bytesReceivedFromPeer_)
.catch((e:Error) => {
log.warn('session %1 failed to connect to remote endpoint: %2', [
channelLabel, e.message]);
});
session.onceStopped.then(() => {
log.info('discarded session %1', channelLabel);
}, (e:Error) => {
log.error('session %1 terminated with error: %2', [
channelLabel, e.message]);
});
}, (e:Error) => {
log.error('failed to open channel for new SOCKS client: %1 ',
e.message);
// TODO: return bytes to the client!
});
}
public handleSignalFromPeer = (signal:signals.Message)
: void => {
this.peerConnection_.handleSignalMessage(signal);
}
public toString = () : string => {
var ret :string;
var sessionsAsStrings :string[] = [];
var label :string;
ret = JSON.stringify({ tcpServer_: this.tcpServer_.toString() });
return ret;
}
} // class SocksToRtc
// A Socks sesson links a Tcp connection to a particular data channel on the
// peer connection. CONSIDER: when we have a lightweight webrtc provider, we
// can use the DataChannel class directly here instead of the awkward pairing
// of peerConnection with chanelLabel.
export class Session {
private tcpConnection_ :tcp.Connection;
private dataChannel_ :peerconnection.DataChannel;
private bytesSentToPeer_ :handler.Queue;
private bytesReceivedFromPeer_ :handler.Queue;
// Fulfills once the SOCKS negotiation process has successfully completed.
// Rejects if negotiation fails for any reason.
public onceReady :Promise;
// Call this to initiate shutdown.
private fulfillStopping_ :() => void;
private onceStopping_ = new Promise((F, R) => {
this.fulfillStopping_ = F;
});
// Fulfills once the SOCKS session has terminated and the TCP connection
// and datachannel have been shutdown.
// This can happen in response to:
// - startup (negotiation) failure
// - TCP connection or datachannel termination
// - manual invocation of stop()
// Should never reject.
public onceStopped :Promise;
// The supplied TCP connection and datachannel must already be
// successfully established.
// Returns onceReady.
public start = (
tcpConnection:tcp.Connection,
dataChannel:peerconnection.DataChannel,
bytesSentToPeer:handler.Queue,
bytesReceivedFromPeer:handler.Queue)
: Promise => {
this.tcpConnection_ = tcpConnection;
this.dataChannel_ = dataChannel;
this.bytesSentToPeer_ = bytesSentToPeer;
this.bytesReceivedFromPeer_ = bytesReceivedFromPeer;
// The session is ready once we've completed both
// auth and request handshakes.
this.onceReady = this.doAuthHandshake_().then(
this.doRequestHandshake_).then((response:socks.Response) => {
if (response.reply !== socks.Reply.SUCCEEDED) {
throw new Error('handshake failed with reply code ' +
socks.Reply[response.reply]);
}
log.info('%1: connected to remote host', [this.longId()]);
log.debug('%1: remote peer bound address: %2', [
this.longId(),
JSON.stringify(response.endpoint)]);
});
// Once the handshakes have completed, start forwarding data between the
// socket and channel and listen for socket and channel termination.
// If handshake fails, shutdown.
this.onceReady.then(() => {
this.linkSocketAndChannel_();
// Shutdown once the data channel terminates.
this.dataChannel_.onceClosed.then(() => {
if (this.dataChannel_.dataFromPeerQueue.getLength() > 0) {
log.warn('%1: channel closed with %2 unprocessed incoming messages',
this.longId(), this.dataChannel_.dataFromPeerQueue.getLength());
} else {
log.info('%1: channel closed', this.longId());
}
this.fulfillStopping_();
});
}, this.fulfillStopping_);
// Once shutdown has been requested, free resources.
// TODO: This promise is only used for logging, so we should probably
// remove it, or at least make it private.
this.onceStopped = this.onceStopping_.then(this.stopResources_);
return this.onceReady;
}
public longId = () : string => {
return 'session ' + this.channelLabel() + ' (socket ' +
this.tcpConnection_.connectionId + ' ' +
(this.tcpConnection_.isClosed() ? 'closed' : 'open') + ')';
}
// Initiates shutdown of the TCP server and peerconnection.
// Returns onceStopped.
public stop = () : Promise => {
log.debug('%1: stop requested', [this.longId()]);
this.fulfillStopping_();
return this.onceStopped;
}
// Closes the TCP connection and datachannel if they haven't already
// closed, fulfilling once both have closed. Since neither object's
// close() methods should ever reject, this should never reject.
private stopResources_ = () : Promise => {
log.debug('%1: freeing resources', [this.longId()]);
// DataChannel.close() returns void, implying that it is
// effectively immediate. However, we wrap it in a promise to ensure
// that any exception is sent to the Promise.catch, rather than
// propagating synchronously up the stack.
return Promise.all([]>[
new Promise((F, R) => { this.dataChannel_.close(); F(); }),
this.tcpConnection_.close()
]).then((discard:any) => {});
}
public channelLabel = () : string => {
return this.dataChannel_.getLabel();
}
public toString = () : string => {
return JSON.stringify({
channelLabel_: this.channelLabel(),
tcpConnection: this.tcpConnection_.toString()
});
}
// Receive a socks connection and send the initial Auth messages.
// Assumes: no packet fragmentation.
// TODO: send failure to client if auth fails
// TODO: handle packet fragmentation:
// https://github.com/uProxy/uproxy/issues/323
// TODO: Needs unit tests badly since it's mocked by several other tests.
private doAuthHandshake_ = ()
: Promise => {
return this.tcpConnection_.receiveNext()
.then(socks.interpretAuthHandshakeBuffer)
.then((auths:socks.Auth[]) => {
this.tcpConnection_.send(
socks.composeAuthResponse(socks.Auth.NOAUTH));
});
}
// Handles the SOCKS handshake, fulfilling with the socks.Response instance
// sent to the SOCKS client iff all the following steps succeed:
// - reads the next packet from the socket
// - parses this packet as a socks.Request instance
// - pauses the socket to avoid receiving data before it can be forwarded
// - forwards this to RtcToNet
// - receives the next message from the channel
// - parses this message as a socks.Response instance
// - forwards the socks.Response to the SOCKS client
// If a response is not received from RtcToNet or any other error
// occurs then we send a generic FAILURE response back to the SOCKS
// client before rejecting.
// TODO: Needs unit tests badly since it's mocked by several other tests.
private doRequestHandshake_ = () : Promise => {
return this.tcpConnection_.receiveNext()
.then(socks.interpretRequestBuffer)
.then((request:socks.Request) => {
// The domain name is very sensitive, so we keep it out of the
// info-level logs, which may be uploaded.
log.debug('%1: received endpoint from SOCKS client: %2', [
this.longId(), JSON.stringify(request.endpoint)]);
this.tcpConnection_.pause();
return this.dataChannel_.send({ str: JSON.stringify(request) });
})
.then(() => {
// Equivalent to channel.receiveNext(), if it existed.
return new Promise((F, R) => {
this.dataChannel_.dataFromPeerQueue.setSyncNextHandler(F).catch(R);
});
})
.then((data:peerconnection.Data) => {
if (!data.str) {
throw new Error('received non-string data from peer ' +
'during handshake: ' + JSON.stringify(data));
}
try {
var response :socks.Response = JSON.parse(data.str);
if (!socks.isValidResponse(response)) {
throw new Error('invalid response received from peer ' +
'during handshake: ' + data.str);
}
return response;
} catch (e) {
throw new Error('could not parse response from peer: ' + e.message);
}
})
.catch((e:Error) => {
log.debug('%1: unexpected failure during handshake, ' +
'returning generic FAILURE to SOCKS client: %2', [
this.longId(),
e.message]);
return {
reply: socks.Reply.FAILURE
};
})
.then((response:socks.Response) => {
return this.tcpConnection_.send(socks.composeResponseBuffer(
response)).then((discard:any) => { return response; });
});
}
// Sends a packet over the data channel.
// Invoked when a packet is received over the TCP socket.
private sendOnChannel_ = (data:ArrayBuffer) : Promise => {
return this.dataChannel_.send({buffer: data});
}
// Sends a packet over the TCP socket.
// Invoked when a packet is received over the data channel.
private sendOnSocket_ = (data:peerconnection.Data)
: Promise => {
if (!data.buffer) {
return Promise.reject(new Error(
'received non-buffer data from datachannel'));
}
this.bytesReceivedFromPeer_.handle(data.buffer.byteLength);
return this.tcpConnection_.send(data.buffer);
}
// Configures forwarding of data from the TCP socket over the data channel
// and vice versa. Should only be called once both socket and channel have
// been successfully established.
private linkSocketAndChannel_ = () : void => {
log.info('%1: linking socket and channel', this.longId());
var socketReader = (data:ArrayBuffer) => {
this.sendOnChannel_(data).then(() => {
this.bytesSentToPeer_.handle(data.byteLength);
}, (e:Error) => {
log.error('%1: failed to send data on datachannel: %2',
this.longId(),
e.message);
});
};
this.tcpConnection_.dataFromSocketQueue.setSyncHandler(socketReader);
// Shutdown the session once the TCP connection terminates.
// This should be safe now because
// (1) this.tcpConnection_.dataFromPeerQueue has now been emptied into
// this.dataChannel_.send() and (2) this.dataChannel_.close() should delay
// closing until all pending messages have been sent.
this.tcpConnection_.onceClosed.then((kind:tcp.SocketCloseKind) => {
log.info('%1: socket closed (%2)',
this.longId(),
tcp.SocketCloseKind[kind]);
this.fulfillStopping_();
});
var channelReader = (data:peerconnection.Data) : void => {
this.sendOnSocket_(data).catch((e:{ errcode: string }) => {
// TODO: e is actually a freedom.Error (uproxy-lib 20+)
// errcode values are defined here:
// https://github.com/freedomjs/freedom/blob/master/interface/core.tcpsocket.json
if (e.errcode === 'NOT_CONNECTED') {
// This can happen if, for example, there was still data to be
// read on the datachannel's queue when the socket closed.
log.warn('%1: tried to send data on closed socket: %2', [
this.longId(),
e.errcode]);
} else {
log.error('%1: failed to send data on socket: %2', [
this.longId(),
e]);
}
});
};
this.dataChannel_.dataFromPeerQueue.setSyncHandler(channelReader);
// The TCP connection starts in the paused state. However, in extreme
// cases, enough data can arrive before the pause takes effect to put
// the data channel into overflow. In that case, the socket will
// eventually be resumed by the overflow listener below.
if (!this.dataChannel_.isInOverflow()) {
this.tcpConnection_.resume();
}
this.dataChannel_.setOverflowListener((overflow:boolean) => {
if (this.tcpConnection_.isClosed()) {
return;
}
if (overflow) {
this.tcpConnection_.pause();
log.debug('%1: Hit overflow, pausing socket', this.longId());
} else {
this.tcpConnection_.resume();
log.debug('%1: Exited overflow, resuming socket', this.longId());
}
});
}
} // Session
} // module SocksToRtc
export = SocksToRtc;