1/*
2Copyright 2012 Google Inc.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8     http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15
16Author: Renato Mangini (mangini@chromium.org)
17*/
18
19const DEFAULT_MAX_CONNECTIONS=5;
20
21(function(exports) {
22
23  // Define some local variables here.
24  var socket = chrome.sockets.tcpServer;
25
26  /**
27   * Creates an instance of the client
28   *
29   * @param {String} host The remote host to connect to
30   * @param {Number} port The port to connect to at the remote host
31   */
32  function TcpServer(addr, port, options) {
33    this.addr = addr;
34    this.port = port;
35    this.maxConnections = typeof(options) != 'undefined'
36        && options.maxConnections || DEFAULT_MAX_CONNECTIONS;
37
38    this._onAccept = this._onAccept.bind(this);
39    this._onAcceptError = this._onAcceptError.bind(this);
40
41    // Callback functions.
42    this.callbacks = {
43      listen: null,    // Called when socket is connected.
44      connect: null,    // Called when socket is connected.
45      disconnect: null, // Called when socket is disconnected.
46      recv: null,       // Called when client receives data from server.
47      sent: null        // Called when client sends data to server.
48    };
49
50    // Sockets open
51    this.openSockets=[];
52
53    // server socket (one server connection, accepts and opens one socket per client)
54    this.serverSocketId = null;
55
56    log('initialized tcp server, not listening yet');
57  }
58
59
60  /**
61   * Static method to return available network interfaces.
62   *
63   * @see https://developer.chrome.com/apps/system_network#method-getNetworkInterfaces
64   *
65   * @param {Function} callback The function to call with the available network
66   * interfaces. The callback parameter is an array of
67   * {name(string), address(string)} objects. Use the address property of the
68   * preferred network as the addr parameter on TcpServer contructor.
69   */
70  TcpServer.getNetworkAddresses=function(callback) {
71    chrome.system.network.getNetworkInterfaces(callback);
72  }
73
74  TcpServer.prototype.isConnected=function() {
75    return this.serverSocketId > 0;
76  }
77
78  /**
79   * Connects to the TCP socket, and creates an open socket.
80   *
81   * @see https://developer.chrome.com/apps/sockets_tcpServer#method-create
82   * @param {Function} callback The function to call on connection
83   */
84  TcpServer.prototype.listen = function(callback) {
85    // Register connect callback.
86    this.callbacks.connect = callback;
87    socket.create({}, this._onCreate.bind(this));
88  };
89
90
91  /**
92   * Disconnects from the remote side
93   *
94   * @see https://developer.chrome.com/apps/sockets_tcpServer#method-disconnect
95   */
96  TcpServer.prototype.disconnect = function() {
97    if (this.serverSocketId) {
98      socket.onAccept.removeListener(this._onAccept);
99      socket.onAcceptError.removeListener(this._onAcceptError);
100      socket.close(this.serverSocketId);
101    }
102    for (var i=0; i<this.openSockets.length; i++) {
103      try {
104        this.openSockets[i].close();
105      } catch (ex) {
106        console.log(ex);
107      }
108    }
109    this.openSockets=[];
110    this.serverSocketId=0;
111  };
112
113  /**
114   * The callback function used for when we attempt to have Chrome
115   * create a socket. If the socket is successfully created
116   * we go ahead and start listening for incoming connections.
117   *
118   * @private
119   * @see https://developer.chrome.com/apps/sockets_tcpServer#method-listen
120   * @param {Object} createInfo The socket details
121   */
122  TcpServer.prototype._onCreate = function(createInfo) {
123    this.serverSocketId = createInfo.socketId;
124    if (this.serverSocketId > 0) {
125      socket.onAccept.addListener(this._onAccept);
126      socket.onAcceptError.addListener(this._onAcceptError);
127      socket.listen(this.serverSocketId, this.addr, this.port, 50,
128        this._onListenComplete.bind(this));
129      this.isListening = true;
130    } else {
131      error('Unable to create socket');
132    }
133  };
134
135  /**
136   * The callback function used for when we attempt to have Chrome
137   * connect to the remote side. If a successful connection is
138   * made then we accept it by opening it in a new socket (accept method)
139   *
140   * @private
141   */
142  TcpServer.prototype._onListenComplete = function(resultCode) {
143    if (resultCode !==0) {
144      error('Unable to listen to socket. Resultcode='+resultCode);
145    }
146  }
147
148  TcpServer.prototype._onAccept = function (info) {
149    if (info.socketId != this.serverSocketId)
150      return;
151
152    if (this.openSockets.length >= this.maxConnections) {
153      this._onNoMoreConnectionsAvailable(info.clientSocketId);
154      return;
155    }
156
157    var tcpConnection = new TcpConnection(info.clientSocketId);
158    this.openSockets.push(tcpConnection);
159
160    tcpConnection.requestSocketInfo(this._onSocketInfo.bind(this));
161    log('Incoming connection handled.');
162  }
163
164  TcpServer.prototype._onAcceptError = function(info) {
165    if (info.socketId != this.serverSocketId)
166      return;
167
168    error('Unable to accept incoming connection. Error code=' + info.resultCode);
169  }
170
171  TcpServer.prototype._onNoMoreConnectionsAvailable = function(socketId) {
172    var msg="No more connections available. Try again later\n";
173    _stringToArrayBuffer(msg, function(arrayBuffer) {
174      chrome.sockets.tcp.send(socketId, arrayBuffer,
175        function() {
176          chrome.sockets.tcp.close(socketId);
177        });
178    });
179  }
180
181  TcpServer.prototype._onSocketInfo = function(tcpConnection, socketInfo) {
182    if (this.callbacks.connect) {
183      this.callbacks.connect(tcpConnection, socketInfo);
184    }
185  }
186
187  /**
188   * Holds a connection to a client
189   *
190   * @param {number} socketId The ID of the server<->client socket
191   */
192  function TcpConnection(socketId) {
193    this.socketId = socketId;
194    this.socketInfo = null;
195
196    // Callback functions.
197    this.callbacks = {
198      disconnect: null, // Called when socket is disconnected.
199      recv: null,       // Called when client receives data from server.
200      sent: null        // Called when client sends data to server.
201    };
202
203    log('Established client connection. Listening...');
204
205  };
206
207  TcpConnection.prototype.setSocketInfo = function(socketInfo) {
208    this.socketInfo = socketInfo;
209  };
210
211  TcpConnection.prototype.requestSocketInfo = function(callback) {
212    chrome.sockets.tcp.getInfo(this.socketId,
213      this._onSocketInfo.bind(this, callback));
214  };
215
216  /**
217   * Add receive listeners for when a message is received
218   *
219   * @param {Function} callback The function to call when a message has arrived
220   */
221  TcpConnection.prototype.startListening = function(callback) {
222    this.callbacks.recv = callback;
223
224    // Add receive listeners.
225    this._onReceive = this._onReceive.bind(this);
226    this._onReceiveError = this._onReceiveError.bind(this);
227    chrome.sockets.tcp.onReceive.addListener(this._onReceive);
228    chrome.sockets.tcp.onReceiveError.addListener(this._onReceiveError);
229
230    chrome.sockets.tcp.setPaused(this.socketId, false);
231  };
232
233  /**
234   * Sets the callback for when a message is received
235   *
236   * @param {Function} callback The function to call when a message has arrived
237   */
238  TcpConnection.prototype.addDataReceivedListener = function(callback) {
239    // If this is the first time a callback is set, start listening for incoming data.
240    if (!this.callbacks.recv) {
241      this.startListening(callback);
242    } else {
243      this.callbacks.recv = callback;
244    }
245  };
246
247
248  /**
249   * Sends a message down the wire to the remote side
250   *
251   * @see https://developer.chrome.com/apps/sockets_tcp#method-send
252   * @param {String} msg The message to send
253   * @param {Function} callback The function to call when the message has sent
254   */
255  TcpConnection.prototype.sendMessage = function(msg, callback) {
256    _stringToArrayBuffer(msg + '\n', function(arrayBuffer) {
257      chrome.sockets.tcp.send(this.socketId, arrayBuffer, this._onWriteComplete.bind(this));
258    }.bind(this));
259
260    // Register sent callback.
261    this.callbacks.sent = callback;
262  };
263
264
265  /**
266   * Disconnects from the remote side
267   *
268   * @see https://developer.chrome.com/apps/sockets_tcp#method-close
269   */
270  TcpConnection.prototype.close = function() {
271    if (this.socketId) {
272      chrome.sockets.tcp.onReceive.removeListener(this._onReceive);
273      chrome.sockets.tcp.onReceiveError.removeListener(this._onReceiveError);
274      chrome.sockets.tcp.close(this.socketId);
275    }
276  };
277
278
279  /**
280   * Callback function for when socket details (socketInfo) is received.
281   * Stores the socketInfo for future reference and pass it to the
282   * callback sent in its parameter.
283   *
284   * @private
285   */
286  TcpConnection.prototype._onSocketInfo = function(callback, socketInfo) {
287    if (callback && typeof(callback)!='function') {
288      throw "Illegal value for callback: "+callback;
289    }
290    this.socketInfo = socketInfo;
291    callback(this, socketInfo);
292  }
293
294  /**
295   * Callback function for when data has been read from the socket.
296   * Converts the array buffer that is read in to a string
297   * and sends it on for further processing by passing it to
298   * the previously assigned callback function.
299   *
300   * @private
301   * @see TcpConnection.prototype.addDataReceivedListener
302   * @param {Object} readInfo The incoming message
303   */
304  TcpConnection.prototype._onReceive = function(info) {
305    if (this.socketId != info.socketId)
306      return;
307
308    // Call received callback if there's data in the response.
309    if (this.callbacks.recv) {
310      log('onDataRead');
311      // Convert ArrayBuffer to string.
312      _arrayBufferToString(info.data, this.callbacks.recv.bind(this));
313    }
314  };
315
316  TcpConnection.prototype._onReceiveError = function (info) {
317    if (this.socketId != info.socketId)
318      return;
319    this.close();
320  };
321
322  /**
323   * Callback for when data has been successfully
324   * written to the socket.
325   *
326   * @private
327   * @param {Object} writeInfo The outgoing message
328   */
329  TcpConnection.prototype._onWriteComplete = function(writeInfo) {
330    log('onWriteComplete');
331    // Call sent callback.
332    if (this.callbacks.sent) {
333      this.callbacks.sent(writeInfo);
334    }
335  };
336
337
338
339  /**
340   * Converts an array buffer to a string
341   *
342   * @private
343   * @param {ArrayBuffer} buf The buffer to convert
344   * @param {Function} callback The function to call when conversion is complete
345   */
346  function _arrayBufferToString(buf, callback) {
347    var bb = new Blob([new Uint8Array(buf)]);
348    var f = new FileReader();
349    f.onload = function(e) {
350      callback(e.target.result);
351    };
352    f.readAsText(bb);
353  }
354
355  /**
356   * Converts a string to an array buffer
357   *
358   * @private
359   * @param {String} str The string to convert
360   * @param {Function} callback The function to call when conversion is complete
361   */
362  function _stringToArrayBuffer(str, callback) {
363    var bb = new Blob([str]);
364    var f = new FileReader();
365    f.onload = function(e) {
366        callback(e.target.result);
367    };
368    f.readAsArrayBuffer(bb);
369  }
370
371
372  /**
373   * Wrapper function for logging
374   */
375  function log(msg) {
376    console.log(msg);
377  }
378
379  /**
380   * Wrapper function for error logging
381   */
382  function error(msg) {
383    console.error(msg);
384  }
385
386  exports.TcpServer = TcpServer;
387  exports.TcpConnection = TcpConnection;
388
389})(window);
390