2015-12-04 17:44:16 +00:00
|
|
|
/**
|
2015-12-23 18:56:33 +00:00
|
|
|
* Copyright (c) 2015-present, Peel Technologies, Inc.
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
2015-12-04 17:44:16 +00:00
|
|
|
* @providesModule TcpSocket
|
|
|
|
* @flow
|
|
|
|
*/
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var inherits = require('inherits');
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
2015-12-15 00:16:45 +00:00
|
|
|
var ipRegex = require('ip-regex');
|
2015-12-04 17:44:16 +00:00
|
|
|
var {
|
|
|
|
DeviceEventEmitter,
|
|
|
|
NativeModules
|
|
|
|
} = require('react-native');
|
|
|
|
var Sockets = NativeModules.TcpSockets;
|
|
|
|
var base64 = require('base64-js');
|
2015-12-17 16:10:00 +00:00
|
|
|
var Base64Str = require('./base64-str');
|
2015-12-04 17:44:16 +00:00
|
|
|
var noop = function () {};
|
2015-12-22 23:53:08 +00:00
|
|
|
var usedIds = [];
|
2015-12-04 17:44:16 +00:00
|
|
|
var STATE = {
|
|
|
|
DISCONNECTED: 0,
|
|
|
|
CONNECTING: 1,
|
|
|
|
CONNECTED: 2
|
|
|
|
};
|
|
|
|
|
2015-12-23 18:56:33 +00:00
|
|
|
function TcpSocket(options: ?any) {
|
|
|
|
// $FlowFixMe: suppressing this error flow doesn't like EventEmitter
|
2015-12-04 17:44:16 +00:00
|
|
|
EventEmitter.call(this);
|
|
|
|
|
2015-12-22 23:53:08 +00:00
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
var nativeSocket = false;
|
|
|
|
if (!options._id) {
|
|
|
|
// javascript generated sockets range from 1-1000
|
|
|
|
this._id = Math.floor((Math.random() * 1000) + 1);
|
|
|
|
while (usedIds.indexOf(this._id) !== -1) {
|
|
|
|
this._id = Math.floor((Math.random() * 1000) + 1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// native generated sockets range from 5000-6000
|
|
|
|
// e.g. incoming server connections
|
|
|
|
this._id = options._id;
|
|
|
|
nativeSocket = true;
|
|
|
|
}
|
|
|
|
usedIds.push(this._id);
|
|
|
|
|
|
|
|
this._state = nativeSocket ? STATE.CONNECTED : STATE.DISCONNECTED;
|
2015-12-04 17:44:16 +00:00
|
|
|
this._host = null;
|
|
|
|
|
|
|
|
// these will be set once there is a connection
|
|
|
|
this.readable = this.writable = false;
|
|
|
|
|
|
|
|
this._subscription = DeviceEventEmitter.addListener(
|
2015-12-15 00:16:45 +00:00
|
|
|
'tcp-' + this._id + '-event', this._onEvent.bind(this)
|
2015-12-04 17:44:16 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// ensure compatibility with node's EventEmitter
|
|
|
|
if (!this.on) {
|
|
|
|
this.on = this.addListener.bind(this);
|
|
|
|
}
|
|
|
|
|
2015-12-22 23:53:08 +00:00
|
|
|
if (nativeSocket === false) {
|
|
|
|
Sockets.createSocket(this._id);
|
|
|
|
}
|
2015-12-04 17:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
inherits(TcpSocket, EventEmitter);
|
|
|
|
|
|
|
|
TcpSocket.prototype._debug = function() {
|
|
|
|
if (__DEV__) {
|
|
|
|
var args = [].slice.call(arguments);
|
|
|
|
args.unshift('socket-' + this._id);
|
|
|
|
console.log.apply(console, args);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-12-23 18:56:33 +00:00
|
|
|
TcpSocket.prototype.connect = function(options: { port: number, host: ?string, localAddress: ?string, localPort: ?number, family: ?number }, callback: ?() => void) {
|
2015-12-04 17:44:16 +00:00
|
|
|
if (this._state !== STATE.DISCONNECTED) {
|
|
|
|
throw new Error('Socket is already bound');
|
|
|
|
}
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
if (typeof callback === 'function') {
|
2015-12-22 23:53:08 +00:00
|
|
|
this.once('connect', callback);
|
2015-12-15 00:16:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var host = options.host || 'localhost';
|
|
|
|
var port = options.port;
|
|
|
|
var localAddress = options.localAddress;
|
|
|
|
var localPort = options.localPort;
|
|
|
|
|
|
|
|
if (localAddress && !ipRegex({exact: true}).test(localAddress)) {
|
|
|
|
throw new TypeError('"localAddress" option must be a valid IP: ' + localAddress);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (localPort && typeof localPort !== 'number') {
|
|
|
|
throw new TypeError('"localPort" option should be a number: ' + localPort);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof port !== 'undefined') {
|
|
|
|
if (typeof port !== 'number' && typeof port !== 'string') {
|
|
|
|
throw new TypeError('"port" option should be a number or string: ' + port);
|
|
|
|
}
|
|
|
|
if (!isLegalPort(port)) {
|
|
|
|
throw new RangeError('"port" option should be >= 0 and < 65536: ' + port);
|
|
|
|
}
|
2015-12-04 17:44:16 +00:00
|
|
|
}
|
2015-12-16 23:05:08 +00:00
|
|
|
port |= port;
|
2015-12-04 17:44:16 +00:00
|
|
|
|
|
|
|
this._state = STATE.CONNECTING;
|
|
|
|
this._debug('connecting, host:', host, 'port:', port);
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
Sockets.connect(this._id, host, Number(port), options);
|
2015-12-04 17:44:16 +00:00
|
|
|
};
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
// Check that the port number is not NaN when coerced to a number,
|
|
|
|
// is an integer and that it falls within the legal range of port numbers.
|
2015-12-23 18:56:33 +00:00
|
|
|
function isLegalPort(port: number) : boolean {
|
2015-12-15 00:16:45 +00:00
|
|
|
if (typeof port === 'string' && port.trim() === '') {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return +port === (port >>> 0) && port >= 0 && port <= 0xFFFF;
|
|
|
|
}
|
|
|
|
|
2015-12-23 18:56:33 +00:00
|
|
|
TcpSocket.prototype.setTimeout = function(msecs: number, callback: () => void) {
|
2015-12-17 16:10:00 +00:00
|
|
|
var self = this;
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
if (this._timeout) {
|
|
|
|
clearTimeout(this._timeout);
|
|
|
|
this._timeout = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (msecs > 0) {
|
|
|
|
if (callback) {
|
|
|
|
this.once('timeout', callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
var self = this;
|
2015-12-23 18:56:33 +00:00
|
|
|
this._timeout = setTimeout(function() {
|
2015-12-15 00:16:45 +00:00
|
|
|
self.emit('timeout');
|
2015-12-17 16:10:00 +00:00
|
|
|
self._timeout = null;
|
|
|
|
self.destroy();
|
2015-12-23 18:56:33 +00:00
|
|
|
}, msecs);
|
2015-12-15 00:16:45 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.setNoDelay = function(noDelay) {
|
2015-12-04 17:44:16 +00:00
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
TcpSocket.prototype.setEncoding = function(encoding) {
|
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.setKeepAlive = function(enable, initialDelay) {
|
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.pause = function() {
|
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.resume = function() {
|
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.ref = function() {
|
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.unref = function() {
|
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.address = function() {
|
|
|
|
// nothing yet
|
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.end = function(data, encoding) {
|
2015-12-04 17:44:16 +00:00
|
|
|
if (this._destroyed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
if (data) {
|
|
|
|
this.write(data, encoding);
|
|
|
|
}
|
|
|
|
|
2015-12-04 17:44:16 +00:00
|
|
|
this._destroyed = true;
|
|
|
|
this._debug('closing');
|
|
|
|
this._subscription.remove();
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
Sockets.end(this._id, this._debug.bind(this, 'closed'));
|
2015-12-04 17:44:16 +00:00
|
|
|
};
|
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
TcpSocket.prototype.destroy = function() {
|
2015-12-17 16:10:00 +00:00
|
|
|
if (!this._destroyed) {
|
|
|
|
this._destroyed = true;
|
|
|
|
this._debug('destroying');
|
|
|
|
this._subscription.remove();
|
2015-12-04 17:44:16 +00:00
|
|
|
|
2015-12-17 16:10:00 +00:00
|
|
|
Sockets.destroy(this._id, this._debug.bind(this, 'closed'));
|
|
|
|
}
|
2015-12-15 00:16:45 +00:00
|
|
|
};
|
2015-12-04 17:44:16 +00:00
|
|
|
|
2015-12-23 18:56:33 +00:00
|
|
|
TcpSocket.prototype._onEvent = function(info: { event: string, data: ?any }) {
|
2015-12-15 00:16:45 +00:00
|
|
|
this._debug('received', info.event);
|
|
|
|
|
|
|
|
if (info.event === 'connect') {
|
2015-12-16 23:05:08 +00:00
|
|
|
this.writable = this.readable = true;
|
|
|
|
this._state = STATE.CONNECTED;
|
2015-12-15 00:16:45 +00:00
|
|
|
} else if (info.event === 'data') {
|
|
|
|
if (this._timeout) {
|
|
|
|
clearTimeout(this._timeout);
|
|
|
|
this._timeout = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// from base64 string
|
|
|
|
info.data = typeof Buffer === 'undefined'
|
|
|
|
? base64.toByteArray(info.data)
|
|
|
|
: new global.Buffer(info.data, 'base64');
|
|
|
|
} else if (info.event === 'close') {
|
2015-12-16 23:05:08 +00:00
|
|
|
this._state = STATE.DISCONNECTED;
|
2015-12-15 00:16:45 +00:00
|
|
|
}
|
2015-12-04 17:44:16 +00:00
|
|
|
|
2015-12-15 00:16:45 +00:00
|
|
|
this.emit(info.event, info.data);
|
2015-12-04 17:44:16 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
TcpSocket.prototype.write = function(buffer, encoding, callback) {
|
|
|
|
var self = this;
|
|
|
|
var encoded = false;
|
|
|
|
|
|
|
|
if (this._state === STATE.DISCONNECTED) {
|
|
|
|
throw new Error('Socket is not connected.');
|
|
|
|
} else if (this._state === STATE.CONNECTING) {
|
|
|
|
// we're ok, GCDAsyncSocket handles queueing internally
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof encoding === 'function') {
|
|
|
|
callback = encoding;
|
|
|
|
encoding = null;
|
|
|
|
}
|
|
|
|
callback = callback || noop;
|
|
|
|
var str;
|
|
|
|
if (typeof buffer === 'string') {
|
2015-12-17 16:10:00 +00:00
|
|
|
console.warn('socket.WRITE(): encoding as base64');
|
|
|
|
str = Base64Str.encode(buffer);
|
2015-12-04 17:44:16 +00:00
|
|
|
} else if (typeof Buffer !== 'undefined' && global.Buffer.isBuffer(buffer)) {
|
|
|
|
encoded = true;
|
|
|
|
str = buffer.toString('base64');
|
|
|
|
} else if (buffer instanceof Uint8Array || Array.isArray(buffer)) {
|
|
|
|
encoded = true;
|
|
|
|
str = base64.fromByteArray(buffer);
|
|
|
|
} else {
|
|
|
|
throw new Error('invalid message format');
|
|
|
|
}
|
|
|
|
|
|
|
|
Sockets.write(this._id, str, encoded, function(err) {
|
2015-12-17 16:10:00 +00:00
|
|
|
if (self._timeout) {
|
|
|
|
clearTimeout(self._timeout);
|
|
|
|
self._timeout = null;
|
|
|
|
}
|
|
|
|
|
2015-12-04 17:44:16 +00:00
|
|
|
err = normalizeError(err);
|
|
|
|
if (err) {
|
2015-12-17 16:10:00 +00:00
|
|
|
self._debug('write failed', err);
|
2015-12-04 17:44:16 +00:00
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-12-23 18:56:33 +00:00
|
|
|
function normalizeError(err) {
|
2015-12-04 17:44:16 +00:00
|
|
|
if (err) {
|
|
|
|
if (typeof err === 'string') {
|
|
|
|
err = new Error(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
}
|
2015-12-22 23:53:08 +00:00
|
|
|
|
2015-12-23 18:56:33 +00:00
|
|
|
module.exports = TcpSocket;
|