deca408387c627b883e1da6d30893e8b9ba1af1b
[platform/framework/web/wrtjs.git] /
1 'use strict';
2
3 const EventEmitter = require('events');
4 const crypto = require('crypto');
5 const http = require('http');
6
7 const PerMessageDeflate = require('./permessage-deflate');
8 const extension = require('./extension');
9 const constants = require('./constants');
10 const WebSocket = require('./websocket');
11
12 /**
13  * Class representing a WebSocket server.
14  *
15  * @extends EventEmitter
16  */
17 class WebSocketServer extends EventEmitter {
18   /**
19    * Create a `WebSocketServer` instance.
20    *
21    * @param {Object} options Configuration options
22    * @param {String} options.host The hostname where to bind the server
23    * @param {Number} options.port The port where to bind the server
24    * @param {http.Server} options.server A pre-created HTTP/S server to use
25    * @param {Function} options.verifyClient An hook to reject connections
26    * @param {Function} options.handleProtocols An hook to handle protocols
27    * @param {String} options.path Accept only connections matching this path
28    * @param {Boolean} options.noServer Enable no server mode
29    * @param {Boolean} options.clientTracking Specifies whether or not to track clients
30    * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate
31    * @param {Number} options.maxPayload The maximum allowed message size
32    * @param {Function} callback A listener for the `listening` event
33    */
34   constructor(options, callback) {
35     super();
36
37     options = Object.assign(
38       {
39         maxPayload: 100 * 1024 * 1024,
40         perMessageDeflate: false,
41         handleProtocols: null,
42         clientTracking: true,
43         verifyClient: null,
44         noServer: false,
45         backlog: null, // use default (511 as implemented in net.js)
46         server: null,
47         host: null,
48         path: null,
49         port: null
50       },
51       options
52     );
53
54     if (options.port == null && !options.server && !options.noServer) {
55       throw new TypeError(
56         'One of the "port", "server", or "noServer" options must be specified'
57       );
58     }
59
60     if (options.port != null) {
61       this._server = http.createServer((req, res) => {
62         const body = http.STATUS_CODES[426];
63
64         res.writeHead(426, {
65           'Content-Length': body.length,
66           'Content-Type': 'text/plain'
67         });
68         res.end(body);
69       });
70       this._server.listen(
71         options.port,
72         options.host,
73         options.backlog,
74         callback
75       );
76     } else if (options.server) {
77       this._server = options.server;
78     }
79
80     if (this._server) {
81       this._removeListeners = addListeners(this._server, {
82         listening: this.emit.bind(this, 'listening'),
83         error: this.emit.bind(this, 'error'),
84         upgrade: (req, socket, head) => {
85           this.handleUpgrade(req, socket, head, (ws) => {
86             this.emit('connection', ws, req);
87           });
88         }
89       });
90     }
91
92     if (options.perMessageDeflate === true) options.perMessageDeflate = {};
93     if (options.clientTracking) this.clients = new Set();
94     this.options = options;
95   }
96
97   /**
98    * Returns the bound address, the address family name, and port of the server
99    * as reported by the operating system if listening on an IP socket.
100    * If the server is listening on a pipe or UNIX domain socket, the name is
101    * returned as a string.
102    *
103    * @return {(Object|String|null)} The address of the server
104    * @public
105    */
106   address() {
107     if (this.options.noServer) {
108       throw new Error('The server is operating in "noServer" mode');
109     }
110
111     if (!this._server) return null;
112     return this._server.address();
113   }
114
115   /**
116    * Close the server.
117    *
118    * @param {Function} cb Callback
119    * @public
120    */
121   close(cb) {
122     if (cb) this.once('close', cb);
123
124     //
125     // Terminate all associated clients.
126     //
127     if (this.clients) {
128       for (const client of this.clients) client.terminate();
129     }
130
131     const server = this._server;
132
133     if (server) {
134       this._removeListeners();
135       this._removeListeners = this._server = null;
136
137       //
138       // Close the http server if it was internally created.
139       //
140       if (this.options.port != null) {
141         server.close(() => this.emit('close'));
142         return;
143       }
144     }
145
146     process.nextTick(emitClose, this);
147   }
148
149   /**
150    * See if a given request should be handled by this server instance.
151    *
152    * @param {http.IncomingMessage} req Request object to inspect
153    * @return {Boolean} `true` if the request is valid, else `false`
154    * @public
155    */
156   shouldHandle(req) {
157     if (this.options.path) {
158       const index = req.url.indexOf('?');
159       const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
160
161       if (pathname !== this.options.path) return false;
162     }
163
164     return true;
165   }
166
167   /**
168    * Handle a HTTP Upgrade request.
169    *
170    * @param {http.IncomingMessage} req The request object
171    * @param {net.Socket} socket The network socket between the server and client
172    * @param {Buffer} head The first packet of the upgraded stream
173    * @param {Function} cb Callback
174    * @public
175    */
176   handleUpgrade(req, socket, head, cb) {
177     socket.on('error', socketOnError);
178
179     const version = +req.headers['sec-websocket-version'];
180     const extensions = {};
181
182     if (
183       req.method !== 'GET' ||
184       req.headers.upgrade.toLowerCase() !== 'websocket' ||
185       !req.headers['sec-websocket-key'] ||
186       (version !== 8 && version !== 13) ||
187       !this.shouldHandle(req)
188     ) {
189       return abortHandshake(socket, 400);
190     }
191
192     if (this.options.perMessageDeflate) {
193       const perMessageDeflate = new PerMessageDeflate(
194         this.options.perMessageDeflate,
195         true,
196         this.options.maxPayload
197       );
198
199       try {
200         const offers = extension.parse(req.headers['sec-websocket-extensions']);
201
202         if (offers[PerMessageDeflate.extensionName]) {
203           perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
204           extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
205         }
206       } catch (err) {
207         return abortHandshake(socket, 400);
208       }
209     }
210
211     //
212     // Optionally call external client verification handler.
213     //
214     if (this.options.verifyClient) {
215       const info = {
216         origin:
217           req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
218         secure: !!(req.connection.authorized || req.connection.encrypted),
219         req
220       };
221
222       if (this.options.verifyClient.length === 2) {
223         this.options.verifyClient(info, (verified, code, message, headers) => {
224           if (!verified) {
225             return abortHandshake(socket, code || 401, message, headers);
226           }
227
228           this.completeUpgrade(extensions, req, socket, head, cb);
229         });
230         return;
231       }
232
233       if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
234     }
235
236     this.completeUpgrade(extensions, req, socket, head, cb);
237   }
238
239   /**
240    * Upgrade the connection to WebSocket.
241    *
242    * @param {Object} extensions The accepted extensions
243    * @param {http.IncomingMessage} req The request object
244    * @param {net.Socket} socket The network socket between the server and client
245    * @param {Buffer} head The first packet of the upgraded stream
246    * @param {Function} cb Callback
247    * @private
248    */
249   completeUpgrade(extensions, req, socket, head, cb) {
250     //
251     // Destroy the socket if the client has already sent a FIN packet.
252     //
253     if (!socket.readable || !socket.writable) return socket.destroy();
254
255     const key = crypto
256       .createHash('sha1')
257       .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary')
258       .digest('base64');
259
260     const headers = [
261       'HTTP/1.1 101 Switching Protocols',
262       'Upgrade: websocket',
263       'Connection: Upgrade',
264       `Sec-WebSocket-Accept: ${key}`
265     ];
266
267     const ws = new WebSocket(null);
268     var protocol = req.headers['sec-websocket-protocol'];
269
270     if (protocol) {
271       protocol = protocol.trim().split(/ *, */);
272
273       //
274       // Optionally call external protocol selection handler.
275       //
276       if (this.options.handleProtocols) {
277         protocol = this.options.handleProtocols(protocol, req);
278       } else {
279         protocol = protocol[0];
280       }
281
282       if (protocol) {
283         headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
284         ws.protocol = protocol;
285       }
286     }
287
288     if (extensions[PerMessageDeflate.extensionName]) {
289       const params = extensions[PerMessageDeflate.extensionName].params;
290       const value = extension.format({
291         [PerMessageDeflate.extensionName]: [params]
292       });
293       headers.push(`Sec-WebSocket-Extensions: ${value}`);
294       ws._extensions = extensions;
295     }
296
297     //
298     // Allow external modification/inspection of handshake headers.
299     //
300     this.emit('headers', headers, req);
301
302     socket.write(headers.concat('\r\n').join('\r\n'));
303     socket.removeListener('error', socketOnError);
304
305     ws.setSocket(socket, head, this.options.maxPayload);
306
307     if (this.clients) {
308       this.clients.add(ws);
309       ws.on('close', () => this.clients.delete(ws));
310     }
311
312     cb(ws);
313   }
314 }
315
316 module.exports = WebSocketServer;
317
318 /**
319  * Add event listeners on an `EventEmitter` using a map of <event, listener>
320  * pairs.
321  *
322  * @param {EventEmitter} server The event emitter
323  * @param {Object.<String, Function>} map The listeners to add
324  * @return {Function} A function that will remove the added listeners when called
325  * @private
326  */
327 function addListeners(server, map) {
328   for (const event of Object.keys(map)) server.on(event, map[event]);
329
330   return function removeListeners() {
331     for (const event of Object.keys(map)) {
332       server.removeListener(event, map[event]);
333     }
334   };
335 }
336
337 /**
338  * Emit a `'close'` event on an `EventEmitter`.
339  *
340  * @param {EventEmitter} server The event emitter
341  * @private
342  */
343 function emitClose(server) {
344   server.emit('close');
345 }
346
347 /**
348  * Handle premature socket errors.
349  *
350  * @private
351  */
352 function socketOnError() {
353   this.destroy();
354 }
355
356 /**
357  * Close the connection when preconditions are not fulfilled.
358  *
359  * @param {net.Socket} socket The socket of the upgrade request
360  * @param {Number} code The HTTP response status code
361  * @param {String} [message] The HTTP response body
362  * @param {Object} [headers] Additional HTTP response headers
363  * @private
364  */
365 function abortHandshake(socket, code, message, headers) {
366   if (socket.writable) {
367     message = message || http.STATUS_CODES[code];
368     headers = Object.assign(
369       {
370         Connection: 'close',
371         'Content-type': 'text/html',
372         'Content-Length': Buffer.byteLength(message)
373       },
374       headers
375     );
376
377     socket.write(
378       `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
379         Object.keys(headers)
380           .map((h) => `${h}: ${headers[h]}`)
381           .join('\r\n') +
382         '\r\n\r\n' +
383         message
384     );
385   }
386
387   socket.removeListener('error', socketOnError);
388   socket.destroy();
389 }