PageRenderTime 1ms CodeModel.GetById 14ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/vock/socket.js

https://github.com/prodigeni/vock
JavaScript | 235 lines | 158 code | 32 blank | 45 comment | 13 complexity | a67ded291c3008122a106f18a970a69d MD5 | raw file
  1var util = require('util'),
  2    dgram = require('dgram'),
  3    net = require('net'),
  4    msgpack = require('msgpack-js'),
  5    pmp = require('nat-pmp'),
  6    natUpnp = require('nat-upnp'),
  7    netroute = require('netroute'),
  8    EventEmitter = require('events').EventEmitter;
  9
 10var socket = exports;
 11
 12//
 13// ### function Socket (options)
 14// #### @options {Object} Socket options
 15// msgpack encoder/decoder wrapper over udp socket
 16//
 17function Socket(options) {
 18  EventEmitter.call(this);
 19
 20  this.tsocket = null;
 21  this.socket = null;
 22  this.port = null;
 23
 24  this.server = options.server;
 25  this.relayVersion = [0, 1];
 26  this.relaySeq = 0;
 27  this._initQueue = [];
 28  this._initialized = false;
 29
 30  this.natTtl = 60 * 30;
 31
 32  this.init();
 33};
 34util.inherits(Socket, EventEmitter);
 35
 36//
 37// ### function create (options)
 38// #### @options {Object} Socket options
 39// Constructor wrapper
 40//
 41socket.create = function create(options) {
 42  return new Socket(options);
 43};
 44
 45//
 46// ### function bind (port, callback)
 47// #### @ports {Array} Port numbers
 48// #### @callback {Function} Continuation to proceed to
 49Socket.prototype.bind = function bind(ports, callback) {
 50  var self = this,
 51      port = ports[0] || 0;
 52
 53  if (this.tsocket) {
 54    this.tsocket.removeAllListeners('data');
 55    this.tsocket.removeAllListeners('error');
 56    this.tsocket.removeAllListeners('listening');
 57    try {
 58      this.tsocket.close();
 59    } catch(e) {
 60    }
 61  }
 62
 63  // Bind TCP socket
 64  this.tsocket = net.createServer();
 65  this.tsocket.on('error', function(err) {
 66    // Try to bind to a random port
 67    self.bind(ports.slice(1), callback);
 68  });
 69  this.tsocket.listen(port, function() {
 70    self.tsocket.removeAllListeners('error');
 71    self.port = self.tsocket.address().port;
 72
 73    // Bind UDP socket
 74    if (self.socket) {
 75      self.socket.removeAllListeners('message');
 76      self.socket.removeAllListeners('listening');
 77      try {
 78        self.socket.close();
 79      } catch(e) {
 80      }
 81    }
 82
 83    self.socket = dgram.createSocket('udp4');
 84    self.socket.on('message', self.ondata.bind(self));
 85    self.socket.once('listening', function() {
 86      callback(self.port, port !== 0);
 87    });
 88
 89    try {
 90      self.socket.bind(self.port);
 91    } catch (e) {
 92      self.bind(ports.slice(1), callback);
 93    }
 94  });
 95};
 96
 97//
 98// ### function init ()
 99// Internal
100//
101Socket.prototype.init = function init() {
102  var self = this;
103
104  this.upnpClient = natUpnp.createClient();
105  this.pmpClient = pmp.connect(netroute.getGateway());
106
107  this.upnpClient.getMappings({
108    local: true,
109    description: /vock/i
110  }, function(err, list) {
111    list = list || [];
112
113    self.bind(list.map(function(item) {
114      return item.private.port;
115    }), function(port, reuse) {
116      self.emit('init', port);
117
118      // Unwind accumulated callbacks
119      var queue = self._initQueue;
120      self._initialized = true;
121      self._initQueue = [];
122      queue.forEach(function(callback) {
123        callback.call(self);
124      });
125
126      // Do not add mappings if we've found existing one
127      if (reuse) {
128        self.emit('nat:traversal:udp', 'upnp', port);
129        self.emit('nat:traversal:tcp', 'upnp', port);
130        return;
131      }
132
133      // Create port-forward mapping if possible
134      var protocols = ['udp', 'tcp'];
135      protocols.forEach(function(protocol) {
136        self.upnpClient.portMapping({
137          public: port,
138          private: port,
139          protocol: protocol,
140          description: 'Vock - VoIP on node.js',
141          ttl: 0 // Unlimited, since most routers doesn't support other value
142        }, function(err) {
143          if (err) return;
144          self.emit('nat:traversal:' + protocol, 'upnp', port);
145        });
146
147        self.pmpClient.portMapping({
148          private: port,
149          public: port,
150          type: protocol,
151          ttl: self.natTtl
152        }, function(err) {
153          if (err) return;
154
155          self.emit('nat:traversal:' + protocol, 'pmp', port);
156        });
157      });
158    });
159  });
160};
161
162//
163// ### function send (packet, target)
164// #### @packet {Object} packet
165// #### @target {Object} target address
166// Sends encoded packet to target
167//
168Socket.prototype.send = function(packet, target) {
169  if (!this._initialized) {
170    this._initQueue.push(function() {
171      this.send(packet, target);
172    });
173    return;
174  }
175
176  try {
177    var raw = msgpack.encode(packet);
178  } catch (e) {
179    this.emit('error', e);
180    return;
181  }
182
183  this.socket.send(raw, 0, raw.length, target.port, target.address);
184};
185
186//
187// ### function relay (packet, id, target)
188// #### @packet {Object} packet
189// #### @id {String} room id
190// #### @target {Object} target address
191// Sends encoded packet to target through relay
192//
193Socket.prototype.relay = function relay(packet, id, target) {
194  var wrapper = {
195    protocol: 'relay',
196    seq: this.relaySeq++,
197    id: id,
198    to: { address: target.address, port: target.port },
199    body: packet,
200    version: this.relayVersion
201  };
202
203  this.send(wrapper, this.server);
204};
205
206//
207// ### function ondata (raw, addr)
208// #### @raw {Buffer} packet
209// #### @addr {Object} from address
210// Receives incoming relay or direct packet
211//
212Socket.prototype.ondata = function ondata(raw, addr) {
213  try {
214    var msg = msgpack.decode(raw);
215  } catch (e) {
216    // Ignore decode errors
217    return;
218  }
219
220  if (!msg) return this.emit('error', 'Empty message!');
221
222  addr.relay = false;
223
224  // Unwrap relay packets from server
225  if (msg.protocol === 'relay' &&
226      (addr.address == this.server.address ||
227       this.server.address == '0.0.0.0') &&
228      addr.port == this.server.port) {
229    addr = msg.from;
230    addr.relay = true;
231    msg = msg.body;
232  }
233
234  this.emit('data', msg, addr);
235};