/// /// import freedomMocker = require('../../../third_party/uproxy-lib/freedom/mocks/mock-freedom-in-module-env'); freedom = freedomMocker.makeMockFreedomInModuleEnv(); 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 socks_to_rtc = require('./socks-to-rtc'); import net = require('../net/net.types'); import tcp = require('../net/tcp'); import socks = require('../socks-common/socks-headers'); import logging = require('../../../third_party/uproxy-lib/logging/logging'); var log :logging.Log = new logging.Log('socks-to-rtc spec'); var mockEndpoint :net.Endpoint = { address: '127.0.0.1', port: 1234 }; var mockRemoteEndpoint :net.Endpoint = { // This address and port are both reserved for testing. address: '192.0.2.111', port: 1023 }; var mockConnectionInfo :tcp.ConnectionInfo = { bound: mockEndpoint, remote: mockRemoteEndpoint }; var voidPromise = Promise.resolve(); // Neither fulfills nor rejects. // Useful in a bunch of tests where a promise must be returned // for chaining purposes. var noopPromise = new Promise((F, R) => {}); describe('SOCKS server', function() { var server :socks_to_rtc.SocksToRtc; var onceServerStopped :() => Promise; var mockTcpServer :tcp.Server; var mockPeerConnection :peerconnection.PeerConnection; beforeEach(function() { server = new socks_to_rtc.SocksToRtc(); var serverStopped = new Promise((F, R) => { server.on('stopped', F); }); onceServerStopped = () => { return serverStopped; }; // TODO: create named more fleshed out TcpServer and PeerConnection mock // classes for testing. e.g. failing to listen mock, listen & gets // connection, listen and connection drops, etc. mockTcpServer = jasmine.createSpyObj('tcp server', [ 'on', 'onceListening', 'shutdown', 'onceShutdown', 'isShutdown' ]); // TODO: make a real mock of listen; this one is frgaile to implementation // changes and tests that might call onceListening before listen. mockTcpServer.listen = () => { return mockTcpServer.onceListening(); } mockTcpServer.connectionsQueue = new handler.Queue(); mockPeerConnection = { dataChannels: {}, peerOpenedChannelQueue: new handler.Queue(), signalForPeerQueue: new handler.Queue(), negotiateConnection: jasmine.createSpy('negotiateConnection'), onceConnecting: noopPromise, onceConnected: noopPromise, onceDisconnected: noopPromise, close: jasmine.createSpy('close') }; }); it('onceReady fulfills with server endpoint on server and peerconnection success', (done) => { (mockTcpServer.onceListening).and.returnValue(Promise.resolve(mockEndpoint)); mockPeerConnection.onceConnected = voidPromise; // We're not testing termination. (mockTcpServer.onceShutdown).and.returnValue(noopPromise); server.start(mockTcpServer, mockPeerConnection) .then((result:net.Endpoint) => { expect(result.address).toEqual(mockEndpoint.address); expect(result.port).toEqual(mockEndpoint.port); }) .then(done); }); it('onceReady rejects and \'stopped\' fires on socket setup failure', (done) => { (mockTcpServer.onceListening) .and.returnValue(Promise.reject(new Error('could not allocate port'))); (mockTcpServer.onceShutdown).and.returnValue(Promise.resolve()); server.start(mockTcpServer, mockPeerConnection).catch(onceServerStopped).then(done); }); it('\'stopped\' fires, and start fails, on early peerconnection termination', (done) => { (mockTcpServer.onceListening).and.returnValue(Promise.resolve(mockEndpoint)); (mockTcpServer.onceShutdown).and.returnValue(voidPromise); mockPeerConnection.onceConnected = voidPromise; mockPeerConnection.onceDisconnected = voidPromise; server.start(mockTcpServer, mockPeerConnection).catch(onceServerStopped).then(done); }); it('\'stopped\' fires on peerconnection termination', (done) => { var terminate :() => void; var terminatePromise = new Promise((F, R) => { terminate = F; }); (mockTcpServer.onceListening).and.returnValue(Promise.resolve(mockEndpoint)); (mockTcpServer.onceShutdown).and.returnValue(terminatePromise); mockPeerConnection.onceConnected = voidPromise; mockPeerConnection.onceDisconnected = terminatePromise; server.start(mockTcpServer, mockPeerConnection).then(onceServerStopped).then(done); terminate(); }); it('\'stopped\' fires on call to stop', (done) => { (mockTcpServer.onceListening).and.returnValue(Promise.resolve(mockEndpoint)); mockPeerConnection.onceConnected = voidPromise; // Neither TCP connection nor datachannel close "naturally". (mockTcpServer.onceShutdown).and.returnValue(noopPromise); server.start(mockTcpServer, mockPeerConnection).then( server.stop).then(onceServerStopped).then(done); }); it('stop works before the PeerConnection or TcpServer connects', (done) => { (mockTcpServer.onceListening).and.returnValue(noopPromise); // PeerConnection never connects. mockPeerConnection.onceConnected = noopPromise; // Neither TCP connection nor datachannel close "naturally". (mockTcpServer.onceShutdown).and.returnValue(noopPromise); var onceStartFailed :Promise = new Promise((F, R) => { server.start(mockTcpServer, mockPeerConnection).then(R, F); }); Promise.all([onceStartFailed, server.stop()]).then(done); }); }); describe("SOCKS session", function() { var session :socks_to_rtc.Session; var mockBytesReceived :handler.Queue; var mockBytesSent :handler.Queue; var mockDataChannel :peerconnection.DataChannel; var mockDataFromPeerQueue :handler.Queue; var mockTcpConnection :tcp.Connection; beforeEach(function() { session = new socks_to_rtc.Session(); mockTcpConnection = jasmine.createSpyObj('tcp connection', [ 'onceClosed', 'close', 'isClosed', 'send', 'pause', 'resume' ]); mockTcpConnection.dataFromSocketQueue = new handler.Queue(); (mockTcpConnection.close).and.returnValue(Promise.resolve(-1)); mockTcpConnection.onceClosed = Promise.resolve( tcp.SocketCloseKind.REMOTELY_CLOSED); (mockTcpConnection.send).and.returnValue(Promise.resolve({ bytesWritten: 1 })); mockDataFromPeerQueue = new handler.Queue(); mockDataChannel = { close: jasmine.createSpy('close'), dataFromPeerQueue: mockDataFromPeerQueue, getLabel: jasmine.createSpy('getLabel').and.returnValue('mock label'), onceClosed: noopPromise, onceOpened: noopPromise, send: jasmine.createSpy('send'), isInOverflow: jasmine.createSpy('isInOverflow').and.returnValue(false), setOverflowListener: jasmine.createSpy('setOverflowListener') }; (mockDataChannel.send).and.returnValue(voidPromise); mockBytesReceived = new handler.Queue(); mockBytesSent = new handler.Queue() }); it('onceReady fulfills on successful negotiation', (done) => { spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); session.start(mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived).then(done); }); it('onceReady rejects and onceStopped fulfills on unsuccessful negotiation', (done) => { spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.FAILURE})); session.start(mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived) .catch((e:Error) => { return session.onceStopped; }).then(done); }); it('onceStopped fulfills on TCP connection termination', (done) => { mockTcpConnection.onceClosed = Promise.resolve(); spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); session.start(mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived) .then(() => { return session.onceStopped; }).then(done); }); it('onceStopped fulfills on call to stop', (done) => { // Neither TCP connection nor datachannel close "naturally". mockTcpConnection.onceClosed = new Promise((F, R) => {}); spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); session.start(mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived) .then(session.stop).then(() => { return session.onceStopped; }).then(done); }); it('bytes sent counter', (done) => { // Neither TCP connection nor datachannel close "naturally". mockTcpConnection.onceClosed = new Promise((F, R) => {}); spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); var buffer = new Uint8Array([1,2,3]).buffer; session.start( mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived).then(() => { mockTcpConnection.dataFromSocketQueue.handle(buffer); }); mockBytesSent.setSyncNextHandler((numBytes:number) => { expect(numBytes).toEqual(buffer.byteLength); done(); }); }); it('bytes received counter', (done) => { // Neither TCP connection nor datachannel close "naturally". mockTcpConnection.onceClosed = new Promise((F, R) => {}); spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); var message :peerconnection.Data = { buffer: new Uint8Array([1,2,3]).buffer }; session.start( mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived).then(() => { mockDataFromPeerQueue.handle(message); }); mockBytesReceived.setSyncNextHandler((numBytes:number) => { expect(numBytes).toEqual(message.buffer.byteLength); done(); }); }); it('channel queue drains before termination', (done) => { // TCP connection doesn't close "naturally" but the data // channel is already closed when the session is started. mockTcpConnection.onceClosed = new Promise((F, R) => {}); mockDataChannel.onceClosed = voidPromise; spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); var message :peerconnection.Data = { buffer: new Uint8Array([1,2,3]).buffer }; var onceMessageHandled = mockDataFromPeerQueue.handle(message); session.start( mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived); session.onceStopped.then(() => { return onceMessageHandled; }).then(() => { expect(mockDataChannel.dataFromPeerQueue.getLength()).toEqual(0); done(); }); }); it('socket queue drains before termination', (done) => { // The data channel doesn't close "naturally" but the // TCP connection is already closed when the session is started. mockTcpConnection.onceClosed = Promise.resolve(tcp.SocketCloseKind.WE_CLOSED_IT); (mockTcpConnection.isClosed).and.returnValue(true); mockDataChannel.onceClosed = noopPromise; spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); var buffer = new Uint8Array([1,2,3]).buffer; var onceMessageHandled = mockTcpConnection.dataFromSocketQueue.handle(buffer); session.start( mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived); session.onceStopped.then(() => { return onceMessageHandled; }).then(() => { expect(mockTcpConnection.dataFromSocketQueue.getLength()).toEqual(0); done(); }); }); it('backpressure', (done) => { spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); mockTcpConnection.onceConnected = Promise.resolve(mockConnectionInfo); mockTcpConnection.onceClosed = new Promise((F, R) => {}); var overflowListener :(overflow:boolean) => void; mockDataChannel.setOverflowListener = (listener) => { overflowListener = listener; }; var buffer = new Uint8Array([1,2,3]).buffer; // Messages received before start sit in the TCP receive queue. mockTcpConnection.dataFromSocketQueue.handle(buffer); expect(mockTcpConnection.dataFromSocketQueue.getLength()).toEqual(1); expect(mockDataChannel.send).not.toHaveBeenCalled(); session.start(mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived).then(() => { // After start, the TCP queue should be drained into the datachannel. expect(mockTcpConnection.dataFromSocketQueue.getLength()).toEqual(0); expect(mockDataChannel.send).toHaveBeenCalled(); // After draining the queue, the TCP connection should be resumed. expect(mockTcpConnection.pause).not.toHaveBeenCalled(); expect(mockTcpConnection.resume).toHaveBeenCalled(); // Enter overflow state. This should trigger a call to pause. overflowListener(true); expect(mockTcpConnection.pause).toHaveBeenCalled(); // In the paused state, messages are still forwarded mockTcpConnection.dataFromSocketQueue.handle(buffer); expect(mockTcpConnection.dataFromSocketQueue.getLength()).toEqual(0); expect((mockDataChannel.send).calls.count()).toEqual(2); // Exit overflow state. This should trigger a call to resume. overflowListener(false); expect((mockTcpConnection.resume).calls.count()).toEqual(2); done(); }); }); it('backpressure with early flood', (done) => { spyOn(session, 'doAuthHandshake_').and.returnValue(Promise.resolve()); spyOn(session, 'doRequestHandshake_').and.returnValue( Promise.resolve({reply: socks.Reply.SUCCEEDED})); mockTcpConnection.onceConnected = Promise.resolve(mockConnectionInfo); mockTcpConnection.onceClosed = new Promise((F, R) => {}); var overflowListener :(overflow:boolean) => void; mockDataChannel.setOverflowListener = (listener) => { overflowListener = listener; }; mockDataChannel.isInOverflow = jasmine.createSpy('isInOverflow').and.returnValue(true); var buffer = new Uint8Array([1,2,3]).buffer; // Messages received before start sit in the TCP receive queue. mockTcpConnection.dataFromSocketQueue.handle(buffer); expect(mockTcpConnection.dataFromSocketQueue.getLength()).toEqual(1); expect(mockDataChannel.send).not.toHaveBeenCalled(); session.start(mockTcpConnection, mockDataChannel, mockBytesSent, mockBytesReceived).then(() => { // After start, the TCP queue should be drained into the datachannel. expect(mockTcpConnection.dataFromSocketQueue.getLength()).toEqual(0); expect(mockDataChannel.send).toHaveBeenCalled(); // If the initial queue is enough to trigger overflow, then the // socket should not be resumed. expect(mockTcpConnection.pause).not.toHaveBeenCalled(); expect(mockTcpConnection.resume).not.toHaveBeenCalled(); // Exit overflow state. This should trigger a call to resume. overflowListener(false); expect(mockTcpConnection.resume).toHaveBeenCalled(); done(); }); }); });