├── examples ├── fileapi │ ├── .gitignore │ ├── package.json │ ├── public │ │ ├── index.html │ │ ├── app.js │ │ └── uploader.js │ └── server.js ├── serverstats │ ├── package.json │ ├── server.js │ └── public │ │ └── index.html ├── express-session-parse │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── app.js │ └── index.js └── ssl.js ├── .eslintignore ├── .gitignore ├── .npmignore ├── .eslintrc.yaml ├── .travis.yml ├── lib ├── Constants.js ├── Validation.js ├── ErrorCodes.js ├── Extensions.js ├── BufferUtil.js ├── EventTarget.js ├── WebSocketServer.js ├── Sender.js ├── PerMessageDeflate.js ├── Receiver.js └── WebSocket.js ├── index.js ├── appveyor.yml ├── test ├── autobahn-server.js ├── fixtures │ ├── request.pem │ ├── certificate.pem │ ├── key.pem │ ├── agent1-key.pem │ ├── ca1-cert.pem │ ├── agent1-cert.pem │ └── ca1-key.pem ├── autobahn.js ├── WebSocket.integration.js ├── hybi-util.js ├── Extensions.test.js ├── Validation.test.js ├── Sender.test.js ├── PerMessageDeflate.test.js └── Receiver.test.js ├── LICENSE ├── package.json ├── SECURITY.md ├── bench ├── sender.benchmark.js ├── parser.benchmark.js └── speed.js ├── README.md └── doc └── ws.md /examples/fileapi/.gitignore: -------------------------------------------------------------------------------- 1 | uploaded 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output/ 3 | coverage/ 4 | .vscode/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | examples/ 3 | bench/ 4 | test/ 5 | doc/ 6 | appveyor.yml 7 | .* 8 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: standard 2 | env: 3 | mocha: true 4 | rules: 5 | no-extra-semi: error 6 | semi: 7 | - error 8 | - always 9 | -------------------------------------------------------------------------------- /examples/serverstats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "serverstats", 4 | "version": "0.0.0", 5 | "repository": "websockets/ws", 6 | "dependencies": { 7 | "express": "~4.14.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | sudo: false 4 | node_js: 5 | - "7" 6 | - "6" 7 | - "4" 8 | - "4.1.0" 9 | after_success: 10 | - "npm install coveralls@2 && nyc report --reporter=text-lcov | coveralls" 11 | -------------------------------------------------------------------------------- /examples/fileapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "fileapi", 4 | "version": "0.0.0", 5 | "repository": "websockets/ws", 6 | "dependencies": { 7 | "express": "~4.14.0", 8 | "ansi": "https://github.com/einaros/ansi.js/tarball/master" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/express-session-parse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "express-session-parse", 4 | "version": "0.0.0", 5 | "repository": "websockets/ws", 6 | "dependencies": { 7 | "express": "~4.14.1", 8 | "express-session": "~1.15.1", 9 | "uuid": "~3.0.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/Constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeBuffer = require('safe-buffer'); 4 | 5 | const Buffer = safeBuffer.Buffer; 6 | 7 | exports.BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments']; 8 | exports.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 9 | exports.EMPTY_BUFFER = Buffer.alloc(0); 10 | exports.NOOP = () => {}; 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const WebSocket = require('./lib/WebSocket'); 10 | 11 | WebSocket.Server = require('./lib/WebSocketServer'); 12 | WebSocket.Receiver = require('./lib/Receiver'); 13 | WebSocket.Sender = require('./lib/Sender'); 14 | 15 | module.exports = WebSocket; 16 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "7" 4 | - nodejs_version: "6" 5 | - nodejs_version: "4" 6 | - nodejs_version: "4.1.0" 7 | platform: 8 | - x86 9 | - x64 10 | matrix: 11 | fast_finish: true 12 | install: 13 | - ps: Install-Product node $env:nodejs_version $env:platform 14 | - npm install 15 | test_script: 16 | - node --version 17 | - npm --version 18 | - npm test 19 | build: off 20 | -------------------------------------------------------------------------------- /test/autobahn-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebSocket = require('../'); 4 | 5 | const port = process.argv.length > 2 ? parseInt(process.argv[2]) : 9001; 6 | const wss = new WebSocket.Server({ port }, () => { 7 | console.log(`Listening to port ${port}. Use extra argument to define the port`); 8 | }); 9 | 10 | wss.on('connection', (ws) => { 11 | ws.on('message', (data) => ws.send(data)); 12 | ws.on('error', (e) => console.error(e)); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/Validation.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | try { 10 | const isValidUTF8 = require('utf-8-validate'); 11 | 12 | module.exports = typeof isValidUTF8 === 'object' 13 | ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 14 | : isValidUTF8; 15 | } catch (e) /* istanbul ignore next */ { 16 | module.exports = () => true; 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/request.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBhDCB7gIBADBFMQswCQYDVQQGEwJubzETMBEGA1UECAwKU29tZS1TdGF0ZTEh 3 | MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB 4 | AQUAA4GNADCBiQKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEC 5 | hrR/3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0W 6 | ZxXgf72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwID 7 | AQABoAAwDQYJKoZIhvcNAQEFBQADgYEAjsUXEARgfxZNkMjuUcudgU2w4JXS0gGI 8 | JQ0U1LmU0vMDSKwqndMlvCbKzEgPbJnGJDI8D4MeINCJHa5Ceyb8c+jaJYUcCabl 9 | lQW5Psn3+eWp8ncKlIycDRj1Qk615XuXtV0fhkrgQM2ZCm9LaQ1O1Gd/CzLihLjF 10 | W0MmgMKMMRk= 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /examples/express-session-parse/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Express session demo 6 | 7 | 8 |

Choose an action.

9 | 12 | 15 | 18 |

19 |     
20 |   
21 | 
22 | 


--------------------------------------------------------------------------------
/examples/fileapi/public/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
12 |     
13 |     
14 |   
15 |   
16 |     

This example will upload an entire directory tree to the node.js server via a fast and persistent WebSocket connection.

17 |

Note that the example is Chrome only for now.

18 |

19 | Upload status: 20 |
Please select a directory to upload.
21 | 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICATCCAWoCCQDPufXH86n2QzANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJu 3 | bzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 4 | cyBQdHkgTHRkMB4XDTEyMDEwMTE0NDQwMFoXDTIwMDMxOTE0NDQwMFowRTELMAkG 5 | A1UEBhMCbm8xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 6 | IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtrQ7 7 | +r//2iV/B6F+4boH0XqFn7alcV9lpjvAmwRXNKnxAoa0f97AjYPGNLKrjpkNXXhB 8 | JROIdbRbZnCNeC5fzX1a+JCo7KStzBXuGSZr27TtFmcV4H+9gIRIcNHtZmJLnxbJ 9 | sIhkGR8yVYdmJZe4eT5ldk1zoB1adgPF1hZhCBMCAwEAATANBgkqhkiG9w0BAQUF 10 | AAOBgQCeWBEHYJ4mCB5McwSSUox0T+/mJ4W48L/ZUE4LtRhHasU9hiW92xZkTa7E 11 | QLcoJKQiWfiLX2ysAro0NX4+V8iqLziMqvswnPzz5nezaOLE/9U/QvH3l8qqNkXu 12 | rNbsW1h/IO6FV8avWFYVFoutUwOaZ809k7iMh2F2JMgXQ5EymQ== 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /examples/serverstats/server.js: -------------------------------------------------------------------------------- 1 | var WebSocketServer = require('../../').Server; 2 | var express = require('express'); 3 | var path = require('path'); 4 | var app = express(); 5 | var server = require('http').createServer(); 6 | 7 | app.use(express.static(path.join(__dirname, '/public'))); 8 | 9 | var wss = new WebSocketServer({server: server}); 10 | wss.on('connection', function (ws) { 11 | var id = setInterval(function () { 12 | ws.send(JSON.stringify(process.memoryUsage()), function () { /* ignore errors */ }); 13 | }, 100); 14 | console.log('started client interval'); 15 | ws.on('close', function () { 16 | console.log('stopping client interval'); 17 | clearInterval(id); 18 | }); 19 | }); 20 | 21 | server.on('request', app); 22 | server.listen(8080, function () { 23 | console.log('Listening on http://localhost:8080'); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/ErrorCodes.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | module.exports = { 10 | isValidErrorCode: function (code) { 11 | return (code >= 1000 && code <= 1013 && code !== 1004 && code !== 1005 && code !== 1006) || 12 | (code >= 3000 && code <= 4999); 13 | }, 14 | 1000: 'normal', 15 | 1001: 'going away', 16 | 1002: 'protocol error', 17 | 1003: 'unsupported data', 18 | 1004: 'reserved', 19 | 1005: 'reserved for extensions', 20 | 1006: 'reserved for extensions', 21 | 1007: 'inconsistent or invalid data', 22 | 1008: 'policy violation', 23 | 1009: 'message too big', 24 | 1010: 'extension handshake missing', 25 | 1011: 'an unexpected condition prevented the request from being fulfilled', 26 | 1012: 'service restart', 27 | 1013: 'try again later' 28 | }; 29 | -------------------------------------------------------------------------------- /test/autobahn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebSocket = require('../'); 4 | 5 | let currentTest = 1; 6 | let testCount; 7 | 8 | function nextTest () { 9 | let ws; 10 | 11 | if (currentTest > testCount) { 12 | ws = new WebSocket('ws://localhost:9001/updateReports?agent=ws'); 13 | return; 14 | } 15 | 16 | console.log(`Running test case ${currentTest}/${testCount}`); 17 | 18 | ws = new WebSocket(`ws://localhost:9001/runCase?case=${currentTest}&agent=ws`); 19 | ws.on('message', (data) => ws.send(data)); 20 | ws.on('close', () => { 21 | currentTest++; 22 | process.nextTick(nextTest); 23 | }); 24 | ws.on('error', (e) => console.error(e)); 25 | } 26 | 27 | const ws = new WebSocket('ws://localhost:9001/getCaseCount'); 28 | ws.on('message', (data) => { 29 | testCount = parseInt(data); 30 | }); 31 | ws.on('close', () => { 32 | if (testCount > 0) { 33 | nextTest(); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /test/fixtures/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC2tDv6v//aJX8HoX7hugfReoWftqVxX2WmO8CbBFc0qfEChrR/ 3 | 3sCNg8Y0squOmQ1deEElE4h1tFtmcI14Ll/NfVr4kKjspK3MFe4ZJmvbtO0WZxXg 4 | f72AhEhw0e1mYkufFsmwiGQZHzJVh2Yll7h5PmV2TXOgHVp2A8XWFmEIEwIDAQAB 5 | AoGAAlVY8sHi/aE+9xT77twWX3mGHV0SzdjfDnly40fx6S1Gc7bOtVdd9DC7pk6l 6 | 3ENeJVR02IlgU8iC5lMHq4JEHPE272jtPrLlrpWLTGmHEqoVFv9AITPqUDLhB9Kk 7 | Hjl7h8NYBKbr2JHKICr3DIPKOT+RnXVb1PD4EORbJ3ooYmkCQQDfknUnVxPgxUGs 8 | ouABw1WJIOVgcCY/IFt4Ihf6VWTsxBgzTJKxn3HtgvE0oqTH7V480XoH0QxHhjLq 9 | DrgobWU9AkEA0TRJ8/ouXGnFEPAXjWr9GdPQRZ1Use2MrFjneH2+Sxc0CmYtwwqL 10 | Kr5kS6mqJrxprJeluSjBd+3/ElxURrEXjwJAUvmlN1OPEhXDmRHd92mKnlkyKEeX 11 | OkiFCiIFKih1S5Y/sRJTQ0781nyJjtJqO7UyC3pnQu1oFEePL+UEniRztQJAMfav 12 | AtnpYKDSM+1jcp7uu9BemYGtzKDTTAYfoiNF42EzSJiGrWJDQn4eLgPjY0T0aAf/ 13 | yGz3Z9ErbhMm/Ysl+QJBAL4kBxRT8gM4ByJw4sdOvSeCCANFq8fhbgm8pGWlCPb5 14 | JGmX3/GHFM8x2tbWMGpyZP1DLtiNEFz7eCGktWK5rqE= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/fixtures/agent1-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC+hsCqLGG9GHN0KNDrUFZQieNtPCHcNdYo/rdlubH2Gwopebck 3 | KI54RjNQiYRF8p0KV18IyibiHk0ApHlIm08hSmtG7PPpYthYhqCmG2+BvNaL56pJ 4 | hNog1W/BsN66zOM3mdGXRZ4zLAGWnPIevguLAf6UvQYjDqOqk5deUUJXCQIDAQAB 5 | AoGANu/CBA+SCyVOvRK70u4yRTzNMAUjukxnuSBhH1rg/pajYnwvG6T6F6IeT72n 6 | P0gKkh3JUE6B0bds+p9yPUZTFUXghxjcF33wlIY44H6gFE4K5WutsFJ9c450wtuu 7 | 8rXZTsIg7lAXWjTFVmdtOEPetcGlO2Hpi1O7ZzkzHgB2w9ECQQDksCCYx78or1zY 8 | ZSokm8jmpIjG3VLKdvI9HAoJRN40ldnwFoigrFa1AHwsFtWNe8bKyVRPDoLDUjpB 9 | dkPWgweVAkEA1UfgqguQ2KIkbtp9nDBionu3QaajksrRHwIa8vdfRfLxszfHk2fh 10 | NGY3dkRZF8HUAbzYLrd9poVhCBAEjWekpQJASOM6AHfpnXYHCZF01SYx6hEW5wsz 11 | kARJQODm8f1ZNTlttO/5q/xBxn7ZFNRSTD3fJlL05B2j380ddC/Vf1FT4QJAP1BC 12 | GliqnBSuGhZUWYxni3KMeTm9rzL0F29pjpzutHYlWB2D6ndY/FQnvL0XcZ0Bka58 13 | womIDGnl3x3aLBwLXQJBAJv6h5CHbXHx7VyDJAcNfppAqZGcEaiVg8yf2F33iWy2 14 | FLthhJucx7df7SO2aw5h06bRDRAhb9br0R9/3mLr7RE= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/fixtures/ca1-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICazCCAdQCCQC9/g69HtxXRzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO 4 | BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA 5 | dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB6 6 | MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK 7 | EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqG 8 | SIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0A 9 | MIGJAoGBAKxr1mARUcv7zaqx5y4AxJPK6c1jdbSg7StcL4vg8klaPAlfNO6o+/Cl 10 | w5CdQD3ukaVUwUOJ4T/+b3Xf7785XcWBC33GdjVQkfbHATJYcka7j7JDw3qev5Jk 11 | 1rAbRw48hF6rYlSGcx1mccAjoLoa3I8jgxCNAYHIjUQXgdmU893rAgMBAAEwDQYJ 12 | KoZIhvcNAQEFBQADgYEAis05yxjCtJRuv8uX/DK6TX/j9C9Lzp1rKDNFTaTZ0iRw 13 | KCw1EcNx4OXSj9gNblW4PWxpDvygrt1AmH9h2cb8K859NSHa9JOBFw6MA5C2A4Sj 14 | NQfNATqUl4T6cdORlcDEZwHtT8b6D4A6Er31G/eJF4Sen0TUFpjdjd+l9RBjHlo= 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /test/fixtures/agent1-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICbjCCAdcCCQCVvok5oeLpqzANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV 3 | UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO 4 | BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA 5 | dGlueWNsb3Vkcy5vcmcwHhcNMTMwMzA4MDAzMDIyWhcNNDAwNzIzMDAzMDIyWjB9 6 | MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK 7 | EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDzANBgNVBAMTBmFnZW50MTEgMB4G 8 | CSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQAD 9 | gY0AMIGJAoGBAL6GwKosYb0Yc3Qo0OtQVlCJ4208Idw11ij+t2W5sfYbCil5tyQo 10 | jnhGM1CJhEXynQpXXwjKJuIeTQCkeUibTyFKa0bs8+li2FiGoKYbb4G81ovnqkmE 11 | 2iDVb8Gw3rrM4zeZ0ZdFnjMsAZac8h6+C4sB/pS9BiMOo6qTl15RQlcJAgMBAAEw 12 | DQYJKoZIhvcNAQEFBQADgYEAOtmLo8DwTPnI4wfQbQ3hWlTS/9itww6IsxH2ODt9 13 | ggB7wi7N3uAdIWRZ54ke0NEAO5CW1xNTwsWcxQbiHrDOqX1vfVCjIenI76jVEEap 14 | /Ay53ydHNBKdsKkib61Me14Mu0bA3lUul57VXwmH4NUEFB3w973Q60PschUhOEXj 15 | 7DY= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /examples/serverstats/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 26 | 27 | 28 | Server Stats
29 | RSS:

30 | Heap total:

31 | Heap used:

32 | 33 | 34 | -------------------------------------------------------------------------------- /test/fixtures/ca1-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIFeWxJE1BrRECAggA 3 | MBQGCCqGSIb3DQMHBAgu9PlMSQ+BOASCAoDEZN2tX0xWo/N+Jg+PrvCrFDk3P+3x 4 | 5xG/PEDjtMCAWPBEwbnaYHDzYmhNcAmxzGqEHGMDiWYs46LbO560VS3uMvFbEWPo 5 | KYYVb13vkxl2poXdonCb5cHZA5GUYzTIVVJFptl4LHwBczHoMHtA4FqAhKlYvlWw 6 | EOrdLB8XcwMmGPFabbbGxno0+EWWM27uNjlogfoxj35mQqSW4rOlhZ460XjOB1Zx 7 | LjXMuZeONojkGYQRG5EUMchBoctQpCOM6cAi9r1B9BvtFCBpDV1c1zEZBzTEUd8o 8 | kLn6tjLmY+QpTdylFjEWc7U3ppLY/pkoTBv4r85a2sEMWqkhSJboLaTboWzDJcU3 9 | Ke61pMpovt/3yCUd3TKgwduVwwQtDVTlBe0p66aN9QVj3CrFy/bKAGO3vxlli24H 10 | aIjZf+OVoBY21ESlW3jLvNlBf7Ezf///2E7j4SCDLyZSFMTpFoAG/jDRyvi+wTKX 11 | Kh485Bptnip6DCSuoH4u2SkOqwz3gJS/6s02YKe4m311QT4Pzne5/FwOFaS/HhQg 12 | Xvyh2/d00OgJ0Y0PYQsHILPRgTUCKUXvj1O58opn3fxSacsPxIXwj6Z4FYAjUTaV 13 | 2B85k1lpant/JJEilDqMjqzx4pHZ/Z3Uto1lSM1JZs9SNL/0UR+6F0TXZTULVU9V 14 | w8jYzz4sPr7LEyrrTbzmjQgnQFVbhAN/eKgRZK/SpLjxpmBV5MfpbPKsPUZqT4UC 15 | 4nXa8a/NYUQ9e+QKK8enq9E599c2W442W7Z1uFRZTWReMx/lF8wwA6G8zOPG0bdj 16 | d+T5Gegzd5mvRiXMBklCo8RLxOOvgxun1n3PY4a63aH6mqBhdfhiLp5j 17 | -----END ENCRYPTED PRIVATE KEY----- 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Einar Otto Stangvik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Einar Otto Stangvik (http://2x.io)", 3 | "name": "ws", 4 | "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", 5 | "version": "2.2.3", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "keywords": [ 9 | "HyBi", 10 | "Push", 11 | "RFC-6455", 12 | "WebSocket", 13 | "WebSockets", 14 | "real-time" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/websockets/ws.git" 19 | }, 20 | "scripts": { 21 | "test": "eslint . && nyc --reporter=html --reporter=text mocha test/*.test.js", 22 | "integration": "eslint . && mocha test/*.integration.js", 23 | "lint": "eslint ." 24 | }, 25 | "dependencies": { 26 | "safe-buffer": "~5.0.1", 27 | "ultron": "~1.1.0" 28 | }, 29 | "devDependencies": { 30 | "benchmark": "~2.1.2", 31 | "bufferutil": "~3.0.0", 32 | "eslint": "~3.19.0", 33 | "eslint-config-standard": "~10.2.0", 34 | "eslint-plugin-import": "~2.2.0", 35 | "eslint-plugin-node": "~4.2.0", 36 | "eslint-plugin-promise": "~3.5.0", 37 | "eslint-plugin-standard": "~3.0.0", 38 | "mocha": "~3.2.0", 39 | "nyc": "~10.2.0", 40 | "utf-8-validate": "~3.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/ssl.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | 'use strict'; 4 | 5 | var fs = require('fs'); 6 | 7 | // you'll probably load configuration from config 8 | var cfg = { 9 | ssl: true, 10 | port: 8080, 11 | ssl_key: '/path/to/you/ssl.key', 12 | ssl_cert: '/path/to/you/ssl.crt' 13 | }; 14 | 15 | var httpServ = (cfg.ssl) ? require('https') : require('http'); 16 | 17 | var WebSocketServer = require('../').Server; 18 | 19 | var app = null; 20 | 21 | // dummy request processing 22 | var processRequest = function (req, res) { 23 | res.writeHead(200); 24 | res.end('All glory to WebSockets!\n'); 25 | }; 26 | 27 | if (cfg.ssl) { 28 | app = httpServ.createServer({ 29 | 30 | // providing server with SSL key/cert 31 | key: fs.readFileSync(cfg.ssl_key), 32 | cert: fs.readFileSync(cfg.ssl_cert) 33 | 34 | }, processRequest).listen(cfg.port); 35 | } else { 36 | app = httpServ.createServer(processRequest).listen(cfg.port); 37 | } 38 | 39 | // passing or reference to web server so WS would knew port and SSL capabilities 40 | var wss = new WebSocketServer({ server: app }); 41 | 42 | wss.on('connection', function (wsConnect) { 43 | wsConnect.on('message', function (message) { 44 | console.log(message); 45 | }); 46 | }); 47 | }()); 48 | -------------------------------------------------------------------------------- /examples/fileapi/public/app.js: -------------------------------------------------------------------------------- 1 | /* global Uploader */ 2 | function onFilesSelected (e) { 3 | var button = e.srcElement; 4 | button.disabled = true; 5 | var progress = document.querySelector('div#progress'); 6 | progress.innerHTML = '0%'; 7 | var files = e.target.files; 8 | var totalFiles = files.length; 9 | var filesSent = 0; 10 | if (totalFiles) { 11 | var uploader = new Uploader('ws://localhost:8080', function () { 12 | Array.prototype.slice.call(files, 0).forEach(function (file) { 13 | if (file.name === '.') { 14 | --totalFiles; 15 | return; 16 | } 17 | uploader.sendFile(file, function (error) { 18 | if (error) { 19 | console.log(error); 20 | return; 21 | } 22 | ++filesSent; 23 | progress.innerHTML = ~~(filesSent / totalFiles * 100) + '%'; 24 | console.log('Sent: ' + file.name); 25 | }); 26 | }); 27 | }); 28 | } 29 | uploader.ondone = function () { 30 | uploader.close(); 31 | progress.innerHTML = '100% done, ' + totalFiles + ' files sent.'; 32 | }; 33 | } 34 | 35 | window.onload = function () { 36 | var importButtons = document.querySelectorAll('[type="file"]'); 37 | Array.prototype.slice.call(importButtons, 0).forEach(function (importButton) { 38 | importButton.addEventListener('change', onFilesSelected, false); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /test/WebSocket.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const WebSocket = require('..'); 6 | 7 | describe('WebSocket', function () { 8 | it('communicates successfully with echo service (ws)', function (done) { 9 | const ws = new WebSocket('ws://echo.websocket.org/', { 10 | origin: 'http://www.websocket.org', 11 | protocolVersion: 13 12 | }); 13 | const str = Date.now().toString(); 14 | 15 | let dataReceived = false; 16 | 17 | ws.on('open', () => ws.send(str)); 18 | ws.on('close', () => { 19 | assert.ok(dataReceived); 20 | done(); 21 | }); 22 | ws.on('message', (data) => { 23 | dataReceived = true; 24 | assert.strictEqual(data, str); 25 | ws.close(); 26 | }); 27 | }); 28 | 29 | it('communicates successfully with echo service (wss)', function (done) { 30 | const ws = new WebSocket('wss://echo.websocket.org/', { 31 | origin: 'https://www.websocket.org', 32 | protocolVersion: 13 33 | }); 34 | const str = Date.now().toString(); 35 | 36 | let dataReceived = false; 37 | 38 | ws.on('open', () => ws.send(str)); 39 | ws.on('close', () => { 40 | assert.ok(dataReceived); 41 | done(); 42 | }); 43 | ws.on('message', (data) => { 44 | dataReceived = true; 45 | assert.strictEqual(data, str); 46 | ws.close(); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /examples/express-session-parse/public/app.js: -------------------------------------------------------------------------------- 1 | /* global fetch, WebSocket, location */ 2 | (() => { 3 | const messages = document.querySelector('#messages'); 4 | const wsButton = document.querySelector('#wsButton'); 5 | const logout = document.querySelector('#logout'); 6 | const login = document.querySelector('#login'); 7 | 8 | const showMessage = (message) => { 9 | messages.textContent += `\n${message}`; 10 | messages.scrollTop = messages.scrollHeight; 11 | }; 12 | 13 | const handleResponse = (response) => { 14 | return response.ok 15 | ? response.json().then((data) => JSON.stringify(data, null, 2)) 16 | : Promise.reject(new Error('Unexpected response')); 17 | }; 18 | 19 | login.onclick = () => { 20 | fetch('/login', { method: 'POST', credentials: 'same-origin' }) 21 | .then(handleResponse) 22 | .then(showMessage) 23 | .catch((err) => showMessage(err.message)); 24 | }; 25 | 26 | logout.onclick = () => { 27 | fetch('/logout', { method: 'DELETE', credentials: 'same-origin' }) 28 | .then(handleResponse) 29 | .then(showMessage) 30 | .catch((err) => showMessage(err.message)); 31 | }; 32 | 33 | let ws; 34 | 35 | wsButton.onclick = () => { 36 | if (ws) { 37 | ws.onerror = ws.onopen = ws.onclose = null; 38 | ws.close(); 39 | } 40 | 41 | ws = new WebSocket(`ws://${location.host}`); 42 | ws.onerror = () => showMessage('WebSocket error'); 43 | ws.onopen = () => showMessage('WebSocket connection established'); 44 | ws.onclose = () => showMessage('WebSocket connection closed'); 45 | }; 46 | })(); 47 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Guidelines 2 | 3 | Please contact us directly at **security@3rd-Eden.com** for any bug that might 4 | impact the security of this project. Please prefix the subject of your email 5 | with `[security]` in lowercase and square brackets. Our email filters will 6 | automatically prevent these messages from being moved to our spam box. 7 | 8 | You will receive an acknowledgement of your report within **24 hours**. 9 | 10 | All emails that do not include security vulnerabilities will be removed and 11 | blocked instantly. 12 | 13 | ## Exceptions 14 | 15 | If you do not receive an acknowledgement within the said time frame please give 16 | us the benefit of the doubt as it's possible that we haven't seen it yet. In 17 | this case please send us a message **without details** using one of the 18 | following methods: 19 | 20 | - Contact the lead developers of this project on their personal e-mails. You 21 | can find the e-mails in the git logs, for example using the following command: 22 | `git --no-pager show -s --format='%an <%ae>' ` where `` is the 23 | SHA1 of their latest commit in the project. 24 | - Create a GitHub issue stating contact details and the severity of the issue. 25 | 26 | Once we have acknowledged receipt of your report and confirmed the bug 27 | ourselves we will work with you to fix the vulnerability and publicly acknowledge 28 | your responsible disclosure, if you wish. In addition to that we will report 29 | all vulnerabilities to the [Node Security Project](https://nodesecurity.io/). 30 | 31 | ## History 32 | 33 | 04 Jan 2016: [Buffer vulnerablity](https://github.com/websockets/ws/releases/tag/1.0.1) 34 | -------------------------------------------------------------------------------- /test/hybi-util.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const safeBuffer = require('safe-buffer'); 10 | 11 | const Buffer = safeBuffer.Buffer; 12 | 13 | /** 14 | * Performs hybi07+ type masking. 15 | */ 16 | function mask (buf, maskString) { 17 | const _mask = Buffer.from(maskString || '3483a868', 'hex'); 18 | 19 | buf = Buffer.from(buf); 20 | 21 | for (let i = 0; i < buf.length; ++i) { 22 | buf[i] ^= _mask[i % 4]; 23 | } 24 | 25 | return buf; 26 | } 27 | 28 | /** 29 | * Left pads the string `s` to a total length of `n` with char `c`. 30 | */ 31 | function padl (s, n, c) { 32 | return c.repeat(n - s.length) + s; 33 | } 34 | 35 | /** 36 | * Returns a hex string, representing a specific byte count `length`, from a number. 37 | */ 38 | function pack (length, number) { 39 | return padl(number.toString(16), length, '0'); 40 | } 41 | 42 | /** 43 | * Returns a hex string representing the length of a message. 44 | */ 45 | function getHybiLengthAsHexString (len, masked) { 46 | let s; 47 | 48 | masked = masked ? 0x80 : 0; 49 | 50 | if (len < 126) { 51 | s = pack(2, masked | len); 52 | } else if (len < 65536) { 53 | s = pack(2, masked | 126) + pack(4, len); 54 | } else { 55 | s = pack(2, masked | 127) + pack(16, len); 56 | } 57 | 58 | return s; 59 | } 60 | 61 | /** 62 | * Split a buffer in two. 63 | */ 64 | function splitBuffer (buf) { 65 | const i = Math.floor(buf.length / 2); 66 | return [buf.slice(0, i), buf.slice(i)]; 67 | } 68 | 69 | module.exports = { 70 | getHybiLengthAsHexString, 71 | splitBuffer, 72 | mask, 73 | pack 74 | }; 75 | -------------------------------------------------------------------------------- /bench/sender.benchmark.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const benchmark = require('benchmark'); 10 | const crypto = require('crypto'); 11 | 12 | const Sender = require('../').Sender; 13 | 14 | const data1 = crypto.randomBytes(64); 15 | const data2 = crypto.randomBytes(16 * 1024); 16 | const data3 = crypto.randomBytes(64 * 1024); 17 | const data4 = crypto.randomBytes(200 * 1024); 18 | const data5 = crypto.randomBytes(1024 * 1024); 19 | 20 | const opts1 = { 21 | readOnly: false, 22 | mask: false, 23 | rsv1: false, 24 | opcode: 2, 25 | fin: true 26 | }; 27 | const opts2 = { 28 | readOnly: true, 29 | rsv1: false, 30 | mask: true, 31 | opcode: 2, 32 | fin: true 33 | }; 34 | 35 | const suite = new benchmark.Suite(); 36 | 37 | suite.add('frame, unmasked (64 B)', () => Sender.frame(data1, opts1)); 38 | suite.add('frame, masked (64 B)', () => Sender.frame(data1, opts2)); 39 | suite.add('frame, unmasked (16 KiB)', () => Sender.frame(data2, opts1)); 40 | suite.add('frame, masked (16 KiB)', () => Sender.frame(data2, opts2)); 41 | suite.add('frame, unmasked (64 KiB)', () => Sender.frame(data3, opts1)); 42 | suite.add('frame, masked (64 KiB)', () => Sender.frame(data3, opts2)); 43 | suite.add('frame, unmasked (200 KiB)', () => Sender.frame(data4, opts1)); 44 | suite.add('frame, masked (200 KiB)', () => Sender.frame(data4, opts2)); 45 | suite.add('frame, unmasked (1 MiB)', () => Sender.frame(data5, opts1)); 46 | suite.add('frame, masked (1 MiB)', () => Sender.frame(data5, opts2)); 47 | 48 | suite.on('cycle', (e) => console.log(e.target.toString())); 49 | 50 | if (require.main === module) { 51 | suite.run({ async: true }); 52 | } else { 53 | module.exports = suite; 54 | } 55 | -------------------------------------------------------------------------------- /test/Extensions.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const Extensions = require('../lib/Extensions'); 6 | 7 | describe('Extensions', function () { 8 | describe('parse', function () { 9 | it('should parse', function () { 10 | const extensions = Extensions.parse('foo'); 11 | 12 | assert.deepStrictEqual(extensions, { foo: [{}] }); 13 | }); 14 | 15 | it('should parse params', function () { 16 | const extensions = Extensions.parse('foo; bar; baz=1; bar=2'); 17 | 18 | assert.deepStrictEqual(extensions, { 19 | foo: [{ bar: [true, '2'], baz: ['1'] }] 20 | }); 21 | }); 22 | 23 | it('should parse multiple extensions', function () { 24 | const extensions = Extensions.parse('foo, bar; baz, foo; baz'); 25 | 26 | assert.deepStrictEqual(extensions, { 27 | foo: [{}, { baz: [true] }], 28 | bar: [{ baz: [true] }] 29 | }); 30 | }); 31 | 32 | it('should parse quoted params', function () { 33 | const extensions = Extensions.parse('foo; bar="hi"'); 34 | 35 | assert.deepStrictEqual(extensions, { 36 | foo: [{ bar: ['hi'] }] 37 | }); 38 | }); 39 | }); 40 | 41 | describe('format', function () { 42 | it('should format', function () { 43 | const extensions = Extensions.format({ foo: {} }); 44 | 45 | assert.strictEqual(extensions, 'foo'); 46 | }); 47 | 48 | it('should format params', function () { 49 | const extensions = Extensions.format({ foo: { bar: [true, 2], baz: 1 } }); 50 | 51 | assert.strictEqual(extensions, 'foo; bar; bar=2; baz=1'); 52 | }); 53 | 54 | it('should format multiple extensions', function () { 55 | const extensions = Extensions.format({ 56 | foo: [{}, { baz: true }], 57 | bar: { baz: true } 58 | }); 59 | 60 | assert.strictEqual(extensions, 'foo, foo; baz, bar; baz'); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/fileapi/public/uploader.js: -------------------------------------------------------------------------------- 1 | /* global WebSocket */ 2 | function Uploader (url, cb) { 3 | this.ws = new WebSocket(url); 4 | if (cb) this.ws.onopen = cb; 5 | this.sendQueue = []; 6 | this.sending = null; 7 | this.sendCallback = null; 8 | this.ondone = null; 9 | var self = this; 10 | this.ws.onmessage = function (event) { 11 | var data = JSON.parse(event.data); 12 | var callback; 13 | if (data.event === 'complete') { 14 | if (data.path !== self.sending.path) { 15 | self.sendQueue = []; 16 | self.sending = null; 17 | self.sendCallback = null; 18 | throw new Error('Got message for wrong file!'); 19 | } 20 | self.sending = null; 21 | callback = self.sendCallback; 22 | self.sendCallback = null; 23 | if (callback) callback(); 24 | if (self.sendQueue.length === 0 && self.ondone) self.ondone(null); 25 | if (self.sendQueue.length > 0) { 26 | var args = self.sendQueue.pop(); 27 | setTimeout(function () { self.sendFile.apply(self, args); }, 0); 28 | } 29 | } else if (data.event === 'error') { 30 | self.sendQueue = []; 31 | self.sending = null; 32 | callback = self.sendCallback; 33 | self.sendCallback = null; 34 | var error = new Error('Server reported send error for file ' + data.path); 35 | if (callback) callback(error); 36 | if (self.ondone) self.ondone(error); 37 | } 38 | }; 39 | } 40 | 41 | Uploader.prototype.sendFile = function (file, cb) { 42 | if (this.ws.readyState !== WebSocket.OPEN) throw new Error('Not connected'); 43 | if (this.sending) { 44 | this.sendQueue.push(arguments); 45 | return; 46 | } 47 | var fileData = { name: file.name, path: file.webkitRelativePath }; 48 | this.sending = fileData; 49 | this.sendCallback = cb; 50 | this.ws.send(JSON.stringify(fileData)); 51 | this.ws.send(file); 52 | }; 53 | 54 | Uploader.prototype.close = function () { 55 | this.ws.close(); 56 | }; 57 | -------------------------------------------------------------------------------- /lib/Extensions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Parse the `Sec-WebSocket-Extensions` header into an object. 5 | * 6 | * @param {String} value field value of the header 7 | * @return {Object} The parsed object 8 | * @public 9 | */ 10 | const parse = (value) => { 11 | value = value || ''; 12 | 13 | const extensions = {}; 14 | 15 | value.split(',').forEach((v) => { 16 | const params = v.split(';'); 17 | const token = params.shift().trim(); 18 | const paramsList = extensions[token] = extensions[token] || []; 19 | const parsedParams = {}; 20 | 21 | params.forEach((param) => { 22 | const parts = param.trim().split('='); 23 | const key = parts[0]; 24 | var value = parts[1]; 25 | 26 | if (value === undefined) { 27 | value = true; 28 | } else { 29 | // unquote value 30 | if (value[0] === '"') { 31 | value = value.slice(1); 32 | } 33 | if (value[value.length - 1] === '"') { 34 | value = value.slice(0, value.length - 1); 35 | } 36 | } 37 | (parsedParams[key] = parsedParams[key] || []).push(value); 38 | }); 39 | 40 | paramsList.push(parsedParams); 41 | }); 42 | 43 | return extensions; 44 | }; 45 | 46 | /** 47 | * Serialize a parsed `Sec-WebSocket-Extensions` header to a string. 48 | * 49 | * @param {Object} value The object to format 50 | * @return {String} A string representing the given value 51 | * @public 52 | */ 53 | const format = (value) => { 54 | return Object.keys(value).map((token) => { 55 | var paramsList = value[token]; 56 | if (!Array.isArray(paramsList)) paramsList = [paramsList]; 57 | return paramsList.map((params) => { 58 | return [token].concat(Object.keys(params).map((k) => { 59 | var p = params[k]; 60 | if (!Array.isArray(p)) p = [p]; 61 | return p.map((v) => v === true ? k : `${k}=${v}`).join('; '); 62 | })).join('; '); 63 | }).join(', '); 64 | }).join(', '); 65 | }; 66 | 67 | module.exports = { format, parse }; 68 | -------------------------------------------------------------------------------- /lib/BufferUtil.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const safeBuffer = require('safe-buffer'); 10 | 11 | const Buffer = safeBuffer.Buffer; 12 | 13 | /** 14 | * Merges an array of buffers into a new buffer. 15 | * 16 | * @param {Buffer[]} list The array of buffers to concat 17 | * @param {Number} totalLength The total length of buffers in the list 18 | * @return {Buffer} The resulting buffer 19 | * @public 20 | */ 21 | const concat = (list, totalLength) => { 22 | const target = Buffer.allocUnsafe(totalLength); 23 | var offset = 0; 24 | 25 | for (var i = 0; i < list.length; i++) { 26 | const buf = list[i]; 27 | buf.copy(target, offset); 28 | offset += buf.length; 29 | } 30 | 31 | return target; 32 | }; 33 | 34 | try { 35 | const bufferUtil = require('bufferutil'); 36 | 37 | module.exports = Object.assign({ concat }, bufferUtil.BufferUtil || bufferUtil); 38 | } catch (e) /* istanbul ignore next */ { 39 | /** 40 | * Masks a buffer using the given mask. 41 | * 42 | * @param {Buffer} source The buffer to mask 43 | * @param {Buffer} mask The mask to use 44 | * @param {Buffer} output The buffer where to store the result 45 | * @param {Number} offset The offset at which to start writing 46 | * @param {Number} length The number of bytes to mask. 47 | * @public 48 | */ 49 | const mask = (source, mask, output, offset, length) => { 50 | for (var i = 0; i < length; i++) { 51 | output[offset + i] = source[i] ^ mask[i & 3]; 52 | } 53 | }; 54 | 55 | /** 56 | * Unmasks a buffer using the given mask. 57 | * 58 | * @param {Buffer} buffer The buffer to unmask 59 | * @param {Buffer} mask The mask to use 60 | * @public 61 | */ 62 | const unmask = (buffer, mask) => { 63 | // Required until https://github.com/nodejs/node/issues/9006 is resolved. 64 | const length = buffer.length; 65 | for (var i = 0; i < length; i++) { 66 | buffer[i] ^= mask[i & 3]; 67 | } 68 | }; 69 | 70 | module.exports = { concat, mask, unmask }; 71 | } 72 | -------------------------------------------------------------------------------- /examples/express-session-parse/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const session = require('express-session'); 4 | const express = require('express'); 5 | const http = require('http'); 6 | const uuid = require('uuid'); 7 | 8 | const WebSocket = require('../..'); 9 | 10 | const app = express(); 11 | 12 | // 13 | // We need the same instance of the session parser in express and 14 | // WebSocket server. 15 | // 16 | const sessionParser = session({ 17 | saveUninitialized: false, 18 | secret: '$eCuRiTy', 19 | resave: false 20 | }); 21 | 22 | // 23 | // Serve static files from the 'public' folder. 24 | // 25 | app.use(express.static('public')); 26 | app.use(sessionParser); 27 | 28 | app.post('/login', (req, res) => { 29 | // 30 | // "Log in" user and set userId to session. 31 | // 32 | const id = uuid.v4(); 33 | 34 | console.log(`Updating session for user ${id}`); 35 | req.session.userId = id; 36 | res.send({ result: 'OK', message: 'Session updated' }); 37 | }); 38 | 39 | app.delete('/logout', (request, response) => { 40 | console.log('Destroying session'); 41 | request.session.destroy(); 42 | response.send({ result: 'OK', message: 'Session destroyed' }); 43 | }); 44 | 45 | // 46 | // Create HTTP server by ourselves. 47 | // 48 | const server = http.createServer(app); 49 | 50 | const wss = new WebSocket.Server({ 51 | verifyClient: (info, done) => { 52 | console.log('Parsing session from request...'); 53 | sessionParser(info.req, {}, () => { 54 | console.log('Session is parsed!'); 55 | 56 | // 57 | // We can reject the connection by returning false to done(). For example, 58 | // reject here if user is unknown. 59 | // 60 | done(info.req.session.userId); 61 | }); 62 | }, 63 | server 64 | }); 65 | 66 | wss.on('connection', (ws) => { 67 | ws.on('message', (message) => { 68 | const session = ws.upgradeReq.session; 69 | 70 | // 71 | // Here we can now use session parameters. 72 | // 73 | console.log(`WS message ${message} from user ${session.userId}`); 74 | }); 75 | }); 76 | 77 | // 78 | // Start the server. 79 | // 80 | server.listen(8080, () => console.log('Listening on http://localhost:8080')); 81 | -------------------------------------------------------------------------------- /test/Validation.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeBuffer = require('safe-buffer'); 4 | const assert = require('assert'); 5 | 6 | const isValidUTF8 = require('../lib/Validation'); 7 | 8 | const Buffer = safeBuffer.Buffer; 9 | 10 | describe('Validation', function () { 11 | describe('isValidUTF8', function () { 12 | it('should return true for a valid utf8 string', function () { 13 | const validBuffer = Buffer.from( 14 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 15 | 'Quisque gravida mattis rhoncus. Donec iaculis, metus ' + 16 | 'quis varius accumsan, erat mauris condimentum diam, et ' + 17 | 'egestas erat enim ut ligula. Praesent sollicitudin tellus ' + 18 | 'eget dolor euismod euismod. Nullam ac augue nec neque ' + 19 | 'varius luctus. Curabitur elit mi, consequat ultricies ' + 20 | 'adipiscing mollis, scelerisque in erat. Phasellus facilisis ' + 21 | ' fermentum ullamcorper. Nulla et sem eu arcu pharetra ' + 22 | 'pellentesque. Praesent consectetur tempor justo, vel ' + 23 | 'iaculis dui ullamcorper sit amet. Integer tristique viverra ' + 24 | 'ullamcorper. Vivamus laoreet, nulla eget suscipit eleifend, ' + 25 | 'lacus lectus feugiat libero, non fermentum erat nisi at ' + 26 | 'risus. Lorem ipsum dolor sit amet, consectetur adipiscing ' + 27 | 'elit. Ut pulvinar dignissim tellus, eu dignissim lorem ' + 28 | 'vulputate quis. Morbi ut pulvinar augue.' 29 | ); 30 | 31 | assert.ok(isValidUTF8(validBuffer)); 32 | }); 33 | 34 | it('should return false for an erroneous string', function () { 35 | const invalidBuffer = Buffer.from([ 36 | 0xce, 0xba, 0xe1, 0xbd, 0xb9, 0xcf, 0x83, 37 | 0xce, 0xbc, 0xce, 0xb5, 0xed, 0xa0, 0x80, 38 | 0x65, 0x64, 0x69, 0x74, 0x65, 0x64 39 | ]); 40 | 41 | assert.ok(!isValidUTF8(invalidBuffer)); 42 | }); 43 | 44 | it('should return true for valid cases from the autobahn test suite', function () { 45 | assert.ok(isValidUTF8(Buffer.from([0xf0, 0x90, 0x80, 0x80]))); 46 | assert.ok(isValidUTF8(Buffer.from('\xf0\x90\x80\x80'))); 47 | }); 48 | 49 | it('should return false for erroneous autobahn strings', function () { 50 | assert.ok(!isValidUTF8(Buffer.from([0xce, 0xba, 0xe1, 0xbd]))); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /bench/parser.benchmark.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const safeBuffer = require('safe-buffer'); 10 | const benchmark = require('benchmark'); 11 | const crypto = require('crypto'); 12 | 13 | const util = require('../test/hybi-util'); 14 | const WebSocket = require('..'); 15 | 16 | const Receiver = WebSocket.Receiver; 17 | const Buffer = safeBuffer.Buffer; 18 | 19 | // 20 | // Override the `cleanup` method to make the "close message" test work as 21 | // expected. 22 | // 23 | Receiver.prototype.cleanup = function () { 24 | this.state = 0; 25 | }; 26 | 27 | function createBinaryPacket (length) { 28 | const message = crypto.randomBytes(length); 29 | 30 | return Buffer.from('82' + util.getHybiLengthAsHexString(length, true) + 31 | '3483a868' + util.mask(message, '3483a868').toString('hex'), 'hex'); 32 | } 33 | 34 | const pingMessage = 'Hello'; 35 | const pingPacket1 = Buffer.from('89' + util.pack(2, 0x80 | pingMessage.length) + 36 | '3483a868' + util.mask(pingMessage, '3483a868').toString('hex'), 'hex'); 37 | 38 | const textMessage = 'a'.repeat(20); 39 | const maskedTextPacket = Buffer.from('81' + util.pack(2, 0x80 | textMessage.length) + 40 | '61616161' + util.mask(textMessage, '61616161').toString('hex'), 'hex'); 41 | 42 | const pingPacket2 = Buffer.from('8900', 'hex'); 43 | const closePacket = Buffer.from('8800', 'hex'); 44 | const binaryDataPacket = createBinaryPacket(125); 45 | const binaryDataPacket2 = createBinaryPacket(65535); 46 | const binaryDataPacket3 = createBinaryPacket(200 * 1024); 47 | const binaryDataPacket4 = createBinaryPacket(1024 * 1024); 48 | 49 | const suite = new benchmark.Suite(); 50 | const receiver = new Receiver(); 51 | 52 | receiver.onmessage = receiver.onclose = receiver.onping = () => {}; 53 | 54 | suite.add('ping message', () => receiver.add(pingPacket1)); 55 | suite.add('ping with no data', () => receiver.add(pingPacket2)); 56 | suite.add('close message', () => receiver.add(closePacket)); 57 | suite.add('masked text message (20 bytes)', () => receiver.add(maskedTextPacket)); 58 | suite.add('binary data (125 bytes)', () => receiver.add(binaryDataPacket)); 59 | suite.add('binary data (65535 bytes)', () => receiver.add(binaryDataPacket2)); 60 | suite.add('binary data (200 KiB)', () => receiver.add(binaryDataPacket3)); 61 | suite.add('binary data (1 MiB)', () => receiver.add(binaryDataPacket4)); 62 | 63 | suite.on('cycle', (e) => console.log(e.target.toString())); 64 | 65 | if (require.main === module) { 66 | suite.run({ async: true }); 67 | } else { 68 | module.exports = suite; 69 | } 70 | -------------------------------------------------------------------------------- /bench/speed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeBuffer = require('safe-buffer'); 4 | const cluster = require('cluster'); 5 | 6 | const WebSocket = require('..'); 7 | 8 | const Buffer = safeBuffer.Buffer; 9 | const port = 8181; 10 | 11 | if (cluster.isMaster) { 12 | const wss = new WebSocket.Server({ 13 | maxPayload: 600 * 1024 * 1024, 14 | perMessageDeflate: false, 15 | clientTracking: false, 16 | port 17 | }, () => cluster.fork()); 18 | 19 | wss.on('connection', (ws) => { 20 | ws.on('message', (data, flags) => ws.send(data, { binary: flags.binary || false })); 21 | }); 22 | 23 | cluster.on('exit', () => wss.close()); 24 | } else { 25 | const configs = [ 26 | [true, 10000, 64], 27 | [true, 5000, 16 * 1024], 28 | [true, 1000, 128 * 1024], 29 | [true, 100, 1024 * 1024], 30 | [true, 1, 500 * 1024 * 1024], 31 | [false, 10000, 64], 32 | [false, 5000, 16 * 1024], 33 | [false, 1000, 128 * 1024], 34 | [false, 100, 1024 * 1024] 35 | ]; 36 | 37 | const roundPrec = (num, prec) => { 38 | const mul = Math.pow(10, prec); 39 | return Math.round(num * mul) / mul; 40 | }; 41 | 42 | const humanSize = (bytes) => { 43 | if (bytes >= 1048576) return roundPrec(bytes / 1048576, 2) + ' MiB'; 44 | if (bytes >= 1024) return roundPrec(bytes / 1024, 2) + ' KiB'; 45 | return roundPrec(bytes, 2) + ' B'; 46 | }; 47 | 48 | const largest = configs.reduce((prev, curr) => curr[2] > prev ? curr[2] : prev, 0); 49 | console.log('Generating %s of test data...', humanSize(largest)); 50 | const randomBytes = Buffer.allocUnsafe(largest); 51 | 52 | for (var i = 0; i < largest; ++i) { 53 | randomBytes[i] = ~~(Math.random() * 127); 54 | } 55 | 56 | const runConfig = (useBinary, roundtrips, size, cb) => { 57 | const data = randomBytes.slice(0, size); 58 | const ws = new WebSocket(`ws://localhost:${port}`); 59 | var roundtrip = 0; 60 | var time; 61 | 62 | ws.on('error', (err) => { 63 | console.error(err.stack); 64 | cluster.worker.kill(); 65 | }); 66 | ws.on('open', () => { 67 | time = process.hrtime(); 68 | ws.send(data, { binary: useBinary }); 69 | }); 70 | ws.on('message', () => { 71 | if (++roundtrip !== roundtrips) return ws.send(data, { binary: useBinary }); 72 | 73 | var elapsed = process.hrtime(time); 74 | elapsed = (elapsed[0] * 1e9) + elapsed[1]; 75 | 76 | console.log( 77 | '%d roundtrips of %s %s data:\t%ss\t%s', 78 | roundtrips, 79 | humanSize(size), 80 | useBinary ? 'binary' : 'text', 81 | roundPrec(elapsed / 1e9, 1), 82 | humanSize(size * roundtrips / elapsed * 1e9) + '/s' 83 | ); 84 | 85 | ws.close(); 86 | cb(); 87 | }); 88 | }; 89 | 90 | (function run () { 91 | if (configs.length === 0) return cluster.worker.kill(); 92 | var config = configs.shift(); 93 | config.push(run); 94 | runConfig.apply(null, config); 95 | })(); 96 | } 97 | -------------------------------------------------------------------------------- /examples/fileapi/server.js: -------------------------------------------------------------------------------- 1 | var WebSocketServer = require('../../').Server; 2 | var express = require('express'); 3 | var fs = require('fs'); 4 | var util = require('util'); 5 | var path = require('path'); 6 | var app = express(); 7 | var server = require('http').Server(app); 8 | var events = require('events'); 9 | var ansi = require('ansi'); 10 | var cursor = ansi(process.stdout); 11 | 12 | function BandwidthSampler (ws, interval) { 13 | interval = interval || 2000; 14 | var previousByteCount = 0; 15 | var self = this; 16 | var intervalId = setInterval(function () { 17 | var byteCount = ws.bytesReceived; 18 | var bytesPerSec = (byteCount - previousByteCount) / (interval / 1000); 19 | previousByteCount = byteCount; 20 | self.emit('sample', bytesPerSec); 21 | }, interval); 22 | ws.on('close', function () { 23 | clearInterval(intervalId); 24 | }); 25 | } 26 | util.inherits(BandwidthSampler, events.EventEmitter); 27 | 28 | function makePathForFile (filePath, prefix, cb) { 29 | if (typeof cb !== 'function') throw new Error('callback is required'); 30 | filePath = path.dirname(path.normalize(filePath)).replace(/^(\/|\\)+/, ''); 31 | var pieces = filePath.split(/(\\|\/)/); 32 | var incrementalPath = prefix; 33 | function step (error) { 34 | if (error) return cb(error); 35 | if (pieces.length === 0) return cb(null, incrementalPath); 36 | incrementalPath += '/' + pieces.shift(); 37 | fs.access(incrementalPath, function (err) { 38 | if (err) fs.mkdir(incrementalPath, step); 39 | else process.nextTick(step); 40 | }); 41 | } 42 | step(); 43 | } 44 | 45 | cursor.eraseData(2).goto(1, 1); 46 | app.use(express.static(path.join(__dirname, '/public'))); 47 | 48 | var clientId = 0; 49 | var wss = new WebSocketServer({server: server}); 50 | wss.on('connection', function (ws) { 51 | var thisId = ++clientId; 52 | cursor.goto(1, 4 + thisId).eraseLine(); 53 | console.log('Client #%d connected', thisId); 54 | 55 | var sampler = new BandwidthSampler(ws); 56 | sampler.on('sample', function (bps) { 57 | cursor.goto(1, 4 + thisId).eraseLine(); 58 | console.log('WebSocket #%d incoming bandwidth: %d MB/s', thisId, Math.round(bps / (1024 * 1024))); 59 | }); 60 | 61 | var filesReceived = 0; 62 | var currentFile = null; 63 | ws.on('message', function (data, flags) { 64 | if (!flags.binary) { 65 | currentFile = JSON.parse(data); 66 | // note: a real-world app would want to sanity check the data 67 | } else { 68 | if (currentFile == null) return; 69 | makePathForFile(currentFile.path, path.join(__dirname, '/uploaded'), function (error, path) { 70 | if (error) { 71 | console.log(error); 72 | ws.send(JSON.stringify({event: 'error', path: currentFile.path, message: error.message})); 73 | return; 74 | } 75 | fs.writeFile(path + '/' + currentFile.name, data, function (error) { 76 | if (error) { 77 | console.log(error); 78 | ws.send(JSON.stringify({event: 'error', path: currentFile.path, message: error.message})); 79 | return; 80 | } 81 | ++filesReceived; 82 | // console.log('received %d bytes long file, %s', data.length, currentFile.path); 83 | ws.send(JSON.stringify({event: 'complete', path: currentFile.path})); 84 | currentFile = null; 85 | }); 86 | }); 87 | } 88 | }); 89 | 90 | ws.on('close', function () { 91 | cursor.goto(1, 4 + thisId).eraseLine(); 92 | console.log('Client #%d disconnected. %d files received.', thisId, filesReceived); 93 | }); 94 | 95 | ws.on('error', function (e) { 96 | cursor.goto(1, 4 + thisId).eraseLine(); 97 | console.log('Client #%d error: %s', thisId, e.message); 98 | }); 99 | }); 100 | 101 | fs.mkdir(path.join(__dirname, '/uploaded'), function () { 102 | // ignore errors, most likely means directory exists 103 | console.log('Uploaded files will be saved to %s/uploaded.', __dirname); 104 | console.log('Remember to wipe this directory if you upload lots and lots.'); 105 | server.listen(8080, function () { 106 | console.log('Listening on http://localhost:8080'); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /lib/EventTarget.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Class representing an event. 5 | * 6 | * @private 7 | */ 8 | class Event { 9 | /** 10 | * Create a new `Event`. 11 | * 12 | * @param {String} type The name of the event 13 | * @param {Object} target A reference to the target to which the event was dispatched 14 | */ 15 | constructor (type, target) { 16 | this.target = target; 17 | this.type = type; 18 | } 19 | } 20 | 21 | /** 22 | * Class representing a message event. 23 | * 24 | * @extends Event 25 | * @private 26 | */ 27 | class MessageEvent extends Event { 28 | /** 29 | * Create a new `MessageEvent`. 30 | * 31 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data 32 | * @param {Boolean} isBinary Specifies if `data` is binary 33 | * @param {WebSocket} target A reference to the target to which the event was dispatched 34 | */ 35 | constructor (data, isBinary, target) { 36 | super('message', target); 37 | 38 | this.binary = isBinary; // non-standard. 39 | this.data = data; 40 | } 41 | } 42 | 43 | /** 44 | * Class representing a close event. 45 | * 46 | * @extends Event 47 | * @private 48 | */ 49 | class CloseEvent extends Event { 50 | /** 51 | * Create a new `CloseEvent`. 52 | * 53 | * @param {Number} code The status code explaining why the connection is being closed 54 | * @param {String} reason A human-readable string explaining why the connection is closing 55 | * @param {WebSocket} target A reference to the target to which the event was dispatched 56 | */ 57 | constructor (code, reason, target) { 58 | super('close', target); 59 | 60 | this.wasClean = code === undefined || code === 1000; 61 | this.reason = reason; 62 | this.target = target; 63 | this.type = 'close'; 64 | this.code = code; 65 | } 66 | } 67 | 68 | /** 69 | * Class representing an open event. 70 | * 71 | * @extends Event 72 | * @private 73 | */ 74 | class OpenEvent extends Event { 75 | /** 76 | * Create a new `OpenEvent`. 77 | * 78 | * @param {WebSocket} target A reference to the target to which the event was dispatched 79 | */ 80 | constructor (target) { 81 | super('open', target); 82 | } 83 | } 84 | 85 | /** 86 | * This provides methods for emulating the `EventTarget` interface. It's not 87 | * meant to be used directly. 88 | * 89 | * @mixin 90 | */ 91 | const EventTarget = { 92 | /** 93 | * Register an event listener. 94 | * 95 | * @param {String} method A string representing the event type to listen for 96 | * @param {Function} listener The listener to add 97 | * @public 98 | */ 99 | addEventListener (method, listener) { 100 | if (typeof listener !== 'function') return; 101 | 102 | function onMessage (data, flags) { 103 | listener.call(this, new MessageEvent(data, !!flags.binary, this)); 104 | } 105 | 106 | function onClose (code, message) { 107 | listener.call(this, new CloseEvent(code, message, this)); 108 | } 109 | 110 | function onError (event) { 111 | event.type = 'error'; 112 | event.target = this; 113 | listener.call(this, event); 114 | } 115 | 116 | function onOpen () { 117 | listener.call(this, new OpenEvent(this)); 118 | } 119 | 120 | if (method === 'message') { 121 | onMessage._listener = listener; 122 | this.on(method, onMessage); 123 | } else if (method === 'close') { 124 | onClose._listener = listener; 125 | this.on(method, onClose); 126 | } else if (method === 'error') { 127 | onError._listener = listener; 128 | this.on(method, onError); 129 | } else if (method === 'open') { 130 | onOpen._listener = listener; 131 | this.on(method, onOpen); 132 | } else { 133 | this.on(method, listener); 134 | } 135 | }, 136 | 137 | /** 138 | * Remove an event listener. 139 | * 140 | * @param {String} method A string representing the event type to remove 141 | * @param {Function} listener The listener to remove 142 | * @public 143 | */ 144 | removeEventListener (method, listener) { 145 | const listeners = this.listeners(method); 146 | 147 | for (var i = 0; i < listeners.length; i++) { 148 | if (listeners[i] === listener || listeners[i]._listener === listener) { 149 | this.removeListener(method, listeners[i]); 150 | } 151 | } 152 | } 153 | }; 154 | 155 | module.exports = EventTarget; 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ws: a Node.js WebSocket library 2 | 3 | [![Version npm](https://img.shields.io/npm/v/ws.svg)](https://www.npmjs.com/package/ws) 4 | [![Linux Build](https://img.shields.io/travis/websockets/ws/master.svg)](https://travis-ci.org/websockets/ws) 5 | [![Windows Build](https://ci.appveyor.com/api/projects/status/github/websockets/ws?branch=master&svg=true)](https://ci.appveyor.com/project/lpinca/ws) 6 | [![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/r/websockets/ws?branch=master) 7 | 8 | `ws` is a simple to use, blazing fast, and thoroughly tested WebSocket client 9 | and server implementation. 10 | 11 | Passes the quite extensive Autobahn test suite. See http://websockets.github.io/ws/ 12 | for the full reports. 13 | 14 | ## Protocol support 15 | 16 | * **HyBi drafts 07-12** (Use the option `protocolVersion: 8`) 17 | * **HyBi drafts 13-17** (Current default, alternatively option `protocolVersion: 13`) 18 | 19 | ## Installing 20 | 21 | ``` 22 | npm install --save ws 23 | ``` 24 | 25 | ### Opt-in for performance 26 | 27 | There are 2 optional modules that can be installed along side with the `ws` 28 | module. These modules are binary addons which improve certain operations, but as 29 | they are binary addons they require compilation which can fail if no c++ 30 | compiler is installed on the host system. 31 | 32 | - `npm install --save bufferutil`: Improves internal buffer operations which 33 | allows for faster processing of masked WebSocket frames and general buffer 34 | operations. 35 | - `npm install --save utf-8-validate`: The specification requires validation of 36 | invalid UTF-8 chars, some of these validations could not be done in JavaScript 37 | hence the need for a binary addon. In most cases you will already be 38 | validating the input that you receive for security purposes leading to double 39 | validation. But if you want to be 100% spec-conforming and have fast 40 | validation of UTF-8 then this module is a must. 41 | 42 | ## API Docs 43 | 44 | See [`/doc/ws.md`](https://github.com/websockets/ws/blob/master/doc/ws.md) 45 | for Node.js-like docs for the ws classes. 46 | 47 | ## WebSocket compression 48 | 49 | `ws` supports the [permessage-deflate extension][permessage-deflate] extension 50 | which enables the client and server to negotiate a compression algorithm and 51 | its parameters, and then selectively apply it to the data payloads of each 52 | WebSocket message. 53 | 54 | The extension is enabled by default but adds a significant overhead in terms of 55 | performance and memory comsumption. We suggest to use WebSocket compression 56 | only if it is really needed. 57 | 58 | To disable the extension you can set the `perMessageDeflate` option to `false`. 59 | On the server: 60 | 61 | ```js 62 | const WebSocket = require('ws'); 63 | 64 | const wss = new WebSocket.Server({ 65 | perMessageDeflate: false, 66 | port: 8080 67 | }); 68 | ``` 69 | 70 | On the client: 71 | 72 | ```js 73 | const WebSocket = require('ws'); 74 | 75 | const ws = new WebSocket('ws://www.host.com/path', { 76 | perMessageDeflate: false 77 | }); 78 | ``` 79 | 80 | ## Usage examples 81 | 82 | ### Sending and receiving text data 83 | 84 | ```js 85 | const WebSocket = require('ws'); 86 | 87 | const ws = new WebSocket('ws://www.host.com/path'); 88 | 89 | ws.on('open', function open() { 90 | ws.send('something'); 91 | }); 92 | 93 | ws.on('message', function incoming(data, flags) { 94 | // flags.binary will be set if a binary data is received. 95 | // flags.masked will be set if the data was masked. 96 | }); 97 | ``` 98 | 99 | ### Sending binary data 100 | 101 | ```js 102 | const WebSocket = require('ws'); 103 | 104 | const ws = new WebSocket('ws://www.host.com/path'); 105 | 106 | ws.on('open', function open() { 107 | const array = new Float32Array(5); 108 | 109 | for (var i = 0; i < array.length; ++i) { 110 | array[i] = i / 2; 111 | } 112 | 113 | ws.send(array); 114 | }); 115 | ``` 116 | 117 | ### Server example 118 | 119 | ```js 120 | const WebSocket = require('ws'); 121 | 122 | const wss = new WebSocket.Server({ port: 8080 }); 123 | 124 | wss.on('connection', function connection(ws) { 125 | ws.on('message', function incoming(message) { 126 | console.log('received: %s', message); 127 | }); 128 | 129 | ws.send('something'); 130 | }); 131 | ``` 132 | 133 | ### Broadcast example 134 | 135 | ```js 136 | const WebSocket = require('ws'); 137 | 138 | const wss = new WebSocket.Server({ port: 8080 }); 139 | 140 | // Broadcast to all. 141 | wss.broadcast = function broadcast(data) { 142 | wss.clients.forEach(function each(client) { 143 | if (client.readyState === WebSocket.OPEN) { 144 | client.send(data); 145 | } 146 | }); 147 | }; 148 | 149 | wss.on('connection', function connection(ws) { 150 | ws.on('message', function incoming(data) { 151 | // Broadcast to everyone else. 152 | wss.clients.forEach(function each(client) { 153 | if (client !== ws && client.readyState === WebSocket.OPEN) { 154 | client.send(data); 155 | } 156 | }); 157 | }); 158 | }); 159 | ``` 160 | 161 | ### ExpressJS example 162 | 163 | ```js 164 | const express = require('express'); 165 | const http = require('http'); 166 | const url = require('url'); 167 | const WebSocket = require('ws'); 168 | 169 | const app = express(); 170 | 171 | app.use(function (req, res) { 172 | res.send({ msg: "hello" }); 173 | }); 174 | 175 | const server = http.createServer(app); 176 | const wss = new WebSocket.Server({ server }); 177 | 178 | wss.on('connection', function connection(ws) { 179 | const location = url.parse(ws.upgradeReq.url, true); 180 | // You might use location.query.access_token to authenticate or share sessions 181 | // or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312) 182 | 183 | ws.on('message', function incoming(message) { 184 | console.log('received: %s', message); 185 | }); 186 | 187 | ws.send('something'); 188 | }); 189 | 190 | server.listen(8080, function listening() { 191 | console.log('Listening on %d', server.address().port); 192 | }); 193 | ``` 194 | 195 | ### echo.websocket.org demo 196 | 197 | ```js 198 | const WebSocket = require('ws'); 199 | 200 | const ws = new WebSocket('wss://echo.websocket.org/', { 201 | origin: 'https://websocket.org' 202 | }); 203 | 204 | ws.on('open', function open() { 205 | console.log('connected'); 206 | ws.send(Date.now()); 207 | }); 208 | 209 | ws.on('close', function close() { 210 | console.log('disconnected'); 211 | }); 212 | 213 | ws.on('message', function incoming(data, flags) { 214 | console.log(`Roundtrip time: ${Date.now() - data} ms`, flags); 215 | 216 | setTimeout(function timeout() { 217 | ws.send(Date.now()); 218 | }, 500); 219 | }); 220 | ``` 221 | 222 | ### Other examples 223 | 224 | For a full example with a browser client communicating with a ws server, see the 225 | examples folder. 226 | 227 | Otherwise, see the test cases. 228 | 229 | ## Error handling best practices 230 | 231 | ```js 232 | // If the WebSocket is closed before the following send is attempted 233 | ws.send('something'); 234 | 235 | // Errors (both immediate and async write errors) can be detected in an optional 236 | // callback. The callback is also the only way of being notified that data has 237 | // actually been sent. 238 | ws.send('something', function ack(error) { 239 | // If error is not defined, the send has been completed, otherwise the error 240 | // object will indicate what failed. 241 | }); 242 | 243 | // Immediate errors can also be handled with `try...catch`, but **note** that 244 | // since sends are inherently asynchronous, socket write failures will *not* be 245 | // captured when this technique is used. 246 | try { ws.send('something'); } 247 | catch (e) { /* handle error */ } 248 | ``` 249 | 250 | ## Changelog 251 | 252 | We're using the GitHub [`releases`](https://github.com/websockets/ws/releases) 253 | for changelog entries. 254 | 255 | ## License 256 | 257 | [MIT](LICENSE) 258 | 259 | [permessage-deflate]: https://tools.ietf.org/html/rfc7692 260 | -------------------------------------------------------------------------------- /test/Sender.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeBuffer = require('safe-buffer'); 4 | const assert = require('assert'); 5 | 6 | const PerMessageDeflate = require('../lib/PerMessageDeflate'); 7 | const Sender = require('../lib/Sender'); 8 | 9 | const Buffer = safeBuffer.Buffer; 10 | 11 | describe('Sender', function () { 12 | describe('.frame', function () { 13 | it('does not mutate the input buffer if data is `readOnly`', function () { 14 | const buf = Buffer.from([1, 2, 3, 4, 5]); 15 | 16 | Sender.frame(buf, { 17 | readOnly: true, 18 | rsv1: false, 19 | mask: true, 20 | opcode: 2, 21 | fin: true 22 | }); 23 | 24 | assert.ok(buf.equals(Buffer.from([1, 2, 3, 4, 5]))); 25 | }); 26 | 27 | it('sets RSV1 bit if compressed', function () { 28 | const list = Sender.frame(Buffer.from('hi'), { 29 | readOnly: false, 30 | mask: false, 31 | rsv1: true, 32 | opcode: 1, 33 | fin: true 34 | }); 35 | 36 | assert.strictEqual(list[0][0] & 0x40, 0x40); 37 | }); 38 | }); 39 | 40 | describe('#send', function () { 41 | it('compresses data if compress option is enabled', function (done) { 42 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 43 | let count = 0; 44 | const sender = new Sender({ 45 | write: (data) => { 46 | assert.strictEqual(data[0] & 0x40, 0x40); 47 | if (++count === 3) done(); 48 | } 49 | }, { 50 | 'permessage-deflate': perMessageDeflate 51 | }); 52 | 53 | perMessageDeflate.accept([{}]); 54 | 55 | const options = { compress: true, fin: true }; 56 | const array = new Uint8Array([0x68, 0x69]); 57 | 58 | sender.send(array.buffer, options); 59 | sender.send(array, options); 60 | sender.send('hi', options); 61 | }); 62 | 63 | it('does not compress data for small payloads', function (done) { 64 | const perMessageDeflate = new PerMessageDeflate(); 65 | const sender = new Sender({ 66 | write: (data) => { 67 | assert.notStrictEqual(data[0] & 0x40, 0x40); 68 | done(); 69 | } 70 | }, { 71 | 'permessage-deflate': perMessageDeflate 72 | }); 73 | 74 | perMessageDeflate.accept([{}]); 75 | 76 | sender.send('hi', { compress: true, fin: true }); 77 | }); 78 | 79 | it('compresses all frames in a fragmented message', function (done) { 80 | const fragments = []; 81 | const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); 82 | const sender = new Sender({ 83 | write: (data) => { 84 | fragments.push(data); 85 | if (fragments.length !== 2) return; 86 | 87 | assert.strictEqual(fragments[0][0] & 0x40, 0x40); 88 | assert.strictEqual(fragments[0].length, 11); 89 | assert.strictEqual(fragments[1][0] & 0x40, 0x00); 90 | assert.strictEqual(fragments[1].length, 6); 91 | done(); 92 | } 93 | }, { 94 | 'permessage-deflate': perMessageDeflate 95 | }); 96 | 97 | perMessageDeflate.accept([{}]); 98 | 99 | sender.send('123', { compress: true, fin: false }); 100 | sender.send('12', { compress: true, fin: true }); 101 | }); 102 | 103 | it('compresses no frames in a fragmented message', function (done) { 104 | const fragments = []; 105 | const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); 106 | const sender = new Sender({ 107 | write: (data) => { 108 | fragments.push(data); 109 | if (fragments.length !== 2) return; 110 | 111 | assert.strictEqual(fragments[0][0] & 0x40, 0x00); 112 | assert.strictEqual(fragments[0].length, 4); 113 | assert.strictEqual(fragments[1][0] & 0x40, 0x00); 114 | assert.strictEqual(fragments[1].length, 5); 115 | done(); 116 | } 117 | }, { 118 | 'permessage-deflate': perMessageDeflate 119 | }); 120 | 121 | perMessageDeflate.accept([{}]); 122 | 123 | sender.send('12', { compress: true, fin: false }); 124 | sender.send('123', { compress: true, fin: true }); 125 | }); 126 | 127 | it('compresses empty buffer as first fragment', function (done) { 128 | const fragments = []; 129 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 130 | const sender = new Sender({ 131 | write: (data) => { 132 | fragments.push(data); 133 | if (fragments.length !== 2) return; 134 | 135 | assert.strictEqual(fragments[0][0] & 0x40, 0x40); 136 | assert.strictEqual(fragments[0].length, 3); 137 | assert.strictEqual(fragments[1][0] & 0x40, 0x00); 138 | assert.strictEqual(fragments[1].length, 8); 139 | done(); 140 | } 141 | }, { 142 | 'permessage-deflate': perMessageDeflate 143 | }); 144 | 145 | perMessageDeflate.accept([{}]); 146 | 147 | sender.send(Buffer.alloc(0), { compress: true, fin: false }); 148 | sender.send('data', { compress: true, fin: true }); 149 | }); 150 | 151 | it('compresses empty buffer as last fragment', function (done) { 152 | const fragments = []; 153 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 154 | const sender = new Sender({ 155 | write: (data) => { 156 | fragments.push(data); 157 | if (fragments.length !== 2) return; 158 | 159 | assert.strictEqual(fragments[0][0] & 0x40, 0x40); 160 | assert.strictEqual(fragments[0].length, 12); 161 | assert.strictEqual(fragments[1][0] & 0x40, 0x00); 162 | assert.strictEqual(fragments[1].length, 3); 163 | done(); 164 | } 165 | }, { 166 | 'permessage-deflate': perMessageDeflate 167 | }); 168 | 169 | perMessageDeflate.accept([{}]); 170 | 171 | sender.send('data', { compress: true, fin: false }); 172 | sender.send(Buffer.alloc(0), { compress: true, fin: true }); 173 | }); 174 | 175 | it('handles many send calls while processing without crashing on flush', function (done) { 176 | let count = 0; 177 | const perMessageDeflate = new PerMessageDeflate(); 178 | const sender = new Sender({ 179 | write: () => { 180 | if (++count > 1e4) done(); 181 | } 182 | }, { 183 | 'permessage-deflate': perMessageDeflate 184 | }); 185 | 186 | perMessageDeflate.accept([{}]); 187 | 188 | for (let i = 0; i < 1e4; i++) { 189 | sender.processing = true; 190 | sender.send('hi', { compress: false, fin: true }); 191 | } 192 | 193 | sender.processing = false; 194 | sender.send('hi', { compress: false, fin: true }); 195 | }); 196 | }); 197 | 198 | describe('#ping', function () { 199 | it('works with multiple types of data', function (done) { 200 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 201 | let count = 0; 202 | const sender = new Sender({ 203 | write: (data) => { 204 | if (++count === 1) return; 205 | 206 | assert.ok(data.equals(Buffer.from([0x89, 0x02, 0x68, 0x69]))); 207 | if (count === 4) done(); 208 | } 209 | }, { 210 | 'permessage-deflate': perMessageDeflate 211 | }); 212 | 213 | perMessageDeflate.accept([{}]); 214 | 215 | const array = new Uint8Array([0x68, 0x69]); 216 | 217 | sender.send('foo', { compress: true, fin: true }); 218 | sender.ping(array.buffer, false); 219 | sender.ping(array, false); 220 | sender.ping('hi', false); 221 | }); 222 | }); 223 | 224 | describe('#pong', function () { 225 | it('works with multiple types of data', function (done) { 226 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 227 | let count = 0; 228 | const sender = new Sender({ 229 | write: (data) => { 230 | if (++count === 1) return; 231 | 232 | assert.ok(data.equals(Buffer.from([0x8a, 0x02, 0x68, 0x69]))); 233 | if (count === 4) done(); 234 | } 235 | }, { 236 | 'permessage-deflate': perMessageDeflate 237 | }); 238 | 239 | perMessageDeflate.accept([{}]); 240 | 241 | const array = new Uint8Array([0x68, 0x69]); 242 | 243 | sender.send('foo', { compress: true, fin: true }); 244 | sender.pong(array.buffer, false); 245 | sender.pong(array, false); 246 | sender.pong('hi', false); 247 | }); 248 | }); 249 | 250 | describe('#close', function () { 251 | it('should consume all data before closing', function (done) { 252 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 253 | 254 | let count = 0; 255 | const sender = new Sender({ 256 | write: (data, cb) => { 257 | count++; 258 | if (cb) cb(); 259 | } 260 | }, { 261 | 'permessage-deflate': perMessageDeflate 262 | }); 263 | 264 | perMessageDeflate.accept([{}]); 265 | 266 | sender.send('foo', { compress: true, fin: true }); 267 | sender.send('bar', { compress: true, fin: true }); 268 | sender.send('baz', { compress: true, fin: true }); 269 | 270 | sender.close(1000, null, false, () => { 271 | assert.strictEqual(count, 4); 272 | done(); 273 | }); 274 | }); 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /lib/WebSocketServer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const safeBuffer = require('safe-buffer'); 10 | const EventEmitter = require('events'); 11 | const crypto = require('crypto'); 12 | const Ultron = require('ultron'); 13 | const http = require('http'); 14 | const url = require('url'); 15 | 16 | const PerMessageDeflate = require('./PerMessageDeflate'); 17 | const Extensions = require('./Extensions'); 18 | const constants = require('./Constants'); 19 | const WebSocket = require('./WebSocket'); 20 | 21 | const Buffer = safeBuffer.Buffer; 22 | 23 | /** 24 | * Class representing a WebSocket server. 25 | * 26 | * @extends EventEmitter 27 | */ 28 | class WebSocketServer extends EventEmitter { 29 | /** 30 | * Create a `WebSocketServer` instance. 31 | * 32 | * @param {Object} options Configuration options 33 | * @param {String} options.host The hostname where to bind the server 34 | * @param {Number} options.port The port where to bind the server 35 | * @param {http.Server} options.server A pre-created HTTP/S server to use 36 | * @param {Function} options.verifyClient An hook to reject connections 37 | * @param {Function} options.handleProtocols An hook to handle protocols 38 | * @param {String} options.path Accept only connections matching this path 39 | * @param {Boolean} options.noServer Enable no server mode 40 | * @param {Boolean} options.clientTracking Specifies whether or not to track clients 41 | * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate 42 | * @param {Number} options.maxPayload The maximum allowed message size 43 | * @param {Function} callback A listener for the `listening` event 44 | */ 45 | constructor (options, callback) { 46 | super(); 47 | 48 | options = Object.assign({ 49 | maxPayload: 100 * 1024 * 1024, 50 | perMessageDeflate: true, 51 | handleProtocols: null, 52 | clientTracking: true, 53 | verifyClient: null, 54 | noServer: false, 55 | backlog: null, // use default (511 as implemented in net.js) 56 | server: null, 57 | host: null, 58 | path: null, 59 | port: null 60 | }, options); 61 | 62 | if (options.port == null && !options.server && !options.noServer) { 63 | throw new TypeError('missing or invalid options'); 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.allowHalfOpen = false; 77 | this._server.listen(options.port, options.host, options.backlog, callback); 78 | } else if (options.server) { 79 | this._server = options.server; 80 | } 81 | 82 | if (this._server) { 83 | this._ultron = new Ultron(this._server); 84 | this._ultron.on('listening', () => this.emit('listening')); 85 | this._ultron.on('error', (err) => this.emit('error', err)); 86 | this._ultron.on('upgrade', (req, socket, head) => { 87 | this.handleUpgrade(req, socket, head, (client) => { 88 | this.emit(`connection${req.url}`, client); 89 | this.emit('connection', client); 90 | }); 91 | }); 92 | } 93 | 94 | if (options.clientTracking) this.clients = new Set(); 95 | this.options = options; 96 | this.path = options.path; 97 | } 98 | 99 | /** 100 | * Close the server. 101 | * 102 | * @param {Function} cb Callback 103 | * @public 104 | */ 105 | close (cb) { 106 | // 107 | // Terminate all associated clients. 108 | // 109 | if (this.clients) { 110 | for (const client of this.clients) client.terminate(); 111 | } 112 | 113 | const server = this._server; 114 | 115 | if (server) { 116 | this._ultron.destroy(); 117 | this._ultron = this._server = null; 118 | 119 | // 120 | // Close the http server if it was internally created. 121 | // 122 | if (this.options.port != null) return server.close(cb); 123 | } 124 | 125 | if (cb) cb(); 126 | } 127 | 128 | /** 129 | * See if a given request should be handled by this server instance. 130 | * 131 | * @param {http.IncomingMessage} req Request object to inspect 132 | * @return {Boolean} `true` if the request is valid, else `false` 133 | * @public 134 | */ 135 | shouldHandle (req) { 136 | if (this.options.path && url.parse(req.url).pathname !== this.options.path) { 137 | return false; 138 | } 139 | 140 | return true; 141 | } 142 | 143 | /** 144 | * Handle a HTTP Upgrade request. 145 | * 146 | * @param {http.IncomingMessage} req The request object 147 | * @param {net.Socket} socket The network socket between the server and client 148 | * @param {Buffer} head The first packet of the upgraded stream 149 | * @param {Function} cb Callback 150 | * @public 151 | */ 152 | handleUpgrade (req, socket, head, cb) { 153 | socket.on('error', socketError); 154 | 155 | const version = +req.headers['sec-websocket-version']; 156 | 157 | if ( 158 | req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket' || 159 | !req.headers['sec-websocket-key'] || (version !== 8 && version !== 13) || 160 | !this.shouldHandle(req) 161 | ) { 162 | return abortConnection(socket, 400); 163 | } 164 | 165 | var protocol = (req.headers['sec-websocket-protocol'] || '').split(/, */); 166 | 167 | // 168 | // Optionally call external protocol selection handler. 169 | // 170 | if (this.options.handleProtocols) { 171 | protocol = this.options.handleProtocols(protocol, req); 172 | if (protocol === false) return abortConnection(socket, 401); 173 | } else { 174 | protocol = protocol[0]; 175 | } 176 | 177 | // 178 | // Optionally call external client verification handler. 179 | // 180 | if (this.options.verifyClient) { 181 | const info = { 182 | origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], 183 | secure: !!(req.connection.authorized || req.connection.encrypted), 184 | req 185 | }; 186 | 187 | if (this.options.verifyClient.length === 2) { 188 | this.options.verifyClient(info, (verified, code, message) => { 189 | if (!verified) return abortConnection(socket, code || 401, message); 190 | 191 | this.completeUpgrade(protocol, version, req, socket, head, cb); 192 | }); 193 | return; 194 | } else if (!this.options.verifyClient(info)) { 195 | return abortConnection(socket, 401); 196 | } 197 | } 198 | 199 | this.completeUpgrade(protocol, version, req, socket, head, cb); 200 | } 201 | 202 | /** 203 | * Upgrade the connection to WebSocket. 204 | * 205 | * @param {String} protocol The chosen subprotocol 206 | * @param {Number} version The WebSocket protocol version 207 | * @param {http.IncomingMessage} req The request object 208 | * @param {net.Socket} socket The network socket between the server and client 209 | * @param {Buffer} head The first packet of the upgraded stream 210 | * @param {Function} cb Callback 211 | * @private 212 | */ 213 | completeUpgrade (protocol, version, req, socket, head, cb) { 214 | // 215 | // Destroy the socket if the client has already sent a FIN packet. 216 | // 217 | if (!socket.readable || !socket.writable) return socket.destroy(); 218 | 219 | const key = crypto.createHash('sha1') 220 | .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary') 221 | .digest('base64'); 222 | 223 | const headers = [ 224 | 'HTTP/1.1 101 Switching Protocols', 225 | 'Upgrade: websocket', 226 | 'Connection: Upgrade', 227 | `Sec-WebSocket-Accept: ${key}` 228 | ]; 229 | 230 | if (protocol) headers.push(`Sec-WebSocket-Protocol: ${protocol}`); 231 | 232 | const offer = Extensions.parse(req.headers['sec-websocket-extensions']); 233 | var extensions; 234 | 235 | try { 236 | extensions = acceptExtensions(this.options, offer); 237 | } catch (err) { 238 | return abortConnection(socket, 400); 239 | } 240 | 241 | const props = Object.keys(extensions); 242 | 243 | if (props.length) { 244 | const serverExtensions = props.reduce((obj, key) => { 245 | obj[key] = [extensions[key].params]; 246 | return obj; 247 | }, {}); 248 | 249 | headers.push(`Sec-WebSocket-Extensions: ${Extensions.format(serverExtensions)}`); 250 | } 251 | 252 | // 253 | // Allow external modification/inspection of handshake headers. 254 | // 255 | this.emit('headers', headers, req); 256 | 257 | socket.write(headers.concat('', '').join('\r\n')); 258 | 259 | const client = new WebSocket([req, socket, head], null, { 260 | maxPayload: this.options.maxPayload, 261 | protocolVersion: version, 262 | extensions, 263 | protocol 264 | }); 265 | 266 | if (this.clients) { 267 | this.clients.add(client); 268 | client.on('close', () => this.clients.delete(client)); 269 | } 270 | 271 | socket.removeListener('error', socketError); 272 | cb(client); 273 | } 274 | } 275 | 276 | module.exports = WebSocketServer; 277 | 278 | /** 279 | * Handle premature socket errors. 280 | * 281 | * @private 282 | */ 283 | function socketError () { 284 | this.destroy(); 285 | } 286 | 287 | /** 288 | * Accept WebSocket extensions. 289 | * 290 | * @param {Object} options The `WebSocketServer` configuration options 291 | * @param {Object} offer The parsed value of the `sec-websocket-extensions` header 292 | * @return {Object} Accepted extensions 293 | * @private 294 | */ 295 | function acceptExtensions (options, offer) { 296 | const pmd = options.perMessageDeflate; 297 | const extensions = {}; 298 | 299 | if (pmd && offer[PerMessageDeflate.extensionName]) { 300 | const perMessageDeflate = new PerMessageDeflate( 301 | pmd !== true ? pmd : {}, 302 | true, 303 | options.maxPayload 304 | ); 305 | 306 | perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]); 307 | extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 308 | } 309 | 310 | return extensions; 311 | } 312 | 313 | /** 314 | * Close the connection when preconditions are not fulfilled. 315 | * 316 | * @param {net.Socket} socket The socket of the upgrade request 317 | * @param {Number} code The HTTP response status code 318 | * @param {String} [message] The HTTP response body 319 | * @private 320 | */ 321 | function abortConnection (socket, code, message) { 322 | if (socket.writable) { 323 | message = message || http.STATUS_CODES[code]; 324 | socket.write( 325 | `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + 326 | 'Connection: close\r\n' + 327 | 'Content-type: text/html\r\n' + 328 | `Content-Length: ${Buffer.byteLength(message)}\r\n` + 329 | '\r\n' + 330 | message 331 | ); 332 | } 333 | 334 | socket.removeListener('error', socketError); 335 | socket.destroy(); 336 | } 337 | -------------------------------------------------------------------------------- /lib/Sender.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const safeBuffer = require('safe-buffer'); 10 | const crypto = require('crypto'); 11 | 12 | const PerMessageDeflate = require('./PerMessageDeflate'); 13 | const bufferUtil = require('./BufferUtil'); 14 | const ErrorCodes = require('./ErrorCodes'); 15 | 16 | const Buffer = safeBuffer.Buffer; 17 | 18 | /** 19 | * HyBi Sender implementation. 20 | */ 21 | class Sender { 22 | /** 23 | * Creates a Sender instance. 24 | * 25 | * @param {net.Socket} socket The connection socket 26 | * @param {Object} extensions An object containing the negotiated extensions 27 | */ 28 | constructor (socket, extensions) { 29 | this.perMessageDeflate = (extensions || {})[PerMessageDeflate.extensionName]; 30 | this._socket = socket; 31 | 32 | this.firstFragment = true; 33 | this.compress = false; 34 | 35 | this.bufferedBytes = 0; 36 | this.deflating = false; 37 | this.queue = []; 38 | 39 | this.onerror = null; 40 | } 41 | 42 | /** 43 | * Frames a piece of data according to the HyBi WebSocket protocol. 44 | * 45 | * @param {Buffer} data The data to frame 46 | * @param {Object} options Options object 47 | * @param {Number} options.opcode The opcode 48 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 49 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 50 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 51 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 52 | * @return {Buffer[]} The framed data as a list of `Buffer` instances 53 | * @public 54 | */ 55 | static frame (data, options) { 56 | const merge = data.length < 1024 || (options.mask && options.readOnly); 57 | var offset = options.mask ? 6 : 2; 58 | var payloadLength = data.length; 59 | 60 | if (data.length >= 65536) { 61 | offset += 8; 62 | payloadLength = 127; 63 | } else if (data.length > 125) { 64 | offset += 2; 65 | payloadLength = 126; 66 | } 67 | 68 | const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); 69 | 70 | target[0] = options.fin ? options.opcode | 0x80 : options.opcode; 71 | if (options.rsv1) target[0] |= 0x40; 72 | 73 | if (payloadLength === 126) { 74 | target.writeUInt16BE(data.length, 2, true); 75 | } else if (payloadLength === 127) { 76 | target.writeUInt32BE(0, 2, true); 77 | target.writeUInt32BE(data.length, 6, true); 78 | } 79 | 80 | if (!options.mask) { 81 | target[1] = payloadLength; 82 | if (merge) { 83 | data.copy(target, offset); 84 | return [target]; 85 | } 86 | 87 | return [target, data]; 88 | } 89 | 90 | const mask = crypto.randomBytes(4); 91 | 92 | target[1] = payloadLength | 0x80; 93 | target[offset - 4] = mask[0]; 94 | target[offset - 3] = mask[1]; 95 | target[offset - 2] = mask[2]; 96 | target[offset - 1] = mask[3]; 97 | 98 | if (merge) { 99 | bufferUtil.mask(data, mask, target, offset, data.length); 100 | return [target]; 101 | } 102 | 103 | bufferUtil.mask(data, mask, data, 0, data.length); 104 | return [target, data]; 105 | } 106 | 107 | /** 108 | * Sends a close message to the other peer. 109 | * 110 | * @param {(Number|undefined)} code The status code component of the body 111 | * @param {String} data The message component of the body 112 | * @param {Boolean} mask Specifies whether or not to mask the message 113 | * @param {Function} cb Callback 114 | * @public 115 | */ 116 | close (code, data, mask, cb) { 117 | if (code !== undefined && (typeof code !== 'number' || !ErrorCodes.isValidErrorCode(code))) { 118 | throw new Error('first argument must be a valid error code number'); 119 | } 120 | 121 | const buf = Buffer.allocUnsafe(2 + (data ? Buffer.byteLength(data) : 0)); 122 | 123 | buf.writeUInt16BE(code || 1000, 0, true); 124 | if (buf.length > 2) buf.write(data, 2); 125 | 126 | if (this.deflating) { 127 | this.enqueue([this.doClose, buf, mask, cb]); 128 | } else { 129 | this.doClose(buf, mask, cb); 130 | } 131 | } 132 | 133 | /** 134 | * Frames and sends a close message. 135 | * 136 | * @param {Buffer} data The message to send 137 | * @param {Boolean} mask Specifies whether or not to mask `data` 138 | * @param {Function} cb Callback 139 | * @private 140 | */ 141 | doClose (data, mask, cb) { 142 | this.sendFrame(Sender.frame(data, { 143 | readOnly: false, 144 | opcode: 0x08, 145 | rsv1: false, 146 | fin: true, 147 | mask 148 | }), cb); 149 | } 150 | 151 | /** 152 | * Sends a ping message to the other peer. 153 | * 154 | * @param {*} data The message to send 155 | * @param {Boolean} mask Specifies whether or not to mask `data` 156 | * @public 157 | */ 158 | ping (data, mask) { 159 | var readOnly = true; 160 | 161 | if (!Buffer.isBuffer(data)) { 162 | if (data instanceof ArrayBuffer) { 163 | data = Buffer.from(data); 164 | } else if (ArrayBuffer.isView(data)) { 165 | data = viewToBuffer(data); 166 | } else { 167 | data = Buffer.from(data); 168 | readOnly = false; 169 | } 170 | } 171 | 172 | if (this.deflating) { 173 | this.enqueue([this.doPing, data, mask, readOnly]); 174 | } else { 175 | this.doPing(data, mask, readOnly); 176 | } 177 | } 178 | 179 | /** 180 | * Frames and sends a ping message. 181 | * 182 | * @param {*} data The message to send 183 | * @param {Boolean} mask Specifies whether or not to mask `data` 184 | * @param {Boolean} readOnly Specifies whether `data` can be modified 185 | * @private 186 | */ 187 | doPing (data, mask, readOnly) { 188 | this.sendFrame(Sender.frame(data, { 189 | opcode: 0x09, 190 | rsv1: false, 191 | fin: true, 192 | readOnly, 193 | mask 194 | })); 195 | } 196 | 197 | /** 198 | * Sends a pong message to the other peer. 199 | * 200 | * @param {*} data The message to send 201 | * @param {Boolean} mask Specifies whether or not to mask `data` 202 | * @public 203 | */ 204 | pong (data, mask) { 205 | var readOnly = true; 206 | 207 | if (!Buffer.isBuffer(data)) { 208 | if (data instanceof ArrayBuffer) { 209 | data = Buffer.from(data); 210 | } else if (ArrayBuffer.isView(data)) { 211 | data = viewToBuffer(data); 212 | } else { 213 | data = Buffer.from(data); 214 | readOnly = false; 215 | } 216 | } 217 | 218 | if (this.deflating) { 219 | this.enqueue([this.doPong, data, mask, readOnly]); 220 | } else { 221 | this.doPong(data, mask, readOnly); 222 | } 223 | } 224 | 225 | /** 226 | * Frames and sends a pong message. 227 | * 228 | * @param {*} data The message to send 229 | * @param {Boolean} mask Specifies whether or not to mask `data` 230 | * @param {Boolean} readOnly Specifies whether `data` can be modified 231 | * @private 232 | */ 233 | doPong (data, mask, readOnly) { 234 | this.sendFrame(Sender.frame(data, { 235 | opcode: 0x0a, 236 | rsv1: false, 237 | fin: true, 238 | readOnly, 239 | mask 240 | })); 241 | } 242 | 243 | /** 244 | * Sends a data message to the other peer. 245 | * 246 | * @param {*} data The message to send 247 | * @param {Object} options Options object 248 | * @param {Boolean} options.compress Specifies whether or not to compress `data` 249 | * @param {Boolean} options.binary Specifies whether `data` is binary or text 250 | * @param {Boolean} options.fin Specifies whether the fragment is the last one 251 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 252 | * @param {Function} cb Callback 253 | * @public 254 | */ 255 | send (data, options, cb) { 256 | var opcode = options.binary ? 2 : 1; 257 | var rsv1 = options.compress; 258 | var readOnly = true; 259 | 260 | if (!Buffer.isBuffer(data)) { 261 | if (data instanceof ArrayBuffer) { 262 | data = Buffer.from(data); 263 | } else if (ArrayBuffer.isView(data)) { 264 | data = viewToBuffer(data); 265 | } else { 266 | data = Buffer.from(data); 267 | readOnly = false; 268 | } 269 | } 270 | 271 | if (this.firstFragment) { 272 | this.firstFragment = false; 273 | if (rsv1 && this.perMessageDeflate) { 274 | rsv1 = data.length >= this.perMessageDeflate.threshold; 275 | } 276 | this.compress = rsv1; 277 | } else { 278 | rsv1 = false; 279 | opcode = 0; 280 | } 281 | 282 | if (options.fin) this.firstFragment = true; 283 | 284 | if (this.perMessageDeflate) { 285 | const opts = { 286 | compress: this.compress, 287 | mask: options.mask, 288 | fin: options.fin, 289 | readOnly, 290 | opcode, 291 | rsv1 292 | }; 293 | 294 | if (this.deflating) { 295 | this.enqueue([this.dispatch, data, opts, cb]); 296 | } else { 297 | this.dispatch(data, opts, cb); 298 | } 299 | } else { 300 | this.sendFrame(Sender.frame(data, { 301 | mask: options.mask, 302 | fin: options.fin, 303 | rsv1: false, 304 | readOnly, 305 | opcode 306 | }), cb); 307 | } 308 | } 309 | 310 | /** 311 | * Dispatches a data message. 312 | * 313 | * @param {Buffer} data The message to send 314 | * @param {Object} options Options object 315 | * @param {Number} options.opcode The opcode 316 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 317 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 318 | * @param {Boolean} options.compress Specifies whether or not to compress `data` 319 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 320 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 321 | * @param {Function} cb Callback 322 | * @private 323 | */ 324 | dispatch (data, options, cb) { 325 | if (!options.compress) { 326 | this.sendFrame(Sender.frame(data, options), cb); 327 | return; 328 | } 329 | 330 | this.deflating = true; 331 | this.perMessageDeflate.compress(data, options.fin, (err, buf) => { 332 | if (err) { 333 | if (cb) cb(err); 334 | else this.onerror(err); 335 | return; 336 | } 337 | 338 | options.readOnly = false; 339 | this.sendFrame(Sender.frame(buf, options), cb); 340 | this.deflating = false; 341 | this.dequeue(); 342 | }); 343 | } 344 | 345 | /** 346 | * Executes queued send operations. 347 | * 348 | * @private 349 | */ 350 | dequeue () { 351 | while (!this.deflating && this.queue.length) { 352 | const params = this.queue.shift(); 353 | 354 | this.bufferedBytes -= params[1].length; 355 | params[0].apply(this, params.slice(1)); 356 | } 357 | } 358 | 359 | /** 360 | * Enqueues a send operation. 361 | * 362 | * @param {Array} params Send operation parameters. 363 | * @private 364 | */ 365 | enqueue (params) { 366 | this.bufferedBytes += params[1].length; 367 | this.queue.push(params); 368 | } 369 | 370 | /** 371 | * Sends a frame. 372 | * 373 | * @param {Buffer[]} list The frame to send 374 | * @param {Function} cb Callback 375 | * @private 376 | */ 377 | sendFrame (list, cb) { 378 | if (list.length === 2) { 379 | this._socket.write(list[0]); 380 | this._socket.write(list[1], cb); 381 | } else { 382 | this._socket.write(list[0], cb); 383 | } 384 | } 385 | } 386 | 387 | module.exports = Sender; 388 | 389 | /** 390 | * Converts an `ArrayBuffer` view into a buffer. 391 | * 392 | * @param {(DataView|TypedArray)} view The view to convert 393 | * @return {Buffer} Converted view 394 | * @private 395 | */ 396 | function viewToBuffer (view) { 397 | const buf = Buffer.from(view.buffer); 398 | 399 | if (view.byteLength !== view.buffer.byteLength) { 400 | return buf.slice(view.byteOffset, view.byteOffset + view.byteLength); 401 | } 402 | 403 | return buf; 404 | } 405 | -------------------------------------------------------------------------------- /lib/PerMessageDeflate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeBuffer = require('safe-buffer'); 4 | const zlib = require('zlib'); 5 | 6 | const bufferUtil = require('./BufferUtil'); 7 | 8 | const Buffer = safeBuffer.Buffer; 9 | 10 | const AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15]; 11 | const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); 12 | const EMPTY_BLOCK = Buffer.from([0x00]); 13 | const DEFAULT_WINDOW_BITS = 15; 14 | const DEFAULT_MEM_LEVEL = 8; 15 | 16 | /** 17 | * Per-message Deflate implementation. 18 | */ 19 | class PerMessageDeflate { 20 | constructor (options, isServer, maxPayload) { 21 | this._options = options || {}; 22 | this._isServer = !!isServer; 23 | this._inflate = null; 24 | this._deflate = null; 25 | this.params = null; 26 | this._maxPayload = maxPayload || 0; 27 | this.threshold = this._options.threshold === undefined ? 1024 : this._options.threshold; 28 | } 29 | 30 | static get extensionName () { 31 | return 'permessage-deflate'; 32 | } 33 | 34 | /** 35 | * Create extension parameters offer. 36 | * 37 | * @return {Object} Extension parameters 38 | * @public 39 | */ 40 | offer () { 41 | const params = {}; 42 | 43 | if (this._options.serverNoContextTakeover) { 44 | params.server_no_context_takeover = true; 45 | } 46 | if (this._options.clientNoContextTakeover) { 47 | params.client_no_context_takeover = true; 48 | } 49 | if (this._options.serverMaxWindowBits) { 50 | params.server_max_window_bits = this._options.serverMaxWindowBits; 51 | } 52 | if (this._options.clientMaxWindowBits) { 53 | params.client_max_window_bits = this._options.clientMaxWindowBits; 54 | } else if (this._options.clientMaxWindowBits == null) { 55 | params.client_max_window_bits = true; 56 | } 57 | 58 | return params; 59 | } 60 | 61 | /** 62 | * Accept extension offer. 63 | * 64 | * @param {Array} paramsList Extension parameters 65 | * @return {Object} Accepted configuration 66 | * @public 67 | */ 68 | accept (paramsList) { 69 | paramsList = this.normalizeParams(paramsList); 70 | 71 | var params; 72 | if (this._isServer) { 73 | params = this.acceptAsServer(paramsList); 74 | } else { 75 | params = this.acceptAsClient(paramsList); 76 | } 77 | 78 | this.params = params; 79 | return params; 80 | } 81 | 82 | /** 83 | * Releases all resources used by the extension. 84 | * 85 | * @public 86 | */ 87 | cleanup () { 88 | if (this._inflate) { 89 | if (this._inflate.writeInProgress) { 90 | this._inflate.pendingClose = true; 91 | } else { 92 | this._inflate.close(); 93 | this._inflate = null; 94 | } 95 | } 96 | if (this._deflate) { 97 | if (this._deflate.writeInProgress) { 98 | this._deflate.pendingClose = true; 99 | } else { 100 | this._deflate.close(); 101 | this._deflate = null; 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Accept extension offer from client. 108 | * 109 | * @param {Array} paramsList Extension parameters 110 | * @return {Object} Accepted configuration 111 | * @private 112 | */ 113 | acceptAsServer (paramsList) { 114 | const accepted = {}; 115 | const result = paramsList.some((params) => { 116 | if (( 117 | this._options.serverNoContextTakeover === false && 118 | params.server_no_context_takeover 119 | ) || ( 120 | this._options.serverMaxWindowBits === false && 121 | params.server_max_window_bits 122 | ) || ( 123 | typeof this._options.serverMaxWindowBits === 'number' && 124 | typeof params.server_max_window_bits === 'number' && 125 | this._options.serverMaxWindowBits > params.server_max_window_bits 126 | ) || ( 127 | typeof this._options.clientMaxWindowBits === 'number' && 128 | !params.client_max_window_bits 129 | )) { 130 | return; 131 | } 132 | 133 | if ( 134 | this._options.serverNoContextTakeover || 135 | params.server_no_context_takeover 136 | ) { 137 | accepted.server_no_context_takeover = true; 138 | } 139 | if (this._options.clientNoContextTakeover) { 140 | accepted.client_no_context_takeover = true; 141 | } 142 | if ( 143 | this._options.clientNoContextTakeover !== false && 144 | params.client_no_context_takeover 145 | ) { 146 | accepted.client_no_context_takeover = true; 147 | } 148 | if (typeof this._options.serverMaxWindowBits === 'number') { 149 | accepted.server_max_window_bits = this._options.serverMaxWindowBits; 150 | } else if (typeof params.server_max_window_bits === 'number') { 151 | accepted.server_max_window_bits = params.server_max_window_bits; 152 | } 153 | if (typeof this._options.clientMaxWindowBits === 'number') { 154 | accepted.client_max_window_bits = this._options.clientMaxWindowBits; 155 | } else if ( 156 | this._options.clientMaxWindowBits !== false && 157 | typeof params.client_max_window_bits === 'number' 158 | ) { 159 | accepted.client_max_window_bits = params.client_max_window_bits; 160 | } 161 | return true; 162 | }); 163 | 164 | if (!result) throw new Error(`Doesn't support the offered configuration`); 165 | 166 | return accepted; 167 | } 168 | 169 | /** 170 | * Accept extension response from server. 171 | * 172 | * @param {Array} paramsList Extension parameters 173 | * @return {Object} Accepted configuration 174 | * @private 175 | */ 176 | acceptAsClient (paramsList) { 177 | const params = paramsList[0]; 178 | 179 | if (this._options.clientNoContextTakeover != null) { 180 | if ( 181 | this._options.clientNoContextTakeover === false && 182 | params.client_no_context_takeover 183 | ) { 184 | throw new Error('Invalid value for "client_no_context_takeover"'); 185 | } 186 | } 187 | if (this._options.clientMaxWindowBits != null) { 188 | if ( 189 | this._options.clientMaxWindowBits === false && 190 | params.client_max_window_bits 191 | ) { 192 | throw new Error('Invalid value for "client_max_window_bits"'); 193 | } 194 | if ( 195 | typeof this._options.clientMaxWindowBits === 'number' && ( 196 | !params.client_max_window_bits || 197 | params.client_max_window_bits > this._options.clientMaxWindowBits 198 | )) { 199 | throw new Error('Invalid value for "client_max_window_bits"'); 200 | } 201 | } 202 | 203 | return params; 204 | } 205 | 206 | /** 207 | * Normalize extensions parameters. 208 | * 209 | * @param {Array} paramsList Extension parameters 210 | * @return {Array} Normalized extensions parameters 211 | * @private 212 | */ 213 | normalizeParams (paramsList) { 214 | return paramsList.map((params) => { 215 | Object.keys(params).forEach((key) => { 216 | var value = params[key]; 217 | if (value.length > 1) { 218 | throw new Error(`Multiple extension parameters for ${key}`); 219 | } 220 | 221 | value = value[0]; 222 | 223 | switch (key) { 224 | case 'server_no_context_takeover': 225 | case 'client_no_context_takeover': 226 | if (value !== true) { 227 | throw new Error(`invalid extension parameter value for ${key} (${value})`); 228 | } 229 | params[key] = true; 230 | break; 231 | case 'server_max_window_bits': 232 | case 'client_max_window_bits': 233 | if (typeof value === 'string') { 234 | value = parseInt(value, 10); 235 | if (!~AVAILABLE_WINDOW_BITS.indexOf(value)) { 236 | throw new Error(`invalid extension parameter value for ${key} (${value})`); 237 | } 238 | } 239 | if (!this._isServer && value === true) { 240 | throw new Error(`Missing extension parameter value for ${key}`); 241 | } 242 | params[key] = value; 243 | break; 244 | default: 245 | throw new Error(`Not defined extension parameter (${key})`); 246 | } 247 | }); 248 | return params; 249 | }); 250 | } 251 | 252 | /** 253 | * Decompress data. 254 | * 255 | * @param {Buffer} data Compressed data 256 | * @param {Boolean} fin Specifies whether or not this is the last fragment 257 | * @param {Function} callback Callback 258 | * @public 259 | */ 260 | decompress (data, fin, callback) { 261 | const endpoint = this._isServer ? 'client' : 'server'; 262 | 263 | if (!this._inflate) { 264 | const maxWindowBits = this.params[`${endpoint}_max_window_bits`]; 265 | this._inflate = zlib.createInflateRaw({ 266 | windowBits: typeof maxWindowBits === 'number' ? maxWindowBits : DEFAULT_WINDOW_BITS 267 | }); 268 | } 269 | this._inflate.writeInProgress = true; 270 | 271 | var totalLength = 0; 272 | const buffers = []; 273 | var err; 274 | 275 | const onData = (data) => { 276 | totalLength += data.length; 277 | if (this._maxPayload < 1 || totalLength <= this._maxPayload) { 278 | return buffers.push(data); 279 | } 280 | 281 | err = new Error('max payload size exceeded'); 282 | err.closeCode = 1009; 283 | this._inflate.reset(); 284 | }; 285 | 286 | const onError = (err) => { 287 | cleanup(); 288 | callback(err); 289 | }; 290 | 291 | const cleanup = () => { 292 | if (!this._inflate) return; 293 | 294 | this._inflate.removeListener('error', onError); 295 | this._inflate.removeListener('data', onData); 296 | this._inflate.writeInProgress = false; 297 | 298 | if ( 299 | (fin && this.params[`${endpoint}_no_context_takeover`]) || 300 | this._inflate.pendingClose 301 | ) { 302 | this._inflate.close(); 303 | this._inflate = null; 304 | } 305 | }; 306 | 307 | this._inflate.on('error', onError).on('data', onData); 308 | this._inflate.write(data); 309 | if (fin) this._inflate.write(TRAILER); 310 | 311 | this._inflate.flush(() => { 312 | cleanup(); 313 | if (err) callback(err); 314 | else callback(null, bufferUtil.concat(buffers, totalLength)); 315 | }); 316 | } 317 | 318 | /** 319 | * Compress data. 320 | * 321 | * @param {Buffer} data Data to compress 322 | * @param {Boolean} fin Specifies whether or not this is the last fragment 323 | * @param {Function} callback Callback 324 | * @public 325 | */ 326 | compress (data, fin, callback) { 327 | if (!data || data.length === 0) { 328 | process.nextTick(callback, null, EMPTY_BLOCK); 329 | return; 330 | } 331 | 332 | const endpoint = this._isServer ? 'server' : 'client'; 333 | 334 | if (!this._deflate) { 335 | const maxWindowBits = this.params[`${endpoint}_max_window_bits`]; 336 | this._deflate = zlib.createDeflateRaw({ 337 | flush: zlib.Z_SYNC_FLUSH, 338 | windowBits: typeof maxWindowBits === 'number' ? maxWindowBits : DEFAULT_WINDOW_BITS, 339 | memLevel: this._options.memLevel || DEFAULT_MEM_LEVEL 340 | }); 341 | } 342 | this._deflate.writeInProgress = true; 343 | 344 | var totalLength = 0; 345 | const buffers = []; 346 | 347 | const onData = (data) => { 348 | totalLength += data.length; 349 | buffers.push(data); 350 | }; 351 | 352 | const onError = (err) => { 353 | cleanup(); 354 | callback(err); 355 | }; 356 | 357 | const cleanup = () => { 358 | if (!this._deflate) return; 359 | 360 | this._deflate.removeListener('error', onError); 361 | this._deflate.removeListener('data', onData); 362 | this._deflate.writeInProgress = false; 363 | 364 | if ( 365 | (fin && this.params[`${endpoint}_no_context_takeover`]) || 366 | this._deflate.pendingClose 367 | ) { 368 | this._deflate.close(); 369 | this._deflate = null; 370 | } 371 | }; 372 | 373 | this._deflate.on('error', onError).on('data', onData); 374 | this._deflate.write(data); 375 | this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { 376 | cleanup(); 377 | var data = bufferUtil.concat(buffers, totalLength); 378 | if (fin) data = data.slice(0, data.length - 4); 379 | callback(null, data); 380 | }); 381 | } 382 | } 383 | 384 | module.exports = PerMessageDeflate; 385 | -------------------------------------------------------------------------------- /lib/Receiver.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const safeBuffer = require('safe-buffer'); 10 | 11 | const PerMessageDeflate = require('./PerMessageDeflate'); 12 | const isValidUTF8 = require('./Validation'); 13 | const bufferUtil = require('./BufferUtil'); 14 | const ErrorCodes = require('./ErrorCodes'); 15 | const constants = require('./Constants'); 16 | 17 | const Buffer = safeBuffer.Buffer; 18 | 19 | const GET_INFO = 0; 20 | const GET_PAYLOAD_LENGTH_16 = 1; 21 | const GET_PAYLOAD_LENGTH_64 = 2; 22 | const GET_MASK = 3; 23 | const GET_DATA = 4; 24 | const INFLATING = 5; 25 | 26 | /** 27 | * HyBi Receiver implementation. 28 | */ 29 | class Receiver { 30 | /** 31 | * Creates a Receiver instance. 32 | * 33 | * @param {Object} extensions An object containing the negotiated extensions 34 | * @param {Number} maxPayload The maximum allowed message length 35 | * @param {String} binaryType The type for binary data 36 | */ 37 | constructor (extensions, maxPayload, binaryType) { 38 | this.binaryType = binaryType || constants.BINARY_TYPES[0]; 39 | this.extensions = extensions || {}; 40 | this.maxPayload = maxPayload | 0; 41 | 42 | this.bufferedBytes = 0; 43 | this.buffers = []; 44 | 45 | this.compressed = false; 46 | this.payloadLength = 0; 47 | this.fragmented = 0; 48 | this.masked = false; 49 | this.fin = false; 50 | this.mask = null; 51 | this.opcode = 0; 52 | 53 | this.totalPayloadLength = 0; 54 | this.messageLength = 0; 55 | this.fragments = []; 56 | 57 | this.cleanupCallback = null; 58 | this.hadError = false; 59 | this.dead = false; 60 | this.loop = false; 61 | 62 | this.onmessage = null; 63 | this.onclose = null; 64 | this.onerror = null; 65 | this.onping = null; 66 | this.onpong = null; 67 | 68 | this.state = GET_INFO; 69 | } 70 | 71 | /** 72 | * Consumes bytes from the available buffered data. 73 | * 74 | * @param {Number} bytes The number of bytes to consume 75 | * @return {Buffer} Consumed bytes 76 | * @private 77 | */ 78 | readBuffer (bytes) { 79 | var offset = 0; 80 | var dst; 81 | var l; 82 | 83 | this.bufferedBytes -= bytes; 84 | 85 | if (bytes === this.buffers[0].length) return this.buffers.shift(); 86 | 87 | if (bytes < this.buffers[0].length) { 88 | dst = this.buffers[0].slice(0, bytes); 89 | this.buffers[0] = this.buffers[0].slice(bytes); 90 | return dst; 91 | } 92 | 93 | dst = Buffer.allocUnsafe(bytes); 94 | 95 | while (bytes > 0) { 96 | l = this.buffers[0].length; 97 | 98 | if (bytes >= l) { 99 | this.buffers[0].copy(dst, offset); 100 | offset += l; 101 | this.buffers.shift(); 102 | } else { 103 | this.buffers[0].copy(dst, offset, 0, bytes); 104 | this.buffers[0] = this.buffers[0].slice(bytes); 105 | } 106 | 107 | bytes -= l; 108 | } 109 | 110 | return dst; 111 | } 112 | 113 | /** 114 | * Checks if the number of buffered bytes is bigger or equal than `n` and 115 | * calls `cleanup` if necessary. 116 | * 117 | * @param {Number} n The number of bytes to check against 118 | * @return {Boolean} `true` if `bufferedBytes >= n`, else `false` 119 | * @private 120 | */ 121 | hasBufferedBytes (n) { 122 | if (this.bufferedBytes >= n) return true; 123 | 124 | this.loop = false; 125 | if (this.dead) this.cleanup(this.cleanupCallback); 126 | return false; 127 | } 128 | 129 | /** 130 | * Adds new data to the parser. 131 | * 132 | * @public 133 | */ 134 | add (data) { 135 | if (this.dead) return; 136 | 137 | this.bufferedBytes += data.length; 138 | this.buffers.push(data); 139 | this.startLoop(); 140 | } 141 | 142 | /** 143 | * Starts the parsing loop. 144 | * 145 | * @private 146 | */ 147 | startLoop () { 148 | this.loop = true; 149 | 150 | while (this.loop) { 151 | switch (this.state) { 152 | case GET_INFO: 153 | this.getInfo(); 154 | break; 155 | case GET_PAYLOAD_LENGTH_16: 156 | this.getPayloadLength16(); 157 | break; 158 | case GET_PAYLOAD_LENGTH_64: 159 | this.getPayloadLength64(); 160 | break; 161 | case GET_MASK: 162 | this.getMask(); 163 | break; 164 | case GET_DATA: 165 | this.getData(); 166 | break; 167 | default: // `INFLATING` 168 | this.loop = false; 169 | } 170 | } 171 | } 172 | 173 | /** 174 | * Reads the first two bytes of a frame. 175 | * 176 | * @private 177 | */ 178 | getInfo () { 179 | if (!this.hasBufferedBytes(2)) return; 180 | 181 | const buf = this.readBuffer(2); 182 | 183 | if ((buf[0] & 0x30) !== 0x00) { 184 | this.error(new Error('RSV2 and RSV3 must be clear'), 1002); 185 | return; 186 | } 187 | 188 | const compressed = (buf[0] & 0x40) === 0x40; 189 | 190 | if (compressed && !this.extensions[PerMessageDeflate.extensionName]) { 191 | this.error(new Error('RSV1 must be clear'), 1002); 192 | return; 193 | } 194 | 195 | this.fin = (buf[0] & 0x80) === 0x80; 196 | this.opcode = buf[0] & 0x0f; 197 | this.payloadLength = buf[1] & 0x7f; 198 | 199 | if (this.opcode === 0x00) { 200 | if (compressed) { 201 | this.error(new Error('RSV1 must be clear'), 1002); 202 | return; 203 | } 204 | 205 | if (!this.fragmented) { 206 | this.error(new Error(`invalid opcode: ${this.opcode}`), 1002); 207 | return; 208 | } else { 209 | this.opcode = this.fragmented; 210 | } 211 | } else if (this.opcode === 0x01 || this.opcode === 0x02) { 212 | if (this.fragmented) { 213 | this.error(new Error(`invalid opcode: ${this.opcode}`), 1002); 214 | return; 215 | } 216 | 217 | this.compressed = compressed; 218 | } else if (this.opcode > 0x07 && this.opcode < 0x0b) { 219 | if (!this.fin) { 220 | this.error(new Error('FIN must be set'), 1002); 221 | return; 222 | } 223 | 224 | if (compressed) { 225 | this.error(new Error('RSV1 must be clear'), 1002); 226 | return; 227 | } 228 | 229 | if (this.payloadLength > 0x7d) { 230 | this.error(new Error('invalid payload length'), 1002); 231 | return; 232 | } 233 | } else { 234 | this.error(new Error(`invalid opcode: ${this.opcode}`), 1002); 235 | return; 236 | } 237 | 238 | if (!this.fin && !this.fragmented) this.fragmented = this.opcode; 239 | 240 | this.masked = (buf[1] & 0x80) === 0x80; 241 | 242 | if (this.payloadLength === 126) this.state = GET_PAYLOAD_LENGTH_16; 243 | else if (this.payloadLength === 127) this.state = GET_PAYLOAD_LENGTH_64; 244 | else this.haveLength(); 245 | } 246 | 247 | /** 248 | * Gets extended payload length (7+16). 249 | * 250 | * @private 251 | */ 252 | getPayloadLength16 () { 253 | if (!this.hasBufferedBytes(2)) return; 254 | 255 | this.payloadLength = this.readBuffer(2).readUInt16BE(0, true); 256 | this.haveLength(); 257 | } 258 | 259 | /** 260 | * Gets extended payload length (7+64). 261 | * 262 | * @private 263 | */ 264 | getPayloadLength64 () { 265 | if (!this.hasBufferedBytes(8)) return; 266 | 267 | const buf = this.readBuffer(8); 268 | const num = buf.readUInt32BE(0, true); 269 | 270 | // 271 | // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned 272 | // if payload length is greater than this number. 273 | // 274 | if (num > Math.pow(2, 53 - 32) - 1) { 275 | this.error(new Error('max payload size exceeded'), 1009); 276 | return; 277 | } 278 | 279 | this.payloadLength = (num * Math.pow(2, 32)) + buf.readUInt32BE(4, true); 280 | this.haveLength(); 281 | } 282 | 283 | /** 284 | * Payload length has been read. 285 | * 286 | * @private 287 | */ 288 | haveLength () { 289 | if (this.opcode < 0x08 && this.maxPayloadExceeded(this.payloadLength)) { 290 | return; 291 | } 292 | 293 | if (this.masked) this.state = GET_MASK; 294 | else this.state = GET_DATA; 295 | } 296 | 297 | /** 298 | * Reads mask bytes. 299 | * 300 | * @private 301 | */ 302 | getMask () { 303 | if (!this.hasBufferedBytes(4)) return; 304 | 305 | this.mask = this.readBuffer(4); 306 | this.state = GET_DATA; 307 | } 308 | 309 | /** 310 | * Reads data bytes. 311 | * 312 | * @private 313 | */ 314 | getData () { 315 | var data = constants.EMPTY_BUFFER; 316 | 317 | if (this.payloadLength) { 318 | if (!this.hasBufferedBytes(this.payloadLength)) return; 319 | 320 | data = this.readBuffer(this.payloadLength); 321 | if (this.masked) bufferUtil.unmask(data, this.mask); 322 | } 323 | 324 | if (this.opcode > 0x07) { 325 | this.controlMessage(data); 326 | } else if (this.compressed) { 327 | this.state = INFLATING; 328 | this.decompress(data); 329 | } else if (this.pushFragment(data)) { 330 | this.dataMessage(); 331 | } 332 | } 333 | 334 | /** 335 | * Decompresses data. 336 | * 337 | * @param {Buffer} data Compressed data 338 | * @private 339 | */ 340 | decompress (data) { 341 | const extension = this.extensions[PerMessageDeflate.extensionName]; 342 | 343 | extension.decompress(data, this.fin, (err, buf) => { 344 | if (err) { 345 | this.error(err, err.closeCode === 1009 ? 1009 : 1007); 346 | return; 347 | } 348 | 349 | if (this.pushFragment(buf)) this.dataMessage(); 350 | this.startLoop(); 351 | }); 352 | } 353 | 354 | /** 355 | * Handles a data message. 356 | * 357 | * @private 358 | */ 359 | dataMessage () { 360 | if (this.fin) { 361 | const messageLength = this.messageLength; 362 | const fragments = this.fragments; 363 | 364 | this.totalPayloadLength = 0; 365 | this.messageLength = 0; 366 | this.fragmented = 0; 367 | this.fragments = []; 368 | 369 | if (this.opcode === 2) { 370 | var data; 371 | 372 | if (this.binaryType === 'nodebuffer') { 373 | data = toBuffer(fragments, messageLength); 374 | } else if (this.binaryType === 'arraybuffer') { 375 | data = toArrayBuffer(toBuffer(fragments, messageLength)); 376 | } else { 377 | data = fragments; 378 | } 379 | 380 | this.onmessage(data, { masked: this.masked, binary: true }); 381 | } else { 382 | const buf = toBuffer(fragments, messageLength); 383 | 384 | if (!isValidUTF8(buf)) { 385 | this.error(new Error('invalid utf8 sequence'), 1007); 386 | return; 387 | } 388 | 389 | this.onmessage(buf.toString(), { masked: this.masked }); 390 | } 391 | } 392 | 393 | this.state = GET_INFO; 394 | } 395 | 396 | /** 397 | * Handles a control message. 398 | * 399 | * @param {Buffer} data Data to handle 400 | * @private 401 | */ 402 | controlMessage (data) { 403 | if (this.opcode === 0x08) { 404 | if (data.length === 0) { 405 | this.onclose(1000, '', { masked: this.masked }); 406 | this.loop = false; 407 | this.cleanup(this.cleanupCallback); 408 | } else if (data.length === 1) { 409 | this.error(new Error('invalid payload length'), 1002); 410 | } else { 411 | const code = data.readUInt16BE(0, true); 412 | 413 | if (!ErrorCodes.isValidErrorCode(code)) { 414 | this.error(new Error(`invalid status code: ${code}`), 1002); 415 | return; 416 | } 417 | 418 | const buf = data.slice(2); 419 | 420 | if (!isValidUTF8(buf)) { 421 | this.error(new Error('invalid utf8 sequence'), 1007); 422 | return; 423 | } 424 | 425 | this.onclose(code, buf.toString(), { masked: this.masked }); 426 | this.loop = false; 427 | this.cleanup(this.cleanupCallback); 428 | } 429 | 430 | return; 431 | } 432 | 433 | const flags = { masked: this.masked, binary: true }; 434 | 435 | if (this.opcode === 0x09) this.onping(data, flags); 436 | else this.onpong(data, flags); 437 | 438 | this.state = GET_INFO; 439 | } 440 | 441 | /** 442 | * Handles an error. 443 | * 444 | * @param {Error} err The error 445 | * @param {Number} code Close code 446 | * @private 447 | */ 448 | error (err, code) { 449 | this.onerror(err, code); 450 | this.hadError = true; 451 | this.loop = false; 452 | this.cleanup(this.cleanupCallback); 453 | } 454 | 455 | /** 456 | * Checks payload size, disconnects socket when it exceeds `maxPayload`. 457 | * 458 | * @param {Number} length Payload length 459 | * @private 460 | */ 461 | maxPayloadExceeded (length) { 462 | if (length === 0 || this.maxPayload < 1) return false; 463 | 464 | const fullLength = this.totalPayloadLength + length; 465 | 466 | if (fullLength <= this.maxPayload) { 467 | this.totalPayloadLength = fullLength; 468 | return false; 469 | } 470 | 471 | this.error(new Error('max payload size exceeded'), 1009); 472 | return true; 473 | } 474 | 475 | /** 476 | * Appends a fragment in the fragments array after checking that the sum of 477 | * fragment lengths does not exceed `maxPayload`. 478 | * 479 | * @param {Buffer} fragment The fragment to add 480 | * @return {Boolean} `true` if `maxPayload` is not exceeded, else `false` 481 | * @private 482 | */ 483 | pushFragment (fragment) { 484 | if (fragment.length === 0) return true; 485 | 486 | const totalLength = this.messageLength + fragment.length; 487 | 488 | if (this.maxPayload < 1 || totalLength <= this.maxPayload) { 489 | this.messageLength = totalLength; 490 | this.fragments.push(fragment); 491 | return true; 492 | } 493 | 494 | this.error(new Error('max payload size exceeded'), 1009); 495 | return false; 496 | } 497 | 498 | /** 499 | * Releases resources used by the receiver. 500 | * 501 | * @param {Function} cb Callback 502 | * @public 503 | */ 504 | cleanup (cb) { 505 | this.dead = true; 506 | 507 | if (!this.hadError && (this.loop || this.state === INFLATING)) { 508 | this.cleanupCallback = cb; 509 | } else { 510 | this.extensions = null; 511 | this.fragments = null; 512 | this.buffers = null; 513 | this.mask = null; 514 | 515 | this.cleanupCallback = null; 516 | this.onmessage = null; 517 | this.onclose = null; 518 | this.onerror = null; 519 | this.onping = null; 520 | this.onpong = null; 521 | 522 | if (cb) cb(); 523 | } 524 | } 525 | } 526 | 527 | module.exports = Receiver; 528 | 529 | /** 530 | * Makes a buffer from a list of fragments. 531 | * 532 | * @param {Buffer[]} fragments The list of fragments composing the message 533 | * @param {Number} messageLength The length of the message 534 | * @return {Buffer} 535 | * @private 536 | */ 537 | function toBuffer (fragments, messageLength) { 538 | if (fragments.length === 1) return fragments[0]; 539 | if (fragments.length > 1) return bufferUtil.concat(fragments, messageLength); 540 | return constants.EMPTY_BUFFER; 541 | } 542 | 543 | /** 544 | * Converts a buffer to an `ArrayBuffer`. 545 | * 546 | * @param {Buffer} The buffer to convert 547 | * @return {ArrayBuffer} Converted buffer 548 | */ 549 | function toArrayBuffer (buf) { 550 | if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) { 551 | return buf.buffer; 552 | } 553 | 554 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 555 | } 556 | -------------------------------------------------------------------------------- /test/PerMessageDeflate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeBuffer = require('safe-buffer'); 4 | const assert = require('assert'); 5 | 6 | const PerMessageDeflate = require('../lib/PerMessageDeflate'); 7 | const Extensions = require('../lib/Extensions'); 8 | 9 | const Buffer = safeBuffer.Buffer; 10 | 11 | describe('PerMessageDeflate', function () { 12 | describe('#offer', function () { 13 | it('should create default params', function () { 14 | const perMessageDeflate = new PerMessageDeflate(); 15 | 16 | assert.deepStrictEqual( 17 | perMessageDeflate.offer(), 18 | { client_max_window_bits: true } 19 | ); 20 | }); 21 | 22 | it('should create params from options', function () { 23 | const perMessageDeflate = new PerMessageDeflate({ 24 | serverNoContextTakeover: true, 25 | clientNoContextTakeover: true, 26 | serverMaxWindowBits: 10, 27 | clientMaxWindowBits: 11 28 | }); 29 | 30 | assert.deepStrictEqual(perMessageDeflate.offer(), { 31 | server_no_context_takeover: true, 32 | client_no_context_takeover: true, 33 | server_max_window_bits: 10, 34 | client_max_window_bits: 11 35 | }); 36 | }); 37 | }); 38 | 39 | describe('#accept', function () { 40 | describe('as server', function () { 41 | it('should accept empty offer', function () { 42 | const perMessageDeflate = new PerMessageDeflate({}, true); 43 | 44 | assert.deepStrictEqual(perMessageDeflate.accept([{}]), {}); 45 | }); 46 | 47 | it('should accept offer', function () { 48 | const perMessageDeflate = new PerMessageDeflate({}, true); 49 | const extensions = Extensions.parse( 50 | 'permessage-deflate; server_no_context_takeover; ' + 51 | 'client_no_context_takeover; server_max_window_bits=10; ' + 52 | 'client_max_window_bits=11' 53 | ); 54 | 55 | assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { 56 | server_no_context_takeover: true, 57 | client_no_context_takeover: true, 58 | server_max_window_bits: 10, 59 | client_max_window_bits: 11 60 | }); 61 | }); 62 | 63 | it('should prefer configuration than offer', function () { 64 | const perMessageDeflate = new PerMessageDeflate({ 65 | serverNoContextTakeover: true, 66 | clientNoContextTakeover: true, 67 | serverMaxWindowBits: 12, 68 | clientMaxWindowBits: 11 69 | }, true); 70 | const extensions = Extensions.parse( 71 | 'permessage-deflate; server_max_window_bits=14; client_max_window_bits=13' 72 | ); 73 | 74 | assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { 75 | server_no_context_takeover: true, 76 | client_no_context_takeover: true, 77 | server_max_window_bits: 12, 78 | client_max_window_bits: 11 79 | }); 80 | }); 81 | 82 | it('should fallback', function () { 83 | const perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); 84 | const extensions = Extensions.parse( 85 | 'permessage-deflate; server_max_window_bits=10, permessage-deflate' 86 | ); 87 | 88 | assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { 89 | server_max_window_bits: 11 90 | }); 91 | }); 92 | 93 | it('should throw an error if server_no_context_takeover is unsupported', function () { 94 | const perMessageDeflate = new PerMessageDeflate({ serverNoContextTakeover: false }, true); 95 | const extensions = Extensions.parse('permessage-deflate; server_no_context_takeover'); 96 | 97 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 98 | }); 99 | 100 | it('should throw an error if server_max_window_bits is unsupported', function () { 101 | const perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: false }, true); 102 | const extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10'); 103 | 104 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 105 | }); 106 | 107 | it('should throw an error if server_max_window_bits is less than configuration', function () { 108 | const perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); 109 | const extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10'); 110 | 111 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 112 | }); 113 | 114 | it('should throw an error if client_max_window_bits is unsupported on client', function () { 115 | const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }, true); 116 | const extensions = Extensions.parse('permessage-deflate'); 117 | 118 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 119 | }); 120 | }); 121 | 122 | describe('as client', function () { 123 | it('should accept empty response', function () { 124 | const perMessageDeflate = new PerMessageDeflate({}); 125 | 126 | assert.deepStrictEqual(perMessageDeflate.accept([{}]), {}); 127 | }); 128 | 129 | it('should accept response parameter', function () { 130 | const perMessageDeflate = new PerMessageDeflate({}); 131 | const extensions = Extensions.parse( 132 | 'permessage-deflate; server_no_context_takeover; ' + 133 | 'client_no_context_takeover; server_max_window_bits=10; ' + 134 | 'client_max_window_bits=11' 135 | ); 136 | 137 | assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { 138 | server_no_context_takeover: true, 139 | client_no_context_takeover: true, 140 | server_max_window_bits: 10, 141 | client_max_window_bits: 11 142 | }); 143 | }); 144 | 145 | it('should throw an error if client_no_context_takeover is unsupported', function () { 146 | const perMessageDeflate = new PerMessageDeflate({ clientNoContextTakeover: false }); 147 | const extensions = Extensions.parse('permessage-deflate; client_no_context_takeover'); 148 | 149 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 150 | }); 151 | 152 | it('should throw an error if client_max_window_bits is unsupported', function () { 153 | const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: false }); 154 | const extensions = Extensions.parse('permessage-deflate; client_max_window_bits=10'); 155 | 156 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 157 | }); 158 | 159 | it('should throw an error if client_max_window_bits is greater than configuration', function () { 160 | const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }); 161 | const extensions = Extensions.parse('permessage-deflate; client_max_window_bits=11'); 162 | 163 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 164 | }); 165 | }); 166 | 167 | describe('validate parameters', function () { 168 | it('should throw an error if a parameter has multiple values', function () { 169 | const perMessageDeflate = new PerMessageDeflate(); 170 | const extensions = Extensions.parse( 171 | 'permessage-deflate; server_no_context_takeover; server_no_context_takeover' 172 | ); 173 | 174 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 175 | }); 176 | 177 | it('should throw an error if a parameter is undefined', function () { 178 | const perMessageDeflate = new PerMessageDeflate(); 179 | const extensions = Extensions.parse('permessage-deflate; foo;'); 180 | 181 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 182 | }); 183 | 184 | it('should throw an error if server_no_context_takeover has a value', function () { 185 | const perMessageDeflate = new PerMessageDeflate(); 186 | const extensions = Extensions.parse('permessage-deflate; server_no_context_takeover=10'); 187 | 188 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 189 | }); 190 | 191 | it('should throw an error if client_no_context_takeover has a value', function () { 192 | const perMessageDeflate = new PerMessageDeflate(); 193 | const extensions = Extensions.parse('permessage-deflate; client_no_context_takeover=10'); 194 | 195 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 196 | }); 197 | 198 | it('should throw an error if server_max_window_bits has an invalid value', function () { 199 | const perMessageDeflate = new PerMessageDeflate(); 200 | const extensions = Extensions.parse('permessage-deflate; server_max_window_bits=7'); 201 | 202 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 203 | }); 204 | 205 | it('should throw an error if client_max_window_bits has an invalid value', function () { 206 | const perMessageDeflate = new PerMessageDeflate(); 207 | const extensions = Extensions.parse('permessage-deflate; client_max_window_bits=16'); 208 | 209 | assert.throws(() => perMessageDeflate.accept(extensions['permessage-deflate'])); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('#compress/#decompress', function () { 215 | it('should compress/decompress data', function (done) { 216 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 217 | 218 | perMessageDeflate.accept([{}]); 219 | perMessageDeflate.compress(Buffer.from([1, 2, 3]), true, (err, compressed) => { 220 | if (err) return done(err); 221 | 222 | perMessageDeflate.decompress(compressed, true, (err, data) => { 223 | if (err) return done(err); 224 | 225 | assert.ok(data.equals(Buffer.from([1, 2, 3]))); 226 | done(); 227 | }); 228 | }); 229 | }); 230 | 231 | it('should compress/decompress fragments', function (done) { 232 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 233 | const buf = Buffer.from([1, 2, 3, 4]); 234 | 235 | perMessageDeflate.accept([{}]); 236 | 237 | perMessageDeflate.compress(buf.slice(0, 2), false, (err, compressed1) => { 238 | if (err) return done(err); 239 | 240 | perMessageDeflate.compress(buf.slice(2), true, (err, compressed2) => { 241 | if (err) return done(err); 242 | 243 | perMessageDeflate.decompress(compressed1, false, (err, data1) => { 244 | if (err) return done(err); 245 | 246 | perMessageDeflate.decompress(compressed2, true, (err, data2) => { 247 | if (err) return done(err); 248 | 249 | assert.ok(Buffer.concat([data1, data2]).equals(Buffer.from([1, 2, 3, 4]))); 250 | done(); 251 | }); 252 | }); 253 | }); 254 | }); 255 | }); 256 | 257 | it('should compress/decompress data with parameters', function (done) { 258 | const perMessageDeflate = new PerMessageDeflate({ 259 | threshold: 0, 260 | memLevel: 5 261 | }); 262 | const extensions = Extensions.parse( 263 | 'permessage-deflate; server_no_context_takeover; ' + 264 | 'client_no_context_takeover; server_max_window_bits=10; ' + 265 | 'client_max_window_bits=11' 266 | ); 267 | 268 | perMessageDeflate.accept(extensions['permessage-deflate']); 269 | 270 | perMessageDeflate.compress(Buffer.from([1, 2, 3]), true, (err, compressed) => { 271 | if (err) return done(err); 272 | 273 | perMessageDeflate.decompress(compressed, true, (err, data) => { 274 | if (err) return done(err); 275 | 276 | assert.ok(data.equals(Buffer.from([1, 2, 3]))); 277 | done(); 278 | }); 279 | }); 280 | }); 281 | 282 | it('should compress/decompress data with no context takeover', function (done) { 283 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 284 | const extensions = Extensions.parse( 285 | 'permessage-deflate; server_no_context_takeover; client_no_context_takeover' 286 | ); 287 | const buf = Buffer.from('foofoo'); 288 | 289 | perMessageDeflate.accept(extensions['permessage-deflate']); 290 | 291 | perMessageDeflate.compress(buf, true, (err, compressed1) => { 292 | if (err) return done(err); 293 | 294 | perMessageDeflate.decompress(compressed1, true, (err, data) => { 295 | if (err) return done(err); 296 | 297 | perMessageDeflate.compress(data, true, (err, compressed2) => { 298 | if (err) return done(err); 299 | 300 | perMessageDeflate.decompress(compressed2, true, (err, data) => { 301 | if (err) return done(err); 302 | 303 | assert.strictEqual(compressed2.length, compressed1.length); 304 | assert.ok(data.equals(buf)); 305 | done(); 306 | }); 307 | }); 308 | }); 309 | }); 310 | }); 311 | 312 | it('should compress data between contexts when allowed', function (done) { 313 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 314 | const extensions = Extensions.parse('permessage-deflate'); 315 | const buf = Buffer.from('foofoo'); 316 | 317 | perMessageDeflate.accept(extensions['permessage-deflate']); 318 | 319 | perMessageDeflate.compress(buf, true, (err, compressed1) => { 320 | if (err) return done(err); 321 | 322 | perMessageDeflate.decompress(compressed1, true, (err, data) => { 323 | if (err) return done(err); 324 | 325 | perMessageDeflate.compress(data, true, (err, compressed2) => { 326 | if (err) return done(err); 327 | 328 | perMessageDeflate.decompress(compressed2, true, (err, data) => { 329 | if (err) return done(err); 330 | 331 | assert.ok(compressed2.length < compressed1.length); 332 | assert.ok(data.equals(buf)); 333 | done(); 334 | }); 335 | }); 336 | }); 337 | }); 338 | }); 339 | 340 | it('should call the callback when an error occurs (inflate)', function (done) { 341 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); 342 | const data = Buffer.from('something invalid'); 343 | 344 | perMessageDeflate.accept([{}]); 345 | perMessageDeflate.decompress(data, true, (err) => { 346 | assert.ok(err instanceof Error); 347 | assert.strictEqual(err.errno, -3); 348 | done(); 349 | }); 350 | }); 351 | 352 | it('should not call the callback twice when `maxPayload` is exceeded', function (done) { 353 | const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }, false, 25); 354 | const buf = Buffer.from('A'.repeat(50)); 355 | const errors = []; 356 | 357 | perMessageDeflate.accept([{}]); 358 | perMessageDeflate.compress(buf, true, (err, data) => { 359 | if (err) return done(err); 360 | 361 | perMessageDeflate.decompress(data, true, (err) => errors.push(err)); 362 | perMessageDeflate._inflate.flush(() => { 363 | assert.strictEqual(errors.length, 1); 364 | assert.ok(errors[0] instanceof Error); 365 | assert.strictEqual(errors[0].message, 'max payload size exceeded'); 366 | done(); 367 | }); 368 | }); 369 | }); 370 | }); 371 | }); 372 | -------------------------------------------------------------------------------- /doc/ws.md: -------------------------------------------------------------------------------- 1 | # ws 2 | 3 | ## Class: WebSocket.Server 4 | 5 | This class represents a WebSocket server. It extends the `EventEmitter`. 6 | 7 | ### new WebSocket.Server(options[, callback]) 8 | 9 | - `options` {Object} 10 | - `host` {String} The hostname where to bind the server. 11 | - `port` {Number} The port where to bind the server. 12 | - `backlog` {Number} The maximum length of the queue of pending connections. 13 | - `server` {http.Server|https.Server} A pre-created Node.js HTTP server. 14 | - `verifyClient` {Function} A function which can be used to validate incoming 15 | connections. See description below. 16 | - `handleProtocols` {Function} A function which can be used to handle the 17 | WebSocket subprotocols. See description below. 18 | - `path` {String} Accept only connections matching this path. 19 | - `noServer` {Boolean} Enable no server mode. 20 | - `clientTracking` {Boolean} Specifies whether or not to track clients. 21 | - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. 22 | - `maxPayload` {Number} The maximum allowed message size in bytes. 23 | - `callback` {Function} 24 | 25 | Create a new server instance. One of `port`, `server` or `noServer` must be 26 | provided or an error is thrown. 27 | 28 | 29 | If `verifyClient` is not set then the handshake is automatically accepted. If 30 | it is is provided with a single argument then that is: 31 | 32 | - `info` {Object} 33 | - `origin` {String} The value in the Origin header indicated by the client. 34 | - `req` {http.IncomingMessage} The client HTTP GET request. 35 | - `secure` {Boolean} `true` if `req.connection.authorized` or 36 | `req.connection.encrypted` is set. 37 | 38 | The return value (Boolean) of the function determines whether or not to accept 39 | the handshake. 40 | 41 | if `verifyClient` is provided with two arguments then those are: 42 | 43 | - `info` {Object} Same as above. 44 | - `cb` {Function} A callback that must be called by the user upon inspection 45 | of the `info` fields. Arguments in this callback are: 46 | - `result` {Boolean} Whether or not to accept the handshake. 47 | - `code` {Number} When `result` is `false` this field determines the HTTP 48 | error status code to be sent to the client. 49 | - `name` {String} When `result` is `false` this field determines the HTTP 50 | reason phrase. 51 | 52 | 53 | If `handleProtocols` is not set then the handshake is automatically accepted, 54 | otherwise the function takes two arguments: 55 | 56 | - `protocols` {Array} The list of WebSocket subprotocols indicated by the 57 | client in the `Sec-WebSocket-Protocol` header. 58 | - `request` {http.IncomingMessage} The client HTTP GET request. 59 | 60 | If returned value is `false` then the handshake is rejected with the HTTP 401 61 | status code, otherwise the returned value sets the value of the 62 | `Sec-WebSocket-Protocol` header in the HTTP 101 response. 63 | 64 | `perMessageDeflate` can be used to control the behavior of 65 | [permessage-deflate extension][permessage-deflate]. 66 | The extension is disabled when `false`. Defaults to `true`. If an object is 67 | provided then that is extension parameters: 68 | 69 | - `serverNoContextTakeover` {Boolean} Whether to use context take over or not. 70 | - `clientNoContextTakeover` {Boolean} The value to be requested to clients 71 | whether to use context take over or not. 72 | - `serverMaxWindowBits` {Number} The value of windowBits. 73 | - `clientMaxWindowBits` {Number} The value of max windowBits to be requested 74 | to clients. 75 | - `memLevel` {Number} The value of memLevel. 76 | - `threshold` {Number} Payloads smaller than this will not be compressed. 77 | Defaults to 1024 bytes. 78 | 79 | If a property is empty then either an offered configuration or a default value 80 | is used. 81 | When sending a fragmented message the length of the first fragment is compared 82 | to the threshold. This determines if compression is used for the entire message. 83 | 84 | 85 | `callback` will be added as a listener for the `listening` event when the 86 | HTTP server is created internally and that is when the `port` option is 87 | provided. 88 | 89 | ### Event: 'connection' 90 | 91 | - `socket` {WebSocket} 92 | 93 | Emitted when the handshake is complete. `socket` is an instance of `WebSocket`. 94 | 95 | ### Event: 'error' 96 | 97 | - `error` {Error} 98 | 99 | Emitted when an error occurs on the underlying server. 100 | 101 | ### Event: 'headers' 102 | 103 | - `headers` {Array} 104 | - `request` {http.IncomingMessage} 105 | 106 | Emitted before the response headers are written to the socket as part of the 107 | handshake. This allows you to inspect/modify the headers before they are sent. 108 | 109 | ### Event: 'listening' 110 | 111 | Emitted when the underlying server has been bound. 112 | 113 | ### server.clients 114 | 115 | - {Set} 116 | 117 | A set that stores all connected clients. Please note that this property is only 118 | added when the `clientTracking` is truthy. 119 | 120 | ### server.close([callback]) 121 | 122 | Close the server and terminate all clients, calls callback when done. 123 | 124 | ### server.handleUpgrade(request, socket, head, callback) 125 | 126 | - `request` {http.IncomingMessage} The client HTTP GET request. 127 | - `socket` {net.Socket} The network socket between the server and client. 128 | - `head` {Buffer} The first packet of the upgraded stream. 129 | - `callback` {Function}. 130 | 131 | Handle a HTTP upgrade request. When the HTTP server is created internally or 132 | when the HTTP server is passed via the `server` option, this method is called 133 | automatically. When operating in "noServer" mode, this method must be called 134 | manually. 135 | 136 | If the upgrade is successfull, the `callback` is called with a `WebSocket` 137 | object as parameter. 138 | 139 | ### server.shouldHandle(request) 140 | 141 | - `request` {http.IncomingMessage} The client HTTP GET request. 142 | 143 | See if a given request should be handled by this server. 144 | By default this method validates the pathname of the request, matching it 145 | against the `path` option if provided. 146 | The return value, `true` or `false`, determines whether or not to accept the 147 | handshake. 148 | 149 | This method can be overriden when a custom handling logic is required. 150 | 151 | ## Class: WebSocket 152 | 153 | This class represents a WebSocket. It extends the `EventEmitter`. 154 | 155 | ### Ready state constants 156 | 157 | |Constant | Value | Description | 158 | |-----------|-------|--------------------------------------------------| 159 | |CONNECTING | 0 | The connection is not yet open. | 160 | |OPEN | 1 | The connection is open and ready to communicate. | 161 | |CLOSING | 2 | The connection is in the process of closing. | 162 | |CLOSED | 3 | The connection is closed. | 163 | 164 | ### new WebSocket(address[, protocols][, options]) 165 | 166 | - `address` {String} The URL to which to connect. 167 | - `protocols` {String|Array} The list of subprotocols. 168 | - `options` {Object} 169 | - `protocol` {String} Value of the `Sec-WebSocket-Protocol` header. 170 | - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. 171 | - `localAddress` {String} Local interface to bind for network connections. 172 | - `protocolVersion` {Number} Value of the `Sec-WebSocket-Version` header. 173 | - `headers` {Object} An object with custom headers to send along with the 174 | request. 175 | - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header 176 | depending on the `protocolVersion`. 177 | - `agent` {http.Agent|https.Agent} Use the specified Agent, 178 | - `host` {String} Value of the `Host` header. 179 | - `family` {Number} IP address family to use during hostname lookup (4 or 6). 180 | - `checkServerIdentity` {Function} A function to validate the server hostname. 181 | - `rejectUnauthorized` {Boolean} Verify or not the server certificate. 182 | - `passphrase` {String} The passphrase for the private key or pfx. 183 | - `ciphers` {String} The ciphers to use or exclude 184 | - `cert` {String|Array|Buffer} The certificate key. 185 | - `key` {String|Array|Buffer} The private key. 186 | - `pfx` {String|Buffer} The private key, certificate, and CA certs. 187 | - `ca` {Array} Trusted certificates. 188 | 189 | `perMessageDeflate` parameters are the same of the server, the only difference 190 | is the direction of requests (e.g. `serverNoContextTakeover` is the value to be 191 | requested to the server). 192 | 193 | Create a new WebSocket instance. 194 | 195 | #### UNIX Domain Sockets 196 | 197 | `ws` supports making requests to UNIX domain sockets. To make one, use the 198 | following URL scheme: 199 | 200 | ``` 201 | ws+unix:///absolule/path/to/uds_socket:/pathname?search_params 202 | ``` 203 | 204 | Note that `:` is the separator between the socket path and the URL path. If 205 | the URL path is omitted 206 | 207 | ``` 208 | ws+unix:///absolule/path/to/uds_socket 209 | ``` 210 | 211 | it defaults to `/`. 212 | 213 | ### Event: 'close' 214 | 215 | - `code` {Number} 216 | - `reason` {String} 217 | 218 | Emitted when the connection is closed. `code` is a numeric value indicating the 219 | status code explaining why the connection has been closed. `reason` is a 220 | human-readable string explaining why the connection has been closed. 221 | 222 | ### Event: 'error' 223 | 224 | - `error` {Error} 225 | 226 | Emitted when an error occurs. Errors from the underlying `net.Socket` are 227 | forwarded here. 228 | 229 | ### Event: 'message' 230 | 231 | - `data` {String|Buffer} 232 | - `flags` {Object} 233 | - `binary` {Boolean} Specifies if `data` is binary. 234 | - `masked` {Boolean} Specifies if `data` was masked. 235 | 236 | Emitted when a message is received from the server. 237 | 238 | ### Event: 'open' 239 | 240 | Emitted when the connection is established. 241 | 242 | ### Event: 'ping' 243 | 244 | - `data` {Buffer} 245 | - `flags` {Object} 246 | - `binary` {Boolean} Specifies if `data` is binary. 247 | - `masked` {Boolean} Specifies if `data` was masked. 248 | 249 | Emitted when a ping is received from the server. 250 | 251 | ### Event: 'pong' 252 | 253 | - `data` {Buffer} 254 | - `flags` {Object} 255 | - `binary` {Boolean} Specifies if `data` is binary. 256 | - `masked` {Boolean} Specifies if `data` was masked. 257 | 258 | Emitted when a pong is received from the server. 259 | 260 | ### Event: 'unexpected-response' 261 | 262 | - `request` {http.ClientRequest} 263 | - `response` {http.IncomingMessage} 264 | 265 | Emitted when the server response is not the expected one, for example a 401 266 | response. This event gives the ability to read the response in order to extract 267 | useful information. If the server sends an invalid response and there isn't a 268 | listener for this event, an error is emitted. 269 | 270 | ### websocket.addEventListener(type, listener) 271 | 272 | - `type` {String} A string representing the event type to listen for. 273 | - `listener` {Function} The listener to add. 274 | 275 | Register an event listener emulating the `EventTarget` interface. 276 | 277 | ### websocket.binaryType 278 | 279 | - {String} 280 | 281 | A string indicating the type of binary data being transmitted by the connection. 282 | This should be one of "nodebuffer", "arraybuffer" or "fragments". Defaults to 283 | "nodebuffer". Type "fragments" will emit the array of fragments as received from 284 | the sender, without copyfull concatenation, which is useful for the performance 285 | of binary protocols transfering large messages with multiple fragments. 286 | 287 | ### websocket.bufferedAmount 288 | 289 | - {Number} 290 | 291 | The number of bytes of data that have been queued using calls to `send()` but 292 | not yet transmitted to the network. 293 | 294 | ### websocket.bytesReceived 295 | 296 | - {Number} 297 | 298 | Received bytes count. 299 | 300 | ### websocket.close([code][, reason]) 301 | 302 | - `code` {Number} A numeric value indicating the status code explaining why 303 | the connection is being closed. 304 | - `reason` {String} A human-readable string explaining why the connection is 305 | closing. 306 | 307 | Initiate a closing handshake. 308 | 309 | ### websocket.extensions 310 | 311 | - {Object} 312 | 313 | An object containing the negotiated extensions. 314 | 315 | ### websocket.onclose 316 | 317 | - {Function} 318 | 319 | An event listener to be called when connection is closed. The listener receives 320 | a `CloseEvent` named "close". 321 | 322 | ### websocket.onerror 323 | 324 | - {Function} 325 | 326 | An event listener to be called when an error occurs. The listener receives 327 | an `Error` instance. 328 | 329 | ### websocket.onmessage 330 | 331 | - {Function} 332 | 333 | An event listener to be called when a message is received from the server. The 334 | listener receives a `MessageEvent` named "message". 335 | 336 | ### websocket.onopen 337 | 338 | - {Function} 339 | 340 | An event listener to be called when the connection is established. The listener 341 | receives an `OpenEvent` named "open". 342 | 343 | ### websocket.pause() 344 | 345 | Pause the socket. 346 | 347 | ### websocket.ping([data[, mask[, failSilently]]]) 348 | 349 | - `data` {Any} The data to send in the ping frame. 350 | - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults 351 | to `true` when `websocket` is not a server client. 352 | - `failSilently` {Boolean} Specifies whether or not to throw an error if the 353 | connection is not open. 354 | 355 | Send a ping. 356 | 357 | ### websocket.pong([data[, mask[, failSilently]]]) 358 | 359 | - `data` {Any} The data to send in the ping frame. 360 | - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults 361 | to `true` when `websocket` is not a server client. 362 | - `failSilently` {Boolean} Specifies whether or not to throw an error if the 363 | connection is not open. 364 | 365 | Send a pong. 366 | 367 | ### websocket.protocol 368 | 369 | - {String} 370 | 371 | The subprotocol selected by the server. 372 | 373 | ### websocket.protocolVersion 374 | 375 | - {Number} 376 | 377 | The WebSocket protocol version used for this connection, 8 or 13. 378 | 379 | ### websocket.readyState 380 | 381 | - {Number} 382 | 383 | The current state of the connection. This is one of the ready state constants. 384 | 385 | ### websocket.removeEventListener(type, listener) 386 | 387 | - `type` {String} A string representing the event type to remove. 388 | - `listener` {Function} The listener to remove. 389 | 390 | Removes an event listener emulating the `EventTarget` interface. 391 | 392 | ### websocket.resume() 393 | 394 | Resume the socket 395 | 396 | ### websocket.send(data, [options][, callback]) 397 | 398 | - `data` {Any} The data to send. 399 | - `options` {Object} 400 | - `compress` {Boolean} Specifies whether `data` should be compressed or not. 401 | Defaults to `true` when permessage-deflate is enabled. 402 | - `binary` {Boolean} Specifies whether `data` should be sent as a binary or not. 403 | Default is autodetected. 404 | - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults 405 | to `true` when `websocket` is not a server client. 406 | - `fin` {Boolean} Specifies whether `data` is the last fragment of a message or 407 | not. Defaults to `true`. 408 | - `callback` {Function} An optional callback which is invoked when `data` is 409 | written out. 410 | 411 | Send `data` through the connection. 412 | 413 | ### websocket.terminate() 414 | 415 | Forcibly close the connection. 416 | 417 | ### websocket.upgradeReq 418 | 419 | - {http.IncomingMessage} 420 | 421 | The http GET request sent by the client. Useful for parsing authority headers, 422 | cookie headers, and other information. This is only available for server clients. 423 | 424 | ### websocket.url 425 | 426 | - {String} 427 | 428 | The URL of the WebSocket server. Server clients don't have this attribute. 429 | 430 | [permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 431 | -------------------------------------------------------------------------------- /lib/WebSocket.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ws: a node.js websocket client 3 | * Copyright(c) 2011 Einar Otto Stangvik 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const EventEmitter = require('events'); 10 | const crypto = require('crypto'); 11 | const Ultron = require('ultron'); 12 | const https = require('https'); 13 | const http = require('http'); 14 | const url = require('url'); 15 | 16 | const PerMessageDeflate = require('./PerMessageDeflate'); 17 | const EventTarget = require('./EventTarget'); 18 | const Extensions = require('./Extensions'); 19 | const constants = require('./Constants'); 20 | const Receiver = require('./Receiver'); 21 | const Sender = require('./Sender'); 22 | 23 | const protocolVersions = [8, 13]; 24 | const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly. 25 | 26 | /** 27 | * Class representing a WebSocket. 28 | * 29 | * @extends EventEmitter 30 | */ 31 | class WebSocket extends EventEmitter { 32 | /** 33 | * Create a new `WebSocket`. 34 | * 35 | * @param {String} address The URL to which to connect 36 | * @param {(String|String[])} protocols The subprotocols 37 | * @param {Object} options Connection options 38 | */ 39 | constructor (address, protocols, options) { 40 | super(); 41 | 42 | if (!protocols) { 43 | protocols = []; 44 | } else if (typeof protocols === 'string') { 45 | protocols = [protocols]; 46 | } else if (!Array.isArray(protocols)) { 47 | options = protocols; 48 | protocols = []; 49 | } 50 | 51 | this.readyState = WebSocket.CONNECTING; 52 | this.bytesReceived = 0; 53 | this.extensions = {}; 54 | this.protocol = ''; 55 | 56 | this._binaryType = constants.BINARY_TYPES[0]; 57 | this._finalize = this.finalize.bind(this); 58 | this._finalizeCalled = false; 59 | this._closeMessage = null; 60 | this._closeTimer = null; 61 | this._closeCode = null; 62 | this._receiver = null; 63 | this._sender = null; 64 | this._socket = null; 65 | this._ultron = null; 66 | 67 | if (Array.isArray(address)) { 68 | initAsServerClient.call(this, address[0], address[1], address[2], options); 69 | } else { 70 | initAsClient.call(this, address, protocols, options); 71 | } 72 | } 73 | 74 | get CONNECTING () { return WebSocket.CONNECTING; } 75 | get CLOSING () { return WebSocket.CLOSING; } 76 | get CLOSED () { return WebSocket.CLOSED; } 77 | get OPEN () { return WebSocket.OPEN; } 78 | 79 | /** 80 | * @type {Number} 81 | */ 82 | get bufferedAmount () { 83 | var amount = 0; 84 | 85 | if (this._socket) { 86 | amount = this._socket.bufferSize + this._sender.bufferedBytes; 87 | } 88 | return amount; 89 | } 90 | 91 | /** 92 | * This deviates from the WHATWG interface since ws doesn't support the required 93 | * default "blob" type (instead we define a custom "nodebuffer" type). 94 | * 95 | * @type {String} 96 | */ 97 | get binaryType () { 98 | return this._binaryType; 99 | } 100 | 101 | set binaryType (type) { 102 | if (constants.BINARY_TYPES.indexOf(type) < 0) return; 103 | 104 | this._binaryType = type; 105 | 106 | // 107 | // Allow to change `binaryType` on the fly. 108 | // 109 | if (this._receiver) this._receiver.binaryType = type; 110 | } 111 | 112 | /** 113 | * Set up the socket and the internal resources. 114 | * 115 | * @param {net.Socket} socket The network socket between the server and client 116 | * @param {Buffer} head The first packet of the upgraded stream 117 | * @private 118 | */ 119 | setSocket (socket, head) { 120 | socket.setTimeout(0); 121 | socket.setNoDelay(); 122 | 123 | this._receiver = new Receiver(this.extensions, this.maxPayload, this.binaryType); 124 | this._sender = new Sender(socket, this.extensions); 125 | this._ultron = new Ultron(socket); 126 | this._socket = socket; 127 | 128 | // socket cleanup handlers 129 | this._ultron.on('close', this._finalize); 130 | this._ultron.on('error', this._finalize); 131 | this._ultron.on('end', this._finalize); 132 | 133 | // ensure that the head is added to the receiver 134 | if (head && head.length > 0) { 135 | socket.unshift(head); 136 | head = null; 137 | } 138 | 139 | // subsequent packets are pushed to the receiver 140 | this._ultron.on('data', (data) => { 141 | this.bytesReceived += data.length; 142 | this._receiver.add(data); 143 | }); 144 | 145 | // receiver event handlers 146 | this._receiver.onmessage = (data, flags) => this.emit('message', data, flags); 147 | this._receiver.onping = (data, flags) => { 148 | this.pong(data, !this._isServer, true); 149 | this.emit('ping', data, flags); 150 | }; 151 | this._receiver.onpong = (data, flags) => this.emit('pong', data, flags); 152 | this._receiver.onclose = (code, reason) => { 153 | this._closeMessage = reason; 154 | this._closeCode = code; 155 | this.close(code, reason); 156 | }; 157 | this._receiver.onerror = (error, code) => { 158 | // close the connection when the receiver reports a HyBi error code 159 | this.close(code, ''); 160 | this.emit('error', error); 161 | }; 162 | 163 | // sender event handlers 164 | this._sender.onerror = (error) => { 165 | this.close(1002, ''); 166 | this.emit('error', error); 167 | }; 168 | 169 | this.readyState = WebSocket.OPEN; 170 | this.emit('open'); 171 | } 172 | 173 | /** 174 | * Clean up and release internal resources. 175 | * 176 | * @param {(Boolean|Error)} Indicates whether or not an error occurred 177 | * @private 178 | */ 179 | finalize (error) { 180 | if (this._finalizeCalled) return; 181 | 182 | this.readyState = WebSocket.CLOSING; 183 | this._finalizeCalled = true; 184 | 185 | clearTimeout(this._closeTimer); 186 | this._closeTimer = null; 187 | 188 | // 189 | // If the connection was closed abnormally (with an error), or if the close 190 | // control frame was malformed or not received then the close code must be 191 | // 1006. 192 | // 193 | if (error) this._closeCode = 1006; 194 | 195 | if (this._socket) { 196 | this._ultron.destroy(); 197 | this._socket.on('error', function onerror () { 198 | this.destroy(); 199 | }); 200 | 201 | if (!error) this._socket.end(); 202 | else this._socket.destroy(); 203 | 204 | this._socket = null; 205 | this._ultron = null; 206 | } 207 | 208 | if (this._sender) { 209 | this._sender = this._sender.onerror = null; 210 | } 211 | 212 | if (this._receiver) { 213 | this._receiver.cleanup(() => this.emitClose()); 214 | this._receiver = null; 215 | } else { 216 | this.emitClose(); 217 | } 218 | } 219 | 220 | /** 221 | * Emit the `close` event. 222 | * 223 | * @private 224 | */ 225 | emitClose () { 226 | this.readyState = WebSocket.CLOSED; 227 | this.emit('close', this._closeCode || 1006, this._closeMessage || ''); 228 | 229 | if (this.extensions[PerMessageDeflate.extensionName]) { 230 | this.extensions[PerMessageDeflate.extensionName].cleanup(); 231 | } 232 | 233 | this.extensions = null; 234 | 235 | this.removeAllListeners(); 236 | this.on('error', constants.NOOP); // Catch all errors after this. 237 | } 238 | 239 | /** 240 | * Pause the socket stream. 241 | * 242 | * @public 243 | */ 244 | pause () { 245 | if (this.readyState !== WebSocket.OPEN) throw new Error('not opened'); 246 | 247 | this._socket.pause(); 248 | } 249 | 250 | /** 251 | * Resume the socket stream 252 | * 253 | * @public 254 | */ 255 | resume () { 256 | if (this.readyState !== WebSocket.OPEN) throw new Error('not opened'); 257 | 258 | this._socket.resume(); 259 | } 260 | 261 | /** 262 | * Start a closing handshake. 263 | * 264 | * @param {Number} code Status code explaining why the connection is closing 265 | * @param {String} data A string explaining why the connection is closing 266 | * @public 267 | */ 268 | close (code, data) { 269 | if (this.readyState === WebSocket.CLOSED) return; 270 | if (this.readyState === WebSocket.CONNECTING) { 271 | if (this._req && !this._req.aborted) { 272 | this._req.abort(); 273 | this.emit('error', new Error('closed before the connection is established')); 274 | this.finalize(true); 275 | } 276 | return; 277 | } 278 | 279 | if (this.readyState === WebSocket.CLOSING) { 280 | if (this._closeCode && this._socket) this._socket.end(); 281 | return; 282 | } 283 | 284 | this.readyState = WebSocket.CLOSING; 285 | this._sender.close(code, data, !this._isServer, (err) => { 286 | if (err) this.emit('error', err); 287 | 288 | if (this._socket) { 289 | if (this._closeCode) this._socket.end(); 290 | // 291 | // Ensure that the connection is cleaned up even when the closing 292 | // handshake fails. 293 | // 294 | clearTimeout(this._closeTimer); 295 | this._closeTimer = setTimeout(this._finalize, closeTimeout, true); 296 | } 297 | }); 298 | } 299 | 300 | /** 301 | * Send a ping message. 302 | * 303 | * @param {*} data The message to send 304 | * @param {Boolean} mask Indicates whether or not to mask `data` 305 | * @param {Boolean} failSilently Indicates whether or not to throw if `readyState` isn't `OPEN` 306 | * @public 307 | */ 308 | ping (data, mask, failSilently) { 309 | if (this.readyState !== WebSocket.OPEN) { 310 | if (failSilently) return; 311 | throw new Error('not opened'); 312 | } 313 | 314 | if (typeof data === 'number') data = data.toString(); 315 | if (mask === undefined) mask = !this._isServer; 316 | this._sender.ping(data || constants.EMPTY_BUFFER, mask); 317 | } 318 | 319 | /** 320 | * Send a pong message. 321 | * 322 | * @param {*} data The message to send 323 | * @param {Boolean} mask Indicates whether or not to mask `data` 324 | * @param {Boolean} failSilently Indicates whether or not to throw if `readyState` isn't `OPEN` 325 | * @public 326 | */ 327 | pong (data, mask, failSilently) { 328 | if (this.readyState !== WebSocket.OPEN) { 329 | if (failSilently) return; 330 | throw new Error('not opened'); 331 | } 332 | 333 | if (typeof data === 'number') data = data.toString(); 334 | if (mask === undefined) mask = !this._isServer; 335 | this._sender.pong(data || constants.EMPTY_BUFFER, mask); 336 | } 337 | 338 | /** 339 | * Send a data message. 340 | * 341 | * @param {*} data The message to send 342 | * @param {Object} options Options object 343 | * @param {Boolean} options.compress Specifies whether or not to compress `data` 344 | * @param {Boolean} options.binary Specifies whether `data` is binary or text 345 | * @param {Boolean} options.fin Specifies whether the fragment is the last one 346 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 347 | * @param {Function} cb Callback which is executed when data is written out 348 | * @public 349 | */ 350 | send (data, options, cb) { 351 | if (typeof options === 'function') { 352 | cb = options; 353 | options = {}; 354 | } 355 | 356 | if (this.readyState !== WebSocket.OPEN) { 357 | if (cb) cb(new Error('not opened')); 358 | else throw new Error('not opened'); 359 | return; 360 | } 361 | 362 | if (typeof data === 'number') data = data.toString(); 363 | 364 | const opts = Object.assign({ 365 | binary: typeof data !== 'string', 366 | mask: !this._isServer, 367 | compress: true, 368 | fin: true 369 | }, options); 370 | 371 | if (!this.extensions[PerMessageDeflate.extensionName]) { 372 | opts.compress = false; 373 | } 374 | 375 | this._sender.send(data || constants.EMPTY_BUFFER, opts, cb); 376 | } 377 | 378 | /** 379 | * Forcibly close the connection. 380 | * 381 | * @public 382 | */ 383 | terminate () { 384 | if (this.readyState === WebSocket.CLOSED) return; 385 | if (this.readyState === WebSocket.CONNECTING) { 386 | if (this._req && !this._req.aborted) { 387 | this._req.abort(); 388 | this.emit('error', new Error('closed before the connection is established')); 389 | this.finalize(true); 390 | } 391 | return; 392 | } 393 | 394 | this.finalize(true); 395 | } 396 | } 397 | 398 | WebSocket.CONNECTING = 0; 399 | WebSocket.OPEN = 1; 400 | WebSocket.CLOSING = 2; 401 | WebSocket.CLOSED = 3; 402 | 403 | // 404 | // Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. 405 | // See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface 406 | // 407 | ['open', 'error', 'close', 'message'].forEach((method) => { 408 | Object.defineProperty(WebSocket.prototype, `on${method}`, { 409 | /** 410 | * Return the listener of the event. 411 | * 412 | * @return {(Function|undefined)} The event listener or `undefined` 413 | * @public 414 | */ 415 | get () { 416 | const listeners = this.listeners(method); 417 | for (var i = 0; i < listeners.length; i++) { 418 | if (listeners[i]._listener) return listeners[i]._listener; 419 | } 420 | }, 421 | /** 422 | * Add a listener for the event. 423 | * 424 | * @param {Function} listener The listener to add 425 | * @public 426 | */ 427 | set (listener) { 428 | const listeners = this.listeners(method); 429 | for (var i = 0; i < listeners.length; i++) { 430 | // 431 | // Remove only the listeners added via `addEventListener`. 432 | // 433 | if (listeners[i]._listener) this.removeListener(method, listeners[i]); 434 | } 435 | this.addEventListener(method, listener); 436 | } 437 | }); 438 | }); 439 | 440 | WebSocket.prototype.addEventListener = EventTarget.addEventListener; 441 | WebSocket.prototype.removeEventListener = EventTarget.removeEventListener; 442 | 443 | module.exports = WebSocket; 444 | 445 | /** 446 | * Initialize a WebSocket server client. 447 | * 448 | * @param {http.IncomingMessage} req The request object 449 | * @param {net.Socket} socket The network socket between the server and client 450 | * @param {Buffer} head The first packet of the upgraded stream 451 | * @param {Object} options WebSocket attributes 452 | * @param {Number} options.protocolVersion The WebSocket protocol version 453 | * @param {Object} options.extensions The negotiated extensions 454 | * @param {Number} options.maxPayload The maximum allowed message size 455 | * @param {String} options.protocol The chosen subprotocol 456 | * @private 457 | */ 458 | function initAsServerClient (req, socket, head, options) { 459 | this.protocolVersion = options.protocolVersion; 460 | this.extensions = options.extensions; 461 | this.maxPayload = options.maxPayload; 462 | this.protocol = options.protocol; 463 | 464 | this.upgradeReq = req; 465 | this._isServer = true; 466 | 467 | this.setSocket(socket, head); 468 | } 469 | 470 | /** 471 | * Initialize a WebSocket client. 472 | * 473 | * @param {String} address The URL to which to connect 474 | * @param {String[]} protocols The list of subprotocols 475 | * @param {Object} options Connection options 476 | * @param {String} options.protocol Value of the `Sec-WebSocket-Protocol` header 477 | * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate 478 | * @param {String} options.localAddress Local interface to bind for network connections 479 | * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` header 480 | * @param {Object} options.headers An object containing request headers 481 | * @param {String} options.origin Value of the `Origin` or `Sec-WebSocket-Origin` header 482 | * @param {http.Agent} options.agent Use the specified Agent 483 | * @param {String} options.host Value of the `Host` header 484 | * @param {Number} options.family IP address family to use during hostname lookup (4 or 6). 485 | * @param {Function} options.checkServerIdentity A function to validate the server hostname 486 | * @param {Boolean} options.rejectUnauthorized Verify or not the server certificate 487 | * @param {String} options.passphrase The passphrase for the private key or pfx 488 | * @param {String} options.ciphers The ciphers to use or exclude 489 | * @param {(String|String[]|Buffer|Buffer[])} options.cert The certificate key 490 | * @param {(String|String[]|Buffer|Buffer[])} options.key The private key 491 | * @param {(String|Buffer)} options.pfx The private key, certificate, and CA certs 492 | * @param {(String|String[]|Buffer|Buffer[])} options.ca Trusted certificates 493 | * @private 494 | */ 495 | function initAsClient (address, protocols, options) { 496 | options = Object.assign({ 497 | protocolVersion: protocolVersions[1], 498 | protocol: protocols.join(','), 499 | perMessageDeflate: true, 500 | localAddress: null, 501 | headers: null, 502 | family: null, 503 | origin: null, 504 | agent: null, 505 | host: null, 506 | 507 | // 508 | // SSL options. 509 | // 510 | checkServerIdentity: null, 511 | rejectUnauthorized: null, 512 | passphrase: null, 513 | ciphers: null, 514 | cert: null, 515 | key: null, 516 | pfx: null, 517 | ca: null 518 | }, options); 519 | 520 | if (protocolVersions.indexOf(options.protocolVersion) === -1) { 521 | throw new Error( 522 | `unsupported protocol version: ${options.protocolVersion} ` + 523 | `(supported versions: ${protocolVersions.join(', ')})` 524 | ); 525 | } 526 | 527 | this.protocolVersion = options.protocolVersion; 528 | this._isServer = false; 529 | this.url = address; 530 | 531 | const serverUrl = url.parse(address); 532 | const isUnixSocket = serverUrl.protocol === 'ws+unix:'; 533 | 534 | if (!serverUrl.host && (!isUnixSocket || !serverUrl.path)) { 535 | throw new Error('invalid url'); 536 | } 537 | 538 | const isSecure = serverUrl.protocol === 'wss:' || serverUrl.protocol === 'https:'; 539 | const key = crypto.randomBytes(16).toString('base64'); 540 | const httpObj = isSecure ? https : http; 541 | 542 | // 543 | // Prepare extensions. 544 | // 545 | const extensionsOffer = {}; 546 | var perMessageDeflate; 547 | 548 | if (options.perMessageDeflate) { 549 | perMessageDeflate = new PerMessageDeflate( 550 | options.perMessageDeflate !== true ? options.perMessageDeflate : {}, 551 | false 552 | ); 553 | extensionsOffer[PerMessageDeflate.extensionName] = perMessageDeflate.offer(); 554 | } 555 | 556 | const requestOptions = { 557 | port: serverUrl.port || (isSecure ? 443 : 80), 558 | host: serverUrl.hostname, 559 | path: '/', 560 | headers: { 561 | 'Sec-WebSocket-Version': options.protocolVersion, 562 | 'Sec-WebSocket-Key': key, 563 | 'Connection': 'Upgrade', 564 | 'Upgrade': 'websocket' 565 | } 566 | }; 567 | 568 | if (options.headers) Object.assign(requestOptions.headers, options.headers); 569 | if (Object.keys(extensionsOffer).length) { 570 | requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format(extensionsOffer); 571 | } 572 | if (options.protocol) { 573 | requestOptions.headers['Sec-WebSocket-Protocol'] = options.protocol; 574 | } 575 | if (options.origin) { 576 | if (options.protocolVersion < 13) { 577 | requestOptions.headers['Sec-WebSocket-Origin'] = options.origin; 578 | } else { 579 | requestOptions.headers.Origin = options.origin; 580 | } 581 | } 582 | if (options.host) requestOptions.headers.Host = options.host; 583 | if (serverUrl.auth) requestOptions.auth = serverUrl.auth; 584 | 585 | if (options.localAddress) requestOptions.localAddress = options.localAddress; 586 | if (options.family) requestOptions.family = options.family; 587 | 588 | if (isUnixSocket) { 589 | const parts = serverUrl.path.split(':'); 590 | 591 | requestOptions.socketPath = parts[0]; 592 | requestOptions.path = parts[1]; 593 | } else if (serverUrl.path) { 594 | // 595 | // Make sure that path starts with `/`. 596 | // 597 | if (serverUrl.path.charAt(0) !== '/') { 598 | requestOptions.path = `/${serverUrl.path}`; 599 | } else { 600 | requestOptions.path = serverUrl.path; 601 | } 602 | } 603 | 604 | var agent = options.agent; 605 | 606 | // 607 | // A custom agent is required for these options. 608 | // 609 | if ( 610 | options.rejectUnauthorized != null || 611 | options.checkServerIdentity || 612 | options.passphrase || 613 | options.ciphers || 614 | options.cert || 615 | options.key || 616 | options.pfx || 617 | options.ca 618 | ) { 619 | if (options.passphrase) requestOptions.passphrase = options.passphrase; 620 | if (options.ciphers) requestOptions.ciphers = options.ciphers; 621 | if (options.cert) requestOptions.cert = options.cert; 622 | if (options.key) requestOptions.key = options.key; 623 | if (options.pfx) requestOptions.pfx = options.pfx; 624 | if (options.ca) requestOptions.ca = options.ca; 625 | if (options.checkServerIdentity) { 626 | requestOptions.checkServerIdentity = options.checkServerIdentity; 627 | } 628 | if (options.rejectUnauthorized != null) { 629 | requestOptions.rejectUnauthorized = options.rejectUnauthorized; 630 | } 631 | 632 | if (!agent) agent = new httpObj.Agent(requestOptions); 633 | } 634 | 635 | if (agent) requestOptions.agent = agent; 636 | 637 | this._req = httpObj.get(requestOptions); 638 | 639 | this._req.on('error', (error) => { 640 | if (this._req.aborted) return; 641 | 642 | this._req = null; 643 | this.emit('error', error); 644 | this.finalize(true); 645 | }); 646 | 647 | this._req.on('response', (res) => { 648 | if (!this.emit('unexpected-response', this._req, res)) { 649 | this._req.abort(); 650 | this.emit('error', new Error(`unexpected server response (${res.statusCode})`)); 651 | this.finalize(true); 652 | } 653 | }); 654 | 655 | this._req.on('upgrade', (res, socket, head) => { 656 | this._req = null; 657 | 658 | const digest = crypto.createHash('sha1') 659 | .update(key + constants.GUID, 'binary') 660 | .digest('base64'); 661 | 662 | if (res.headers['sec-websocket-accept'] !== digest) { 663 | socket.destroy(); 664 | this.emit('error', new Error('invalid server key')); 665 | return this.finalize(true); 666 | } 667 | 668 | const serverProt = res.headers['sec-websocket-protocol']; 669 | const protList = (options.protocol || '').split(/, */); 670 | var protError; 671 | 672 | if (!options.protocol && serverProt) { 673 | protError = 'server sent a subprotocol even though none requested'; 674 | } else if (options.protocol && !serverProt) { 675 | protError = 'server sent no subprotocol even though requested'; 676 | } else if (serverProt && protList.indexOf(serverProt) === -1) { 677 | protError = 'server responded with an invalid protocol'; 678 | } 679 | 680 | if (protError) { 681 | socket.destroy(); 682 | this.emit('error', new Error(protError)); 683 | return this.finalize(true); 684 | } 685 | 686 | if (serverProt) this.protocol = serverProt; 687 | 688 | const serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']); 689 | 690 | if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) { 691 | try { 692 | perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]); 693 | } catch (err) { 694 | socket.destroy(); 695 | this.emit('error', new Error('invalid extension parameter')); 696 | return this.finalize(true); 697 | } 698 | 699 | this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 700 | } 701 | 702 | this.setSocket(socket, head); 703 | }); 704 | } 705 | -------------------------------------------------------------------------------- /test/Receiver.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeBuffer = require('safe-buffer'); 4 | const assert = require('assert'); 5 | const crypto = require('crypto'); 6 | 7 | const PerMessageDeflate = require('../lib/PerMessageDeflate'); 8 | const Receiver = require('../lib/Receiver'); 9 | const Sender = require('../lib/Sender'); 10 | const util = require('./hybi-util'); 11 | 12 | const Buffer = safeBuffer.Buffer; 13 | 14 | describe('Receiver', function () { 15 | it('can parse unmasked text message', function (done) { 16 | const p = new Receiver(); 17 | 18 | p.onmessage = function (data) { 19 | assert.strictEqual(data, 'Hello'); 20 | done(); 21 | }; 22 | 23 | p.add(Buffer.from('810548656c6c6f', 'hex')); 24 | }); 25 | 26 | it('can parse close message', function (done) { 27 | const p = new Receiver(); 28 | 29 | p.onclose = function (code, data) { 30 | assert.strictEqual(code, 1000); 31 | assert.strictEqual(data, ''); 32 | done(); 33 | }; 34 | 35 | p.add(Buffer.from('8800', 'hex')); 36 | }); 37 | 38 | it('can parse masked text message', function (done) { 39 | const p = new Receiver(); 40 | 41 | p.onmessage = function (data) { 42 | assert.strictEqual(data, '5:::{"name":"echo"}'); 43 | done(); 44 | }; 45 | 46 | p.add(Buffer.from('81933483a86801b992524fa1c60959e68a5216e6cb005ba1d5', 'hex')); 47 | }); 48 | 49 | it('can parse a masked text message longer than 125 B', function (done) { 50 | const p = new Receiver(); 51 | const msg = 'A'.repeat(200); 52 | 53 | const mask = '3483a868'; 54 | const frame = Buffer.from('81FE' + util.pack(4, msg.length) + mask + 55 | util.mask(msg, mask).toString('hex'), 'hex'); 56 | 57 | p.onmessage = function (data) { 58 | assert.strictEqual(data, msg); 59 | done(); 60 | }; 61 | 62 | p.add(frame.slice(0, 2)); 63 | setImmediate(() => p.add(frame.slice(2))); 64 | }); 65 | 66 | it('can parse a really long masked text message', function (done) { 67 | const p = new Receiver(); 68 | const msg = 'A'.repeat(64 * 1024); 69 | 70 | const mask = '3483a868'; 71 | const frame = '81FF' + util.pack(16, msg.length) + mask + 72 | util.mask(msg, mask).toString('hex'); 73 | 74 | p.onmessage = function (data) { 75 | assert.strictEqual(data, msg); 76 | done(); 77 | }; 78 | 79 | p.add(Buffer.from(frame, 'hex')); 80 | }); 81 | 82 | it('can parse a fragmented masked text message of 300 B', function (done) { 83 | const p = new Receiver(); 84 | const msg = 'A'.repeat(300); 85 | 86 | const fragment1 = msg.substr(0, 150); 87 | const fragment2 = msg.substr(150); 88 | 89 | const mask = '3483a868'; 90 | const frame1 = '01FE' + util.pack(4, fragment1.length) + mask + 91 | util.mask(fragment1, mask).toString('hex'); 92 | const frame2 = '80FE' + util.pack(4, fragment2.length) + mask + 93 | util.mask(fragment2, mask).toString('hex'); 94 | 95 | p.onmessage = function (data) { 96 | assert.strictEqual(data, msg); 97 | done(); 98 | }; 99 | 100 | p.add(Buffer.from(frame1, 'hex')); 101 | p.add(Buffer.from(frame2, 'hex')); 102 | }); 103 | 104 | it('can parse a ping message', function (done) { 105 | const p = new Receiver(); 106 | const msg = 'Hello'; 107 | 108 | const mask = '3483a868'; 109 | const frame = '89' + util.getHybiLengthAsHexString(msg.length, true) + mask + 110 | util.mask(msg, mask).toString('hex'); 111 | 112 | p.onping = function (data) { 113 | assert.strictEqual(data.toString(), msg); 114 | done(); 115 | }; 116 | 117 | p.add(Buffer.from(frame, 'hex')); 118 | }); 119 | 120 | it('can parse a ping with no data', function (done) { 121 | const p = new Receiver(); 122 | 123 | p.onping = function (data) { 124 | assert.ok(data.equals(Buffer.alloc(0))); 125 | done(); 126 | }; 127 | 128 | p.add(Buffer.from('8900', 'hex')); 129 | }); 130 | 131 | it('can parse a fragmented masked text message of 300 B with a ping in the middle (1/2)', function (done) { 132 | const p = new Receiver(); 133 | const msg = 'A'.repeat(300); 134 | const pingMessage = 'Hello'; 135 | 136 | const fragment1 = msg.substr(0, 150); 137 | const fragment2 = msg.substr(150); 138 | 139 | const mask = '3483a868'; 140 | const frame1 = '01FE' + util.pack(4, fragment1.length) + mask + 141 | util.mask(fragment1, mask).toString('hex'); 142 | const frame2 = '89' + util.getHybiLengthAsHexString(pingMessage.length, true) + mask + 143 | util.mask(pingMessage, mask).toString('hex'); 144 | const frame3 = '80FE' + util.pack(4, fragment2.length) + mask + 145 | util.mask(fragment2, mask).toString('hex'); 146 | 147 | let gotPing = false; 148 | 149 | p.onmessage = function (data) { 150 | assert.strictEqual(data, msg); 151 | assert.ok(gotPing); 152 | done(); 153 | }; 154 | p.onping = function (data) { 155 | gotPing = true; 156 | assert.strictEqual(data.toString(), pingMessage); 157 | }; 158 | 159 | p.add(Buffer.from(frame1, 'hex')); 160 | p.add(Buffer.from(frame2, 'hex')); 161 | p.add(Buffer.from(frame3, 'hex')); 162 | }); 163 | 164 | it('can parse a fragmented masked text message of 300 B with a ping in the middle (2/2)', function (done) { 165 | const p = new Receiver(); 166 | const msg = 'A'.repeat(300); 167 | const pingMessage = 'Hello'; 168 | 169 | const fragment1 = msg.substr(0, 150); 170 | const fragment2 = msg.substr(150); 171 | 172 | const mask = '3483a868'; 173 | const frame1 = '01FE' + util.pack(4, fragment1.length) + mask + 174 | util.mask(fragment1, mask).toString('hex'); 175 | const frame2 = '89' + util.getHybiLengthAsHexString(pingMessage.length, true) + mask + 176 | util.mask(pingMessage, mask).toString('hex'); 177 | const frame3 = '80FE' + util.pack(4, fragment2.length) + mask + 178 | util.mask(fragment2, mask).toString('hex'); 179 | 180 | let buffers = []; 181 | 182 | buffers = buffers.concat(util.splitBuffer(Buffer.from(frame1, 'hex'))); 183 | buffers = buffers.concat(util.splitBuffer(Buffer.from(frame2, 'hex'))); 184 | buffers = buffers.concat(util.splitBuffer(Buffer.from(frame3, 'hex'))); 185 | 186 | let gotPing = false; 187 | 188 | p.onmessage = function (data) { 189 | assert.strictEqual(data, msg); 190 | assert.ok(gotPing); 191 | done(); 192 | }; 193 | p.onping = function (data) { 194 | gotPing = true; 195 | assert.strictEqual(data.toString(), pingMessage); 196 | }; 197 | 198 | for (let i = 0; i < buffers.length; ++i) { 199 | p.add(buffers[i]); 200 | } 201 | }); 202 | 203 | it('can parse a 100 B long masked binary message', function (done) { 204 | const p = new Receiver(); 205 | const msg = crypto.randomBytes(100); 206 | 207 | const mask = '3483a868'; 208 | const frame = '82' + util.getHybiLengthAsHexString(msg.length, true) + mask + 209 | util.mask(msg, mask).toString('hex'); 210 | 211 | p.onmessage = function (data) { 212 | assert.ok(data.equals(msg)); 213 | done(); 214 | }; 215 | 216 | p.add(Buffer.from(frame, 'hex')); 217 | }); 218 | 219 | it('can parse a 256 B long masked binary message', function (done) { 220 | const p = new Receiver(); 221 | const msg = crypto.randomBytes(256); 222 | 223 | const mask = '3483a868'; 224 | const frame = '82' + util.getHybiLengthAsHexString(msg.length, true) + mask + 225 | util.mask(msg, mask).toString('hex'); 226 | 227 | p.onmessage = function (data) { 228 | assert.ok(data.equals(msg)); 229 | done(); 230 | }; 231 | 232 | p.add(Buffer.from(frame, 'hex')); 233 | }); 234 | 235 | it('can parse a 200 KiB long masked binary message', function (done) { 236 | const p = new Receiver(); 237 | const msg = crypto.randomBytes(200 * 1024); 238 | 239 | const mask = '3483a868'; 240 | const frame = '82' + util.getHybiLengthAsHexString(msg.length, true) + mask + 241 | util.mask(msg, mask).toString('hex'); 242 | 243 | p.onmessage = function (data) { 244 | assert.ok(data.equals(msg)); 245 | done(); 246 | }; 247 | 248 | p.add(Buffer.from(frame, 'hex')); 249 | }); 250 | 251 | it('can parse a 200 KiB long unmasked binary message', function (done) { 252 | const p = new Receiver(); 253 | const msg = crypto.randomBytes(200 * 1024); 254 | 255 | const frame = '82' + util.getHybiLengthAsHexString(msg.length, false) + 256 | msg.toString('hex'); 257 | 258 | p.onmessage = function (data) { 259 | assert.ok(data.equals(msg)); 260 | done(); 261 | }; 262 | 263 | p.add(Buffer.from(frame, 'hex')); 264 | }); 265 | 266 | it('can parse compressed message', function (done) { 267 | const perMessageDeflate = new PerMessageDeflate(); 268 | perMessageDeflate.accept([{}]); 269 | 270 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 271 | const buf = Buffer.from('Hello'); 272 | 273 | p.onmessage = function (data) { 274 | assert.strictEqual(data, 'Hello'); 275 | done(); 276 | }; 277 | 278 | perMessageDeflate.compress(buf, true, function (err, compressed) { 279 | if (err) return done(err); 280 | 281 | p.add(Buffer.from([0xc1, compressed.length])); 282 | p.add(compressed); 283 | }); 284 | }); 285 | 286 | it('can parse compressed fragments', function (done) { 287 | const perMessageDeflate = new PerMessageDeflate(); 288 | perMessageDeflate.accept([{}]); 289 | 290 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 291 | const buf1 = Buffer.from('foo'); 292 | const buf2 = Buffer.from('bar'); 293 | 294 | p.onmessage = function (data) { 295 | assert.strictEqual(data, 'foobar'); 296 | done(); 297 | }; 298 | 299 | perMessageDeflate.compress(buf1, false, function (err, compressed1) { 300 | if (err) return done(err); 301 | 302 | p.add(Buffer.from([0x41, compressed1.length])); 303 | p.add(compressed1); 304 | 305 | perMessageDeflate.compress(buf2, true, function (err, compressed2) { 306 | if (err) return done(err); 307 | 308 | p.add(Buffer.from([0x80, compressed2.length])); 309 | p.add(compressed2); 310 | }); 311 | }); 312 | }); 313 | 314 | it('can parse a buffer with thousands of frames', function (done) { 315 | const buf = Buffer.allocUnsafe(40000); 316 | 317 | for (let i = 0; i < buf.length; i += 2) { 318 | buf[i] = 0x81; 319 | buf[i + 1] = 0x00; 320 | } 321 | 322 | const p = new Receiver(); 323 | let counter = 0; 324 | 325 | p.onmessage = function (data) { 326 | assert.strictEqual(data, ''); 327 | if (++counter === 20000) done(); 328 | }; 329 | 330 | p.add(buf); 331 | }); 332 | 333 | it('resets `totalPayloadLength` only on final frame (unfragmented)', function () { 334 | const p = new Receiver({}, 10); 335 | let message; 336 | 337 | p.onmessage = function (msg) { 338 | message = msg; 339 | }; 340 | 341 | assert.strictEqual(p.totalPayloadLength, 0); 342 | p.add(Buffer.from('810548656c6c6f', 'hex')); 343 | assert.strictEqual(p.totalPayloadLength, 0); 344 | assert.strictEqual(message, 'Hello'); 345 | }); 346 | 347 | it('resets `totalPayloadLength` only on final frame (fragmented)', function () { 348 | const p = new Receiver({}, 10); 349 | let message; 350 | 351 | p.onmessage = function (msg) { 352 | message = msg; 353 | }; 354 | 355 | assert.strictEqual(p.totalPayloadLength, 0); 356 | p.add(Buffer.from('01024865', 'hex')); 357 | assert.strictEqual(p.totalPayloadLength, 2); 358 | p.add(Buffer.from('80036c6c6f', 'hex')); 359 | assert.strictEqual(p.totalPayloadLength, 0); 360 | assert.strictEqual(message, 'Hello'); 361 | }); 362 | 363 | it('resets `totalPayloadLength` only on final frame (fragmented + ping)', function () { 364 | const p = new Receiver({}, 10); 365 | const data = []; 366 | 367 | p.onmessage = p.onping = function (buf) { 368 | data.push(buf.toString()); 369 | }; 370 | 371 | assert.strictEqual(p.totalPayloadLength, 0); 372 | p.add(Buffer.from('02024865', 'hex')); 373 | assert.strictEqual(p.totalPayloadLength, 2); 374 | p.add(Buffer.from('8900', 'hex')); 375 | assert.strictEqual(p.totalPayloadLength, 2); 376 | p.add(Buffer.from('80036c6c6f', 'hex')); 377 | assert.strictEqual(p.totalPayloadLength, 0); 378 | assert.deepStrictEqual(data, ['', 'Hello']); 379 | }); 380 | 381 | it('raises an error when RSV1 is on and permessage-deflate is disabled', function (done) { 382 | const p = new Receiver(); 383 | 384 | p.onerror = function (err, code) { 385 | assert.ok(err instanceof Error); 386 | assert.strictEqual(err.message, 'RSV1 must be clear'); 387 | assert.strictEqual(code, 1002); 388 | done(); 389 | }; 390 | 391 | p.add(Buffer.from([0xc2, 0x80, 0x00, 0x00, 0x00, 0x00])); 392 | }); 393 | 394 | it('raises an error when RSV1 is on and opcode is 0', function (done) { 395 | const perMessageDeflate = new PerMessageDeflate(); 396 | perMessageDeflate.accept([{}]); 397 | 398 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 399 | 400 | p.onerror = function (err, code) { 401 | assert.ok(err instanceof Error); 402 | assert.strictEqual(err.message, 'RSV1 must be clear'); 403 | assert.strictEqual(code, 1002); 404 | done(); 405 | }; 406 | 407 | p.add(Buffer.from([0x40, 0x00])); 408 | }); 409 | 410 | it('raises an error when RSV2 is on', function (done) { 411 | const p = new Receiver(); 412 | 413 | p.onerror = function (err, code) { 414 | assert.ok(err instanceof Error); 415 | assert.strictEqual(err.message, 'RSV2 and RSV3 must be clear'); 416 | assert.strictEqual(code, 1002); 417 | done(); 418 | }; 419 | 420 | p.add(Buffer.from([0xa2, 0x00])); 421 | }); 422 | 423 | it('raises an error when RSV3 is on', function (done) { 424 | const p = new Receiver(); 425 | 426 | p.onerror = function (err, code) { 427 | assert.ok(err instanceof Error); 428 | assert.strictEqual(err.message, 'RSV2 and RSV3 must be clear'); 429 | assert.strictEqual(code, 1002); 430 | done(); 431 | }; 432 | 433 | p.add(Buffer.from([0x92, 0x00])); 434 | }); 435 | 436 | it('raises an error if the first frame in a fragmented message has opcode 0', function (done) { 437 | const p = new Receiver(); 438 | 439 | p.onerror = function (err, code) { 440 | assert.ok(err instanceof Error); 441 | assert.strictEqual(err.message, 'invalid opcode: 0'); 442 | assert.strictEqual(code, 1002); 443 | done(); 444 | }; 445 | 446 | p.add(Buffer.from([0x00, 0x00])); 447 | }); 448 | 449 | it('raises an error if a frame has opcode 1 in the middle of a fragmented message', function (done) { 450 | const p = new Receiver(); 451 | 452 | p.onerror = function (err, code) { 453 | assert.ok(err instanceof Error); 454 | assert.strictEqual(err.message, 'invalid opcode: 1'); 455 | assert.strictEqual(code, 1002); 456 | done(); 457 | }; 458 | 459 | p.add(Buffer.from([0x01, 0x00])); 460 | p.add(Buffer.from([0x01, 0x00])); 461 | }); 462 | 463 | it('raises an error if a frame has opcode 2 in the middle of a fragmented message', function (done) { 464 | const p = new Receiver(); 465 | 466 | p.onerror = function (err, code) { 467 | assert.ok(err instanceof Error); 468 | assert.strictEqual(err.message, 'invalid opcode: 2'); 469 | assert.strictEqual(code, 1002); 470 | done(); 471 | }; 472 | 473 | p.add(Buffer.from([0x01, 0x00])); 474 | p.add(Buffer.from([0x02, 0x00])); 475 | }); 476 | 477 | it('raises an error when a control frame has the FIN bit off', function (done) { 478 | const p = new Receiver(); 479 | 480 | p.onerror = function (err, code) { 481 | assert.ok(err instanceof Error); 482 | assert.strictEqual(err.message, 'FIN must be set'); 483 | assert.strictEqual(code, 1002); 484 | done(); 485 | }; 486 | 487 | p.add(Buffer.from([0x09, 0x00])); 488 | }); 489 | 490 | it('raises an error when a control frame has the RSV1 bit on', function (done) { 491 | const perMessageDeflate = new PerMessageDeflate(); 492 | perMessageDeflate.accept([{}]); 493 | 494 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 495 | 496 | p.onerror = function (err, code) { 497 | assert.ok(err instanceof Error); 498 | assert.strictEqual(err.message, 'RSV1 must be clear'); 499 | assert.strictEqual(code, 1002); 500 | done(); 501 | }; 502 | 503 | p.add(Buffer.from([0xc9, 0x00])); 504 | }); 505 | 506 | it('raises an error when a control frame has the FIN bit off', function (done) { 507 | const p = new Receiver(); 508 | 509 | p.onerror = function (err, code) { 510 | assert.ok(err instanceof Error); 511 | assert.strictEqual(err.message, 'FIN must be set'); 512 | assert.strictEqual(code, 1002); 513 | done(); 514 | }; 515 | 516 | p.add(Buffer.from([0x09, 0x00])); 517 | }); 518 | 519 | it('raises an error when a control frame has a payload bigger than 125 B', function (done) { 520 | const p = new Receiver(); 521 | 522 | p.onerror = function (err, code) { 523 | assert.ok(err instanceof Error); 524 | assert.strictEqual(err.message, 'invalid payload length'); 525 | assert.strictEqual(code, 1002); 526 | done(); 527 | }; 528 | 529 | p.add(Buffer.from([0x89, 0x7e])); 530 | }); 531 | 532 | it('raises an error when a data frame has a payload bigger than 2^53 - 1 B', function (done) { 533 | const p = new Receiver(); 534 | 535 | p.onerror = function (err, code) { 536 | assert.ok(err instanceof Error); 537 | assert.strictEqual(err.message, 'max payload size exceeded'); 538 | assert.strictEqual(code, 1009); 539 | done(); 540 | }; 541 | 542 | p.add(Buffer.from([0x82, 0x7f])); 543 | setImmediate(() => p.add(Buffer.from([ 544 | 0x00, 0x20, 0x00, 0x00, 545 | 0x00, 0x00, 0x00, 0x00 546 | ]))); 547 | }); 548 | 549 | it('raises an error if a text frame contains invalid UTF-8 data', function (done) { 550 | const p = new Receiver(); 551 | 552 | p.onerror = function (err, code) { 553 | assert.ok(err instanceof Error); 554 | assert.strictEqual(err.message, 'invalid utf8 sequence'); 555 | assert.strictEqual(code, 1007); 556 | done(); 557 | }; 558 | 559 | p.add(Buffer.from([0x81, 0x04, 0xce, 0xba, 0xe1, 0xbd])); 560 | }); 561 | 562 | it('raises an error if a close frame has a payload of 1 B', function (done) { 563 | const p = new Receiver(); 564 | 565 | p.onerror = function (err, code) { 566 | assert.ok(err instanceof Error); 567 | assert.strictEqual(err.message, 'invalid payload length'); 568 | assert.strictEqual(code, 1002); 569 | done(); 570 | }; 571 | 572 | p.add(Buffer.from([0x88, 0x01, 0x00])); 573 | }); 574 | 575 | it('raises an error if a close frame contains an invalid close code', function (done) { 576 | const p = new Receiver(); 577 | 578 | p.onerror = function (err, code) { 579 | assert.ok(err instanceof Error); 580 | assert.strictEqual(err.message, 'invalid status code: 0'); 581 | assert.strictEqual(code, 1002); 582 | done(); 583 | }; 584 | 585 | p.add(Buffer.from([0x88, 0x02, 0x00, 0x00])); 586 | }); 587 | 588 | it('raises an error if a close frame contains invalid UTF-8 data', function (done) { 589 | const p = new Receiver(); 590 | 591 | p.onerror = function (err, code) { 592 | assert.ok(err instanceof Error); 593 | assert.strictEqual(err.message, 'invalid utf8 sequence'); 594 | assert.strictEqual(code, 1007); 595 | done(); 596 | }; 597 | 598 | p.add(Buffer.from([0x88, 0x06, 0x03, 0xef, 0xce, 0xba, 0xe1, 0xbd])); 599 | }); 600 | 601 | it('raises an error on a 200 KiB long masked binary message when `maxPayload` is 20 KiB', function (done) { 602 | const p = new Receiver({}, 20 * 1024); 603 | const msg = crypto.randomBytes(200 * 1024); 604 | 605 | const mask = '3483a868'; 606 | const frame = '82' + util.getHybiLengthAsHexString(msg.length, true) + mask + 607 | util.mask(msg, mask).toString('hex'); 608 | 609 | p.onerror = function (err, code) { 610 | assert.ok(err instanceof Error); 611 | assert.strictEqual(err.message, 'max payload size exceeded'); 612 | assert.strictEqual(code, 1009); 613 | done(); 614 | }; 615 | 616 | p.add(Buffer.from(frame, 'hex')); 617 | }); 618 | 619 | it('raises an error on a 200 KiB long unmasked binary message when `maxPayload` is 20 KiB', function (done) { 620 | const p = new Receiver({}, 20 * 1024); 621 | const msg = crypto.randomBytes(200 * 1024); 622 | 623 | const frame = '82' + util.getHybiLengthAsHexString(msg.length, false) + 624 | msg.toString('hex'); 625 | 626 | p.onerror = function (err, code) { 627 | assert.ok(err instanceof Error); 628 | assert.strictEqual(err.message, 'max payload size exceeded'); 629 | assert.strictEqual(code, 1009); 630 | done(); 631 | }; 632 | 633 | p.add(Buffer.from(frame, 'hex')); 634 | }); 635 | 636 | it('raises an error on a compressed message that exceeds `maxPayload`', function (done) { 637 | const perMessageDeflate = new PerMessageDeflate({}, false, 25); 638 | perMessageDeflate.accept([{}]); 639 | 640 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }, 25); 641 | const buf = Buffer.from('A'.repeat(50)); 642 | 643 | p.onerror = function (err, code) { 644 | assert.ok(err instanceof Error); 645 | assert.strictEqual(err.message, 'max payload size exceeded'); 646 | assert.strictEqual(code, 1009); 647 | done(); 648 | }; 649 | 650 | perMessageDeflate.compress(buf, true, function (err, data) { 651 | if (err) return done(err); 652 | 653 | p.add(Buffer.from([0xc1, data.length])); 654 | p.add(data); 655 | }); 656 | }); 657 | 658 | it('raises an error if the sum of fragment lengths exceeds `maxPayload`', function (done) { 659 | const perMessageDeflate = new PerMessageDeflate({}, false, 25); 660 | perMessageDeflate.accept([{}]); 661 | 662 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }, 25); 663 | const buf = Buffer.from('A'.repeat(15)); 664 | 665 | p.onerror = function (err, code) { 666 | assert.ok(err instanceof Error); 667 | assert.strictEqual(err.message, 'max payload size exceeded'); 668 | assert.strictEqual(code, 1009); 669 | done(); 670 | }; 671 | 672 | perMessageDeflate.compress(buf, false, function (err, fragment1) { 673 | if (err) return done(err); 674 | 675 | p.add(Buffer.from([0x41, fragment1.length])); 676 | p.add(fragment1); 677 | 678 | perMessageDeflate.compress(buf, true, function (err, fragment2) { 679 | if (err) return done(err); 680 | 681 | p.add(Buffer.from([0x80, fragment2.length])); 682 | p.add(fragment2); 683 | }); 684 | }); 685 | }); 686 | 687 | it('doesn\'t crash if data is received after `maxPayload` is exceeded', function (done) { 688 | const p = new Receiver({}, 5); 689 | const buf = crypto.randomBytes(10); 690 | 691 | let gotError = false; 692 | 693 | p.onerror = function (reason, code) { 694 | gotError = true; 695 | assert.strictEqual(code, 1009); 696 | }; 697 | 698 | p.add(Buffer.from([0x82, buf.length])); 699 | 700 | assert.ok(gotError); 701 | assert.strictEqual(p.onerror, null); 702 | 703 | p.add(buf); 704 | done(); 705 | }); 706 | 707 | it('consumes all data before calling `cleanup` callback (1/4)', function (done) { 708 | const perMessageDeflate = new PerMessageDeflate(); 709 | perMessageDeflate.accept([{}]); 710 | 711 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 712 | const buf = Buffer.from('Hello'); 713 | const results = []; 714 | 715 | p.onmessage = (message) => results.push(message); 716 | 717 | perMessageDeflate.compress(buf, true, (err, data) => { 718 | if (err) return done(err); 719 | 720 | const frame = Buffer.concat([Buffer.from([0xc1, data.length]), data]); 721 | 722 | p.add(frame); 723 | p.add(frame); 724 | 725 | assert.strictEqual(p.state, 5); 726 | assert.strictEqual(p.bufferedBytes, frame.length); 727 | 728 | p.cleanup(() => { 729 | assert.deepStrictEqual(results, ['Hello', 'Hello']); 730 | assert.strictEqual(p.onmessage, null); 731 | done(); 732 | }); 733 | }); 734 | }); 735 | 736 | it('consumes all data before calling `cleanup` callback (2/4)', function (done) { 737 | const perMessageDeflate = new PerMessageDeflate(); 738 | perMessageDeflate.accept([{}]); 739 | 740 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 741 | const buf = Buffer.from('Hello'); 742 | const results = []; 743 | 744 | p.onclose = (code, reason) => results.push(code, reason); 745 | p.onmessage = (message) => results.push(message); 746 | 747 | perMessageDeflate.compress(buf, true, (err, data) => { 748 | if (err) return done(err); 749 | 750 | const textFrame = Buffer.concat([Buffer.from([0xc1, data.length]), data]); 751 | const closeFrame = Buffer.from([0x88, 0x00]); 752 | 753 | p.add(textFrame); 754 | p.add(textFrame); 755 | p.add(closeFrame); 756 | 757 | assert.strictEqual(p.state, 5); 758 | assert.strictEqual(p.bufferedBytes, textFrame.length + closeFrame.length); 759 | 760 | p.cleanup(() => { 761 | assert.deepStrictEqual(results, ['Hello', 'Hello', 1000, '']); 762 | assert.strictEqual(p.onmessage, null); 763 | done(); 764 | }); 765 | }); 766 | }); 767 | 768 | it('consumes all data before calling `cleanup` callback (3/4)', function (done) { 769 | const perMessageDeflate = new PerMessageDeflate(); 770 | perMessageDeflate.accept([{}]); 771 | 772 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 773 | const buf = Buffer.from('Hello'); 774 | const results = []; 775 | 776 | p.onerror = (err, code) => results.push(err.message, code); 777 | p.onmessage = (message) => results.push(message); 778 | 779 | perMessageDeflate.compress(buf, true, (err, data) => { 780 | if (err) return done(err); 781 | 782 | const textFrame = Buffer.concat([Buffer.from([0xc1, data.length]), data]); 783 | const invalidFrame = Buffer.from([0xa0, 0x00]); 784 | 785 | p.add(textFrame); 786 | p.add(textFrame); 787 | p.add(invalidFrame); 788 | 789 | assert.strictEqual(p.state, 5); 790 | assert.strictEqual(p.bufferedBytes, textFrame.length + invalidFrame.length); 791 | 792 | p.cleanup(() => { 793 | assert.deepStrictEqual(results, [ 794 | 'Hello', 795 | 'Hello', 796 | 'RSV2 and RSV3 must be clear', 797 | 1002 798 | ]); 799 | assert.strictEqual(p.onmessage, null); 800 | done(); 801 | }); 802 | }); 803 | }); 804 | 805 | it('consumes all data before calling `cleanup` callback (4/4)', function (done) { 806 | const perMessageDeflate = new PerMessageDeflate(); 807 | perMessageDeflate.accept([{}]); 808 | 809 | const p = new Receiver({ 'permessage-deflate': perMessageDeflate }); 810 | const buf = Buffer.from('Hello'); 811 | const results = []; 812 | 813 | p.onmessage = (message) => results.push(message); 814 | 815 | perMessageDeflate.compress(buf, true, (err, data) => { 816 | if (err) return done(err); 817 | 818 | const textFrame = Buffer.concat([Buffer.from([0xc1, data.length]), data]); 819 | const incompleteFrame = Buffer.from([0x82, 0x0a, 0x00, 0x00]); 820 | 821 | p.add(textFrame); 822 | p.add(incompleteFrame); 823 | 824 | assert.strictEqual(p.state, 5); 825 | assert.strictEqual(p.bufferedBytes, incompleteFrame.length); 826 | 827 | p.cleanup(() => { 828 | assert.deepStrictEqual(results, ['Hello']); 829 | assert.strictEqual(p.onmessage, null); 830 | done(); 831 | }); 832 | }); 833 | }); 834 | 835 | it('can emit nodebuffer of fragmented binary message', function (done) { 836 | const p = new Receiver(); 837 | const frags = [ 838 | crypto.randomBytes(7321), 839 | crypto.randomBytes(137), 840 | crypto.randomBytes(285787), 841 | crypto.randomBytes(3) 842 | ]; 843 | 844 | p.binaryType = 'nodebuffer'; 845 | p.onmessage = (data) => { 846 | assert.ok(Buffer.isBuffer(data)); 847 | assert.ok(data.equals(Buffer.concat(frags))); 848 | done(); 849 | }; 850 | 851 | frags.forEach((frag, i) => { 852 | Sender.frame(frag, { 853 | fin: i === frags.length - 1, 854 | opcode: i === 0 ? 2 : 0, 855 | readOnly: true, 856 | mask: false, 857 | rsv1: false 858 | }).forEach((buf) => p.add(buf)); 859 | }); 860 | }); 861 | 862 | it('can emit arraybuffer of fragmented binary message', function (done) { 863 | const p = new Receiver(); 864 | const frags = [ 865 | crypto.randomBytes(19221), 866 | crypto.randomBytes(954), 867 | crypto.randomBytes(623987) 868 | ]; 869 | 870 | p.binaryType = 'arraybuffer'; 871 | p.onmessage = (data) => { 872 | assert.ok(data instanceof ArrayBuffer); 873 | assert.ok(Buffer.from(data).equals(Buffer.concat(frags))); 874 | done(); 875 | }; 876 | 877 | frags.forEach((frag, i) => { 878 | Sender.frame(frag, { 879 | fin: i === frags.length - 1, 880 | opcode: i === 0 ? 2 : 0, 881 | readOnly: true, 882 | mask: false, 883 | rsv1: false 884 | }).forEach((buf) => p.add(buf)); 885 | }); 886 | }); 887 | 888 | it('can emit fragments of fragmented binary message', function (done) { 889 | const p = new Receiver(); 890 | const frags = [ 891 | crypto.randomBytes(17), 892 | crypto.randomBytes(419872), 893 | crypto.randomBytes(83), 894 | crypto.randomBytes(9928), 895 | crypto.randomBytes(1) 896 | ]; 897 | 898 | p.binaryType = 'fragments'; 899 | p.onmessage = (data) => { 900 | assert.deepStrictEqual(data, frags); 901 | done(); 902 | }; 903 | 904 | frags.forEach((frag, i) => { 905 | Sender.frame(frag, { 906 | fin: i === frags.length - 1, 907 | opcode: i === 0 ? 2 : 0, 908 | readOnly: true, 909 | mask: false, 910 | rsv1: false 911 | }).forEach((buf) => p.add(buf)); 912 | }); 913 | }); 914 | }); 915 | --------------------------------------------------------------------------------