├── .npmrc ├── .npmignore ├── .gitignore ├── index.js ├── binding.gyp ├── package.json ├── fallback.js ├── LICENSE ├── README.md ├── test └── test.js ├── .github └── workflows │ └── ci.yml └── src └── bufferutil.c /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | test/ 3 | .* 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | prebuilds/ 3 | build/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | try { 4 | module.exports = require('node-gyp-build')(__dirname); 5 | } catch (e) { 6 | module.exports = require('./fallback'); 7 | } 8 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'variables': { 3 | 'openssl_fips': '' 4 | }, 5 | 'targets': [ 6 | { 7 | 'target_name': 'bufferutil', 8 | 'sources': ['src/bufferutil.c'], 9 | 'cflags': ['-std=c99'], 10 | 'conditions': [ 11 | ["OS=='mac'", { 12 | 'xcode_settings': { 13 | 'MACOSX_DEPLOYMENT_TARGET': '10.7' 14 | } 15 | }] 16 | ] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bufferutil", 3 | "version": "4.1.0", 4 | "description": "WebSocket buffer utils", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=6.14.2" 8 | }, 9 | "scripts": { 10 | "install": "node-gyp-build", 11 | "prebuild": "prebuildify --napi --strip --target=8.11.2", 12 | "test": "mocha" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/websockets/bufferutil.git" 17 | }, 18 | "keywords": [ 19 | "bufferutil" 20 | ], 21 | "author": "Einar Otto Stangvik (http://2x.io)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/websockets/bufferutil/issues" 25 | }, 26 | "homepage": "https://github.com/websockets/bufferutil", 27 | "dependencies": { 28 | "node-gyp-build": "^4.3.0" 29 | }, 30 | "devDependencies": { 31 | "mocha": "^11.0.1", 32 | "node-gyp": "^12.1.0", 33 | "prebuildify": "^6.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /fallback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Masks a buffer using the given mask. 5 | * 6 | * @param {Buffer} source The buffer to mask 7 | * @param {Buffer} mask The mask to use 8 | * @param {Buffer} output The buffer where to store the result 9 | * @param {Number} offset The offset at which to start writing 10 | * @param {Number} length The number of bytes to mask. 11 | * @public 12 | */ 13 | const mask = (source, mask, output, offset, length) => { 14 | for (var i = 0; i < length; i++) { 15 | output[offset + i] = source[i] ^ mask[i & 3]; 16 | } 17 | }; 18 | 19 | /** 20 | * Unmasks a buffer using the given mask. 21 | * 22 | * @param {Buffer} buffer The buffer to unmask 23 | * @param {Buffer} mask The mask to use 24 | * @public 25 | */ 26 | const unmask = (buffer, mask) => { 27 | // Required until https://github.com/nodejs/node/issues/9006 is resolved. 28 | const length = buffer.length; 29 | for (var i = 0; i < length; i++) { 30 | buffer[i] ^= mask[i & 3]; 31 | } 32 | }; 33 | 34 | module.exports = { mask, unmask }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Einar Otto Stangvik 2 | Copyright (c) 2013 Arnout Kazemier and contributors 3 | Copyright (c) 2016 Luigi Pinca and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bufferutil 2 | 3 | [![Version npm](https://img.shields.io/npm/v/bufferutil.svg?logo=npm)](https://www.npmjs.com/package/bufferutil) 4 | [![Linux/macOS/Windows Build](https://img.shields.io/github/actions/workflow/status/websockets/bufferutil/ci.yml?branch=master&label=build&logo=github)](https://github.com/websockets/bufferutil/actions?query=workflow%3ACI+branch%3Amaster) 5 | 6 | `bufferutil` is what makes `ws` fast. It provides some utilities to efficiently 7 | perform some operations such as masking and unmasking the data payload of 8 | WebSocket frames. 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install bufferutil --save-optional 14 | ``` 15 | 16 | The `--save-optional` flag tells npm to save the package in your package.json 17 | under the 18 | [`optionalDependencies`](https://docs.npmjs.com/files/package.json#optionaldependencies) 19 | key. 20 | 21 | ## API 22 | 23 | The module exports two functions. To maximize performance, parameters are not 24 | validated. It is the caller's responsibility to ensure that they are correct. 25 | 26 | ### `bufferUtil.mask(source, mask, output, offset, length)` 27 | 28 | Masks a buffer using the given masking-key as specified by the WebSocket 29 | protocol. 30 | 31 | #### Arguments 32 | 33 | - `source` - The buffer to mask. 34 | - `mask` - A buffer representing the masking-key. 35 | - `output` - The buffer where to store the result. 36 | - `offset` - The offset at which to start writing. 37 | - `length` - The number of bytes to mask. 38 | 39 | #### Example 40 | 41 | ```js 42 | 'use strict'; 43 | 44 | const bufferUtil = require('bufferutil'); 45 | const crypto = require('crypto'); 46 | 47 | const source = crypto.randomBytes(10); 48 | const mask = crypto.randomBytes(4); 49 | 50 | bufferUtil.mask(source, mask, source, 0, source.length); 51 | ``` 52 | 53 | ### `bufferUtil.unmask(buffer, mask)` 54 | 55 | Unmasks a buffer using the given masking-key as specified by the WebSocket 56 | protocol. 57 | 58 | #### Arguments 59 | 60 | - `buffer` - The buffer to unmask. 61 | - `mask` - A buffer representing the masking-key. 62 | 63 | #### Example 64 | 65 | ```js 66 | 'use strict'; 67 | 68 | const bufferUtil = require('bufferutil'); 69 | const crypto = require('crypto'); 70 | 71 | const buffer = crypto.randomBytes(10); 72 | const mask = crypto.randomBytes(4); 73 | 74 | bufferUtil.unmask(buffer, mask); 75 | ``` 76 | 77 | ## License 78 | 79 | [MIT](LICENSE) 80 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { deepStrictEqual } = require('assert'); 4 | const { randomBytes } = require('crypto'); 5 | const { join } = require('path'); 6 | 7 | const native = require('node-gyp-build')(join(__dirname, '..')); 8 | const fallback = require('../fallback'); 9 | 10 | function use(bufferUtil) { 11 | return function () { 12 | it('masks a buffer (1/2)', function () { 13 | const buf = Buffer.from([0x6c, 0x3c, 0x58, 0xd9, 0x3e, 0x21, 0x09, 0x9f]); 14 | const mask = Buffer.from([0x48, 0x2a, 0xce, 0x24]); 15 | 16 | bufferUtil.mask(buf, mask, buf, 0, buf.length); 17 | 18 | deepStrictEqual( 19 | buf, 20 | Buffer.from([0x24, 0x16, 0x96, 0xfd, 0x76, 0x0b, 0xc7, 0xbb]) 21 | ); 22 | }); 23 | 24 | it('masks a buffer (2/2)', function () { 25 | const src = Buffer.from([0x6c, 0x3c, 0x58, 0xd9, 0x3e, 0x21, 0x09, 0x9f]); 26 | const mask = Buffer.from([0x48, 0x2a, 0xce, 0x24]); 27 | const dest = Buffer.alloc(src.length + 2); 28 | 29 | bufferUtil.mask(src, mask, dest, 2, src.length); 30 | 31 | deepStrictEqual( 32 | dest, 33 | Buffer.from([0x00, 0x00, 0x24, 0x16, 0x96, 0xfd, 0x76, 0x0b, 0xc7, 0xbb]) 34 | ); 35 | }); 36 | 37 | it('unmasks a buffer', function () { 38 | const buf = Buffer.from([0x24, 0x16, 0x96, 0xfd, 0x76, 0x0b, 0xc7, 0xbb]); 39 | const mask = Buffer.from([0x48, 0x2a, 0xce, 0x24]); 40 | 41 | bufferUtil.unmask(buf, mask); 42 | 43 | deepStrictEqual( 44 | buf, 45 | Buffer.from([0x6c, 0x3c, 0x58, 0xd9, 0x3e, 0x21, 0x09, 0x9f]) 46 | ); 47 | }); 48 | }; 49 | } 50 | 51 | describe('bindings', use(native)); 52 | describe('fallback', use(fallback)); 53 | 54 | describe('bindings match fallback', () => { 55 | const sizes = [1, 127, 128, 200, 1024, 10 * 1024 - 1, 10 * 1024] 56 | const offsets = [0, 1, 10, 16, 128] 57 | 58 | it('masks', function () { 59 | for (const size of sizes) { 60 | for (const offset of offsets) { 61 | const src = randomBytes(size); 62 | const mask = randomBytes(4); 63 | const dest = randomBytes(size + offset); 64 | const destFallback = Buffer.from(dest); 65 | 66 | native.mask(src, mask, dest, offset, size); 67 | fallback.mask(src, mask, destFallback, offset, size); 68 | 69 | deepStrictEqual(dest, destFallback); 70 | } 71 | } 72 | }); 73 | 74 | it('unmasks', function () { 75 | for (const size of sizes) { 76 | const buf1 = randomBytes(size); 77 | const buf2 = Buffer.from(buf1); 78 | const mask = randomBytes(4); 79 | 80 | native.unmask(buf1, mask); 81 | fallback.unmask(buf2, mask); 82 | 83 | deepStrictEqual(buf1, buf2); 84 | } 85 | }); 86 | }) 87 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | arch: 15 | - x64 16 | - arm64 17 | node: 18 | - 20 19 | - 22 20 | - 24 21 | os: 22 | - macos-latest 23 | - macos-15-intel 24 | - ubuntu-latest 25 | - windows-latest 26 | exclude: 27 | - arch: arm64 28 | os: macos-15-intel 29 | - arch: arm64 30 | os: ubuntu-latest 31 | - arch: arm64 32 | os: windows-latest 33 | - arch: x64 34 | os: macos-latest 35 | include: 36 | - arch: x86 37 | node: 20 38 | os: windows-latest 39 | - arch: x86 40 | node: 22 41 | os: windows-latest 42 | steps: 43 | - uses: actions/checkout@v6 44 | - uses: actions/setup-node@v6 45 | with: 46 | node-version: ${{ matrix.node }} 47 | architecture: ${{ matrix.arch }} 48 | - run: npm install 49 | - run: npm test 50 | build: 51 | if: startsWith(github.ref, 'refs/tags/') 52 | needs: test 53 | runs-on: ${{ matrix.os }} 54 | strategy: 55 | matrix: 56 | arch: 57 | - x64 58 | os: 59 | - macos-15-intel 60 | - ubuntu-22.04 61 | - windows-latest 62 | include: 63 | - arch: x86 64 | os: windows-latest 65 | - arch: arm64 66 | os: macos-latest 67 | steps: 68 | - uses: actions/checkout@v6 69 | - uses: actions/setup-node@v6 70 | with: 71 | node-version: 20 72 | architecture: ${{ matrix.arch }} 73 | - run: npm install 74 | - run: npm run prebuild 75 | - uses: actions/upload-artifact@v5 76 | with: 77 | name: ${{ matrix.os }}-${{ matrix.arch }} 78 | path: prebuilds 79 | retention-days: 1 80 | release: 81 | needs: build 82 | permissions: 83 | contents: write # Needed for softprops/action-gh-release. 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v6 87 | - uses: actions/download-artifact@v6 88 | with: 89 | path: prebuilds 90 | - run: echo "version=$(git describe --tags)" >> $GITHUB_OUTPUT 91 | id: get_version 92 | - run: 93 | tar -cvf "${{ steps.get_version.outputs.version }}-darwin-x64.tar" -C 94 | "prebuilds/macos-15-intel-x64" darwin-x64 95 | - run: 96 | tar -cvf "${{ steps.get_version.outputs.version }}-darwin-arm64.tar" 97 | -C "prebuilds/macos-latest-arm64" darwin-arm64 98 | - run: 99 | tar -cvf "${{ steps.get_version.outputs.version }}-linux-x64.tar" -C 100 | "prebuilds/ubuntu-22.04-x64" linux-x64 101 | - run: 102 | tar -cvf "${{ steps.get_version.outputs.version }}-win32-ia32.tar" -C 103 | "prebuilds/windows-latest-x86" win32-ia32 104 | - run: 105 | tar -cvf "${{ steps.get_version.outputs.version }}-win32-x64.tar" -C 106 | "prebuilds/windows-latest-x64" win32-x64 107 | - uses: softprops/action-gh-release@v2 108 | with: 109 | files: ${{ steps.get_version.outputs.version }}-*.tar 110 | token: ${{ secrets.GITHUB_TOKEN }} 111 | -------------------------------------------------------------------------------- /src/bufferutil.c: -------------------------------------------------------------------------------- 1 | #define NAPI_VERSION 1 2 | #include 3 | #include 4 | 5 | napi_value Mask(napi_env env, napi_callback_info info) { 6 | napi_status status; 7 | size_t argc = 5; 8 | napi_value argv[5]; 9 | 10 | status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); 11 | assert(status == napi_ok); 12 | 13 | uint8_t *source; 14 | uint8_t *mask; 15 | uint8_t *destination; 16 | int64_t offset; 17 | int64_t length; 18 | 19 | status = napi_get_buffer_info(env, argv[0], (void **)&source, NULL); 20 | assert(status == napi_ok); 21 | 22 | status = napi_get_buffer_info(env, argv[1], (void **)&mask, NULL); 23 | assert(status == napi_ok); 24 | 25 | status = napi_get_buffer_info(env, argv[2], (void **)&destination, NULL); 26 | assert(status == napi_ok); 27 | 28 | status = napi_get_value_int64(env, argv[3], &offset); 29 | assert(status == napi_ok); 30 | 31 | status = napi_get_value_int64(env, argv[4], &length); 32 | assert(status == napi_ok); 33 | 34 | destination += offset; 35 | uint8_t index = 0; 36 | 37 | // 38 | // Alignment preamble. 39 | // 40 | while (index < length && (size_t)source % 8) { 41 | *destination++ = *source++ ^ mask[index % 4]; 42 | index++; 43 | } 44 | 45 | length -= index; 46 | if (!length) 47 | return NULL; 48 | 49 | // 50 | // Realign mask and convert to 64 bit. 51 | // 52 | uint8_t maskAlignedArray[8]; 53 | 54 | for (uint8_t i = 0; i < 8; i++, index++) { 55 | maskAlignedArray[i] = mask[index % 4]; 56 | } 57 | 58 | // 59 | // Apply 64 bit mask in 8 byte chunks. 60 | // 61 | int64_t loop = length / 8; 62 | uint64_t mask8 = ((uint64_t *)maskAlignedArray)[0]; 63 | uint64_t *pFrom8 = (uint64_t *)source; 64 | uint64_t *pTo8 = (uint64_t *)destination; 65 | 66 | for (int64_t i = 0; i < loop; i++) { 67 | pTo8[i] = pFrom8[i] ^ mask8; 68 | } 69 | 70 | // 71 | // Apply mask to remaining data. 72 | // 73 | length %= 8; 74 | source += 8 * loop; 75 | destination += 8 * loop; 76 | 77 | for (uint8_t i = 0; i < length; i++) { 78 | destination[i] = source[i] ^ maskAlignedArray[i]; 79 | } 80 | 81 | return NULL; 82 | } 83 | 84 | napi_value Unmask(napi_env env, napi_callback_info info) { 85 | napi_status status; 86 | size_t argc = 2; 87 | napi_value argv[2]; 88 | 89 | status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); 90 | assert(status == napi_ok); 91 | 92 | uint8_t *source; 93 | uint8_t *mask; 94 | size_t length; 95 | 96 | status = napi_get_buffer_info(env, argv[0], (void **)&source, &length); 97 | assert(status == napi_ok); 98 | 99 | status = napi_get_buffer_info(env, argv[1], (void **)&mask, NULL); 100 | assert(status == napi_ok); 101 | 102 | uint8_t index = 0; 103 | 104 | // 105 | // Alignment preamble. 106 | // 107 | while (index < length && (size_t)source % 8) { 108 | *source++ ^= mask[index % 4]; 109 | index++; 110 | } 111 | 112 | length -= index; 113 | if (!length) 114 | return NULL; 115 | 116 | // 117 | // Realign mask and convert to 64 bit. 118 | // 119 | uint8_t maskAlignedArray[8]; 120 | 121 | for (uint8_t i = 0; i < 8; i++, index++) { 122 | maskAlignedArray[i] = mask[index % 4]; 123 | } 124 | 125 | // 126 | // Apply 64 bit mask in 8 byte chunks. 127 | // 128 | size_t loop = length / 8; 129 | uint64_t mask8 = ((uint64_t *)maskAlignedArray)[0]; 130 | uint64_t *pSource8 = (uint64_t *)source; 131 | 132 | for (size_t i = 0; i < loop; i++) { 133 | pSource8[i] ^= mask8; 134 | } 135 | 136 | // 137 | // Apply mask to remaining data. 138 | // 139 | length %= 8; 140 | source += 8 * loop; 141 | 142 | for (uint8_t i = 0; i < length; i++) { 143 | source[i] ^= maskAlignedArray[i]; 144 | } 145 | 146 | return NULL; 147 | } 148 | 149 | napi_value Init(napi_env env, napi_value exports) { 150 | napi_status status; 151 | napi_value mask; 152 | napi_value unmask; 153 | 154 | status = napi_create_function(env, NULL, 0, Mask, NULL, &mask); 155 | assert(status == napi_ok); 156 | 157 | status = napi_create_function(env, NULL, 0, Unmask, NULL, &unmask); 158 | assert(status == napi_ok); 159 | 160 | status = napi_set_named_property(env, exports, "mask", mask); 161 | assert(status == napi_ok); 162 | 163 | status = napi_set_named_property(env, exports, "unmask", unmask); 164 | assert(status == napi_ok); 165 | 166 | return exports; 167 | } 168 | 169 | NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) 170 | --------------------------------------------------------------------------------