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