Projekt

Obecné

Profil

Stáhnout (11 KB) Statistiky
| Větev: | Revize:
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 WebSocket = require('./websocket');
10
const { GUID } = require('./constants');
11

    
12
const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
13

    
14
/**
15
 * Class representing a WebSocket server.
16
 *
17
 * @extends EventEmitter
18
 */
19
class WebSocketServer extends EventEmitter {
20
  /**
21
   * Create a `WebSocketServer` instance.
22
   *
23
   * @param {Object} options Configuration options
24
   * @param {Number} options.backlog The maximum length of the queue of pending
25
   *     connections
26
   * @param {Boolean} options.clientTracking Specifies whether or not to track
27
   *     clients
28
   * @param {Function} options.handleProtocols An hook to handle protocols
29
   * @param {String} options.host The hostname where to bind the server
30
   * @param {Number} options.maxPayload The maximum allowed message size
31
   * @param {Boolean} options.noServer Enable no server mode
32
   * @param {String} options.path Accept only connections matching this path
33
   * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable
34
   *     permessage-deflate
35
   * @param {Number} options.port The port where to bind the server
36
   * @param {http.Server} options.server A pre-created HTTP/S server to use
37
   * @param {Function} options.verifyClient An hook to reject connections
38
   * @param {Function} callback A listener for the `listening` event
39
   */
40
  constructor(options, callback) {
41
    super();
42

    
43
    options = Object.assign(
44
      {
45
        maxPayload: 100 * 1024 * 1024,
46
        perMessageDeflate: false,
47
        handleProtocols: null,
48
        clientTracking: true,
49
        verifyClient: null,
50
        noServer: false,
51
        backlog: null, // use default (511 as implemented in net.js)
52
        server: null,
53
        host: null,
54
        path: null,
55
        port: null
56
      },
57
      options
58
    );
59

    
60
    if (options.port == null && !options.server && !options.noServer) {
61
      throw new TypeError(
62
        'One of the "port", "server", or "noServer" options must be specified'
63
      );
64
    }
65

    
66
    if (options.port != null) {
67
      this._server = http.createServer((req, res) => {
68
        const body = http.STATUS_CODES[426];
69

    
70
        res.writeHead(426, {
71
          'Content-Length': body.length,
72
          'Content-Type': 'text/plain'
73
        });
74
        res.end(body);
75
      });
76
      this._server.listen(
77
        options.port,
78
        options.host,
79
        options.backlog,
80
        callback
81
      );
82
    } else if (options.server) {
83
      this._server = options.server;
84
    }
85

    
86
    if (this._server) {
87
      this._removeListeners = addListeners(this._server, {
88
        listening: this.emit.bind(this, 'listening'),
89
        error: this.emit.bind(this, 'error'),
90
        upgrade: (req, socket, head) => {
91
          this.handleUpgrade(req, socket, head, (ws) => {
92
            this.emit('connection', ws, req);
93
          });
94
        }
95
      });
96
    }
97

    
98
    if (options.perMessageDeflate === true) options.perMessageDeflate = {};
99
    if (options.clientTracking) this.clients = new Set();
100
    this.options = options;
101
  }
102

    
103
  /**
104
   * Returns the bound address, the address family name, and port of the server
105
   * as reported by the operating system if listening on an IP socket.
106
   * If the server is listening on a pipe or UNIX domain socket, the name is
107
   * returned as a string.
108
   *
109
   * @return {(Object|String|null)} The address of the server
110
   * @public
111
   */
112
  address() {
113
    if (this.options.noServer) {
114
      throw new Error('The server is operating in "noServer" mode');
115
    }
116

    
117
    if (!this._server) return null;
118
    return this._server.address();
119
  }
120

    
121
  /**
122
   * Close the server.
123
   *
124
   * @param {Function} cb Callback
125
   * @public
126
   */
127
  close(cb) {
128
    if (cb) this.once('close', cb);
129

    
130
    //
131
    // Terminate all associated clients.
132
    //
133
    if (this.clients) {
134
      for (const client of this.clients) client.terminate();
135
    }
136

    
137
    const server = this._server;
138

    
139
    if (server) {
140
      this._removeListeners();
141
      this._removeListeners = this._server = null;
142

    
143
      //
144
      // Close the http server if it was internally created.
145
      //
146
      if (this.options.port != null) {
147
        server.close(() => this.emit('close'));
148
        return;
149
      }
150
    }
151

    
152
    process.nextTick(emitClose, this);
153
  }
154

    
155
  /**
156
   * See if a given request should be handled by this server instance.
157
   *
158
   * @param {http.IncomingMessage} req Request object to inspect
159
   * @return {Boolean} `true` if the request is valid, else `false`
160
   * @public
161
   */
162
  shouldHandle(req) {
163
    if (this.options.path) {
164
      const index = req.url.indexOf('?');
165
      const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
166

    
167
      if (pathname !== this.options.path) return false;
168
    }
169

    
170
    return true;
171
  }
172

    
173
  /**
174
   * Handle a HTTP Upgrade request.
175
   *
176
   * @param {http.IncomingMessage} req The request object
177
   * @param {net.Socket} socket The network socket between the server and client
178
   * @param {Buffer} head The first packet of the upgraded stream
179
   * @param {Function} cb Callback
180
   * @public
181
   */
182
  handleUpgrade(req, socket, head, cb) {
183
    socket.on('error', socketOnError);
184

    
185
    const key =
186
      req.headers['sec-websocket-key'] !== undefined
187
        ? req.headers['sec-websocket-key'].trim()
188
        : false;
189
    const version = +req.headers['sec-websocket-version'];
190
    const extensions = {};
191

    
192
    if (
193
      req.method !== 'GET' ||
194
      req.headers.upgrade.toLowerCase() !== 'websocket' ||
195
      !key ||
196
      !keyRegex.test(key) ||
197
      (version !== 8 && version !== 13) ||
198
      !this.shouldHandle(req)
199
    ) {
200
      return abortHandshake(socket, 400);
201
    }
202

    
203
    if (this.options.perMessageDeflate) {
204
      const perMessageDeflate = new PerMessageDeflate(
205
        this.options.perMessageDeflate,
206
        true,
207
        this.options.maxPayload
208
      );
209

    
210
      try {
211
        const offers = extension.parse(req.headers['sec-websocket-extensions']);
212

    
213
        if (offers[PerMessageDeflate.extensionName]) {
214
          perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
215
          extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
216
        }
217
      } catch (err) {
218
        return abortHandshake(socket, 400);
219
      }
220
    }
221

    
222
    //
223
    // Optionally call external client verification handler.
224
    //
225
    if (this.options.verifyClient) {
226
      const info = {
227
        origin:
228
          req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
229
        secure: !!(req.connection.authorized || req.connection.encrypted),
230
        req
231
      };
232

    
233
      if (this.options.verifyClient.length === 2) {
234
        this.options.verifyClient(info, (verified, code, message, headers) => {
235
          if (!verified) {
236
            return abortHandshake(socket, code || 401, message, headers);
237
          }
238

    
239
          this.completeUpgrade(key, extensions, req, socket, head, cb);
240
        });
241
        return;
242
      }
243

    
244
      if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
245
    }
246

    
247
    this.completeUpgrade(key, extensions, req, socket, head, cb);
248
  }
249

    
250
  /**
251
   * Upgrade the connection to WebSocket.
252
   *
253
   * @param {String} key The value of the `Sec-WebSocket-Key` header
254
   * @param {Object} extensions The accepted extensions
255
   * @param {http.IncomingMessage} req The request object
256
   * @param {net.Socket} socket The network socket between the server and client
257
   * @param {Buffer} head The first packet of the upgraded stream
258
   * @param {Function} cb Callback
259
   * @private
260
   */
261
  completeUpgrade(key, extensions, req, socket, head, cb) {
262
    //
263
    // Destroy the socket if the client has already sent a FIN packet.
264
    //
265
    if (!socket.readable || !socket.writable) return socket.destroy();
266

    
267
    const digest = crypto
268
      .createHash('sha1')
269
      .update(key + GUID)
270
      .digest('base64');
271

    
272
    const headers = [
273
      'HTTP/1.1 101 Switching Protocols',
274
      'Upgrade: websocket',
275
      'Connection: Upgrade',
276
      `Sec-WebSocket-Accept: ${digest}`
277
    ];
278

    
279
    const ws = new WebSocket(null);
280
    var protocol = req.headers['sec-websocket-protocol'];
281

    
282
    if (protocol) {
283
      protocol = protocol.trim().split(/ *, */);
284

    
285
      //
286
      // Optionally call external protocol selection handler.
287
      //
288
      if (this.options.handleProtocols) {
289
        protocol = this.options.handleProtocols(protocol, req);
290
      } else {
291
        protocol = protocol[0];
292
      }
293

    
294
      if (protocol) {
295
        headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
296
        ws.protocol = protocol;
297
      }
298
    }
299

    
300
    if (extensions[PerMessageDeflate.extensionName]) {
301
      const params = extensions[PerMessageDeflate.extensionName].params;
302
      const value = extension.format({
303
        [PerMessageDeflate.extensionName]: [params]
304
      });
305
      headers.push(`Sec-WebSocket-Extensions: ${value}`);
306
      ws._extensions = extensions;
307
    }
308

    
309
    //
310
    // Allow external modification/inspection of handshake headers.
311
    //
312
    this.emit('headers', headers, req);
313

    
314
    socket.write(headers.concat('\r\n').join('\r\n'));
315
    socket.removeListener('error', socketOnError);
316

    
317
    ws.setSocket(socket, head, this.options.maxPayload);
318

    
319
    if (this.clients) {
320
      this.clients.add(ws);
321
      ws.on('close', () => this.clients.delete(ws));
322
    }
323

    
324
    cb(ws);
325
  }
326
}
327

    
328
module.exports = WebSocketServer;
329

    
330
/**
331
 * Add event listeners on an `EventEmitter` using a map of <event, listener>
332
 * pairs.
333
 *
334
 * @param {EventEmitter} server The event emitter
335
 * @param {Object.<String, Function>} map The listeners to add
336
 * @return {Function} A function that will remove the added listeners when called
337
 * @private
338
 */
339
function addListeners(server, map) {
340
  for (const event of Object.keys(map)) server.on(event, map[event]);
341

    
342
  return function removeListeners() {
343
    for (const event of Object.keys(map)) {
344
      server.removeListener(event, map[event]);
345
    }
346
  };
347
}
348

    
349
/**
350
 * Emit a `'close'` event on an `EventEmitter`.
351
 *
352
 * @param {EventEmitter} server The event emitter
353
 * @private
354
 */
355
function emitClose(server) {
356
  server.emit('close');
357
}
358

    
359
/**
360
 * Handle premature socket errors.
361
 *
362
 * @private
363
 */
364
function socketOnError() {
365
  this.destroy();
366
}
367

    
368
/**
369
 * Close the connection when preconditions are not fulfilled.
370
 *
371
 * @param {net.Socket} socket The socket of the upgrade request
372
 * @param {Number} code The HTTP response status code
373
 * @param {String} [message] The HTTP response body
374
 * @param {Object} [headers] Additional HTTP response headers
375
 * @private
376
 */
377
function abortHandshake(socket, code, message, headers) {
378
  if (socket.writable) {
379
    message = message || http.STATUS_CODES[code];
380
    headers = Object.assign(
381
      {
382
        Connection: 'close',
383
        'Content-type': 'text/html',
384
        'Content-Length': Buffer.byteLength(message)
385
      },
386
      headers
387
    );
388

    
389
    socket.write(
390
      `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
391
        Object.keys(headers)
392
          .map((h) => `${h}: ${headers[h]}`)
393
          .join('\r\n') +
394
        '\r\n\r\n' +
395
        message
396
    );
397
  }
398

    
399
  socket.removeListener('error', socketOnError);
400
  socket.destroy();
401
}
(9-9/10)