'use strict'; const assert = require('assert'); const PerMessageDeflate = require('../lib/permessage-deflate'); const extension = require('../lib/extension'); describe('PerMessageDeflate', () => { describe('#offer', () => { it('creates an offer', () => { const perMessageDeflate = new PerMessageDeflate(); assert.deepStrictEqual(perMessageDeflate.offer(), { client_max_window_bits: true }); }); it('uses the configuration options', () => { const perMessageDeflate = new PerMessageDeflate({ serverNoContextTakeover: true, clientNoContextTakeover: true, serverMaxWindowBits: 10, clientMaxWindowBits: 11 }); assert.deepStrictEqual(perMessageDeflate.offer(), { server_no_context_takeover: true, client_no_context_takeover: true, server_max_window_bits: 10, client_max_window_bits: 11 }); }); }); describe('#accept', () => { it('throws an error if a parameter has multiple values', () => { const perMessageDeflate = new PerMessageDeflate(); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover; server_no_context_takeover' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: Parameter "server_no_context_takeover" must have only a single value$/ ); }); it('throws an error if a parameter has an invalid name', () => { const perMessageDeflate = new PerMessageDeflate(); const extensions = extension.parse('permessage-deflate;foo'); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: Unknown parameter "foo"$/ ); }); it('throws an error if client_no_context_takeover has a value', () => { const perMessageDeflate = new PerMessageDeflate(); const extensions = extension.parse( 'permessage-deflate; client_no_context_takeover=10' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^TypeError: Invalid value for parameter "client_no_context_takeover": 10$/ ); }); it('throws an error if server_no_context_takeover has a value', () => { const perMessageDeflate = new PerMessageDeflate(); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover=10' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^TypeError: Invalid value for parameter "server_no_context_takeover": 10$/ ); }); it('throws an error if server_max_window_bits has an invalid value', () => { const perMessageDeflate = new PerMessageDeflate(); let extensions = extension.parse( 'permessage-deflate; server_max_window_bits=7' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^TypeError: Invalid value for parameter "server_max_window_bits": 7$/ ); extensions = extension.parse( 'permessage-deflate; server_max_window_bits' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^TypeError: Invalid value for parameter "server_max_window_bits": true$/ ); }); describe('As server', () => { it('accepts an offer with no parameters', () => { const perMessageDeflate = new PerMessageDeflate({}, true); assert.deepStrictEqual(perMessageDeflate.accept([{}]), {}); }); it('accepts an offer with parameters', () => { const perMessageDeflate = new PerMessageDeflate({}, true); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover; ' + 'client_no_context_takeover; server_max_window_bits=10; ' + 'client_max_window_bits=11' ); assert.deepStrictEqual( perMessageDeflate.accept(extensions['permessage-deflate']), { server_no_context_takeover: true, client_no_context_takeover: true, server_max_window_bits: 10, client_max_window_bits: 11, __proto__: null } ); }); it('prefers the configuration options', () => { const perMessageDeflate = new PerMessageDeflate( { serverNoContextTakeover: true, clientNoContextTakeover: true, serverMaxWindowBits: 12, clientMaxWindowBits: 11 }, true ); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=14; client_max_window_bits=13' ); assert.deepStrictEqual( perMessageDeflate.accept(extensions['permessage-deflate']), { server_no_context_takeover: true, client_no_context_takeover: true, server_max_window_bits: 12, client_max_window_bits: 11, __proto__: null } ); }); it('accepts the first supported offer', () => { const perMessageDeflate = new PerMessageDeflate( { serverMaxWindowBits: 11 }, true ); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=10, permessage-deflate' ); assert.deepStrictEqual( perMessageDeflate.accept(extensions['permessage-deflate']), { server_max_window_bits: 11, __proto__: null } ); }); it('throws an error if server_no_context_takeover is unsupported', () => { const perMessageDeflate = new PerMessageDeflate( { serverNoContextTakeover: false }, true ); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: None of the extension offers can be accepted$/ ); }); it('throws an error if server_max_window_bits is unsupported', () => { const perMessageDeflate = new PerMessageDeflate( { serverMaxWindowBits: false }, true ); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=10' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: None of the extension offers can be accepted$/ ); }); it('throws an error if server_max_window_bits is less than configuration', () => { const perMessageDeflate = new PerMessageDeflate( { serverMaxWindowBits: 11 }, true ); const extensions = extension.parse( 'permessage-deflate; server_max_window_bits=10' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: None of the extension offers can be accepted$/ ); }); it('throws an error if client_max_window_bits is unsupported on client', () => { const perMessageDeflate = new PerMessageDeflate( { clientMaxWindowBits: 10 }, true ); const extensions = extension.parse('permessage-deflate'); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: None of the extension offers can be accepted$/ ); }); it('throws an error if client_max_window_bits has an invalid value', () => { const perMessageDeflate = new PerMessageDeflate({}, true); const extensions = extension.parse( 'permessage-deflate; client_max_window_bits=16' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^TypeError: Invalid value for parameter "client_max_window_bits": 16$/ ); }); }); describe('As client', () => { it('accepts a response with no parameters', () => { const perMessageDeflate = new PerMessageDeflate({}); assert.deepStrictEqual(perMessageDeflate.accept([{}]), {}); }); it('accepts a response with parameters', () => { const perMessageDeflate = new PerMessageDeflate({}); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover; ' + 'client_no_context_takeover; server_max_window_bits=10; ' + 'client_max_window_bits=11' ); assert.deepStrictEqual( perMessageDeflate.accept(extensions['permessage-deflate']), { server_no_context_takeover: true, client_no_context_takeover: true, server_max_window_bits: 10, client_max_window_bits: 11, __proto__: null } ); }); it('throws an error if client_no_context_takeover is unsupported', () => { const perMessageDeflate = new PerMessageDeflate({ clientNoContextTakeover: false }); const extensions = extension.parse( 'permessage-deflate; client_no_context_takeover' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: Unexpected parameter "client_no_context_takeover"$/ ); }); it('throws an error if client_max_window_bits is unsupported', () => { const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: false }); const extensions = extension.parse( 'permessage-deflate; client_max_window_bits=10' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: Unexpected or invalid parameter "client_max_window_bits"$/ ); }); it('throws an error if client_max_window_bits is greater than configuration', () => { const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }); const extensions = extension.parse( 'permessage-deflate; client_max_window_bits=11' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^Error: Unexpected or invalid parameter "client_max_window_bits"$/ ); }); it('throws an error if client_max_window_bits has an invalid value', () => { const perMessageDeflate = new PerMessageDeflate(); let extensions = extension.parse( 'permessage-deflate; client_max_window_bits=16' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^TypeError: Invalid value for parameter "client_max_window_bits": 16$/ ); extensions = extension.parse( 'permessage-deflate; client_max_window_bits' ); assert.throws( () => perMessageDeflate.accept(extensions['permessage-deflate']), /^TypeError: Invalid value for parameter "client_max_window_bits": true$/ ); }); it('uses the config value if client_max_window_bits is not specified', () => { const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }); assert.deepStrictEqual(perMessageDeflate.accept([{}]), { client_max_window_bits: 10 }); }); }); }); describe('#compress and #decompress', () => { it('works with unfragmented messages', (done) => { const perMessageDeflate = new PerMessageDeflate(); const buf = Buffer.from([1, 2, 3]); perMessageDeflate.accept([{}]); perMessageDeflate.compress(buf, true, (err, data) => { if (err) return done(err); perMessageDeflate.decompress(data, true, (err, data) => { if (err) return done(err); assert.ok(data.equals(buf)); done(); }); }); }); it('works with fragmented messages', (done) => { const perMessageDeflate = new PerMessageDeflate(); const buf = Buffer.from([1, 2, 3, 4]); perMessageDeflate.accept([{}]); perMessageDeflate.compress(buf.slice(0, 2), false, (err, compressed1) => { if (err) return done(err); perMessageDeflate.compress(buf.slice(2), true, (err, compressed2) => { if (err) return done(err); perMessageDeflate.decompress(compressed1, false, (err, data1) => { if (err) return done(err); perMessageDeflate.decompress(compressed2, true, (err, data2) => { if (err) return done(err); assert.ok(Buffer.concat([data1, data2]).equals(buf)); done(); }); }); }); }); }); it('works with the negotiated parameters', (done) => { const perMessageDeflate = new PerMessageDeflate({ memLevel: 5, level: 9 }); const extensions = extension.parse( 'permessage-deflate; server_no_context_takeover; ' + 'client_no_context_takeover; server_max_window_bits=10; ' + 'client_max_window_bits=11' ); const buf = Buffer.from("Some compressible data, it's compressible."); perMessageDeflate.accept(extensions['permessage-deflate']); perMessageDeflate.compress(buf, true, (err, data) => { if (err) return done(err); perMessageDeflate.decompress(data, true, (err, data) => { if (err) return done(err); assert.ok(data.equals(buf)); done(); }); }); }); it('honors the `level` option', (done) => { const lev0 = new PerMessageDeflate({ zlibDeflateOptions: { level: 0 } }); const lev9 = new PerMessageDeflate({ zlibDeflateOptions: { level: 9 } }); const extensionStr = 'permessage-deflate; server_no_context_takeover; ' + 'client_no_context_takeover; server_max_window_bits=10; ' + 'client_max_window_bits=11'; const buf = Buffer.from("Some compressible data, it's compressible."); lev0.accept(extension.parse(extensionStr)['permessage-deflate']); lev9.accept(extension.parse(extensionStr)['permessage-deflate']); lev0.compress(buf, true, (err, compressed1) => { if (err) return done(err); lev0.decompress(compressed1, true, (err, decompressed1) => { if (err) return done(err); lev9.compress(buf, true, (err, compressed2) => { if (err) return done(err); lev9.decompress(compressed2, true, (err, decompressed2) => { if (err) return done(err); // Level 0 compression actually adds a few bytes due to headers. assert.ok(compressed1.length > buf.length); // Level 9 should not, of course. assert.ok(compressed2.length < buf.length); // Ensure they both decompress back properly. assert.ok(decompressed1.equals(buf)); assert.ok(decompressed2.equals(buf)); done(); }); }); }); }); }); it('honors the `zlib{Deflate,Inflate}Options` option', (done) => { const lev0 = new PerMessageDeflate({ zlibDeflateOptions: { level: 0, chunkSize: 256 }, zlibInflateOptions: { chunkSize: 2048 } }); const lev9 = new PerMessageDeflate({ zlibDeflateOptions: { level: 9, chunkSize: 128 }, zlibInflateOptions: { chunkSize: 1024 } }); // Note no context takeover so we can get a hold of the raw streams after // we do the dance. const extensionStr = 'permessage-deflate; server_max_window_bits=10; ' + 'client_max_window_bits=11'; const buf = Buffer.from("Some compressible data, it's compressible."); lev0.accept(extension.parse(extensionStr)['permessage-deflate']); lev9.accept(extension.parse(extensionStr)['permessage-deflate']); lev0.compress(buf, true, (err, compressed1) => { if (err) return done(err); lev0.decompress(compressed1, true, (err, decompressed1) => { if (err) return done(err); lev9.compress(buf, true, (err, compressed2) => { if (err) return done(err); lev9.decompress(compressed2, true, (err, decompressed2) => { if (err) return done(err); // Level 0 compression actually adds a few bytes due to headers. assert.ok(compressed1.length > buf.length); // Level 9 should not, of course. assert.ok(compressed2.length < buf.length); // Ensure they both decompress back properly. assert.ok(decompressed1.equals(buf)); assert.ok(decompressed2.equals(buf)); // Assert options were set. assert.ok(lev0._deflate._level === 0); assert.ok(lev9._deflate._level === 9); assert.ok(lev0._deflate._chunkSize === 256); assert.ok(lev9._deflate._chunkSize === 128); assert.ok(lev0._inflate._chunkSize === 2048); assert.ok(lev9._inflate._chunkSize === 1024); done(); }); }); }); }); }); it("doesn't use contex takeover if not allowed", (done) => { const perMessageDeflate = new PerMessageDeflate({}, true); const extensions = extension.parse( 'permessage-deflate;server_no_context_takeover' ); const buf = Buffer.from('foofoo'); perMessageDeflate.accept(extensions['permessage-deflate']); perMessageDeflate.compress(buf, true, (err, compressed1) => { if (err) return done(err); perMessageDeflate.decompress(compressed1, true, (err, data) => { if (err) return done(err); assert.ok(data.equals(buf)); perMessageDeflate.compress(data, true, (err, compressed2) => { if (err) return done(err); assert.strictEqual(compressed2.length, compressed1.length); perMessageDeflate.decompress(compressed2, true, (err, data) => { if (err) return done(err); assert.ok(data.equals(buf)); done(); }); }); }); }); }); it('uses contex takeover if allowed', (done) => { const perMessageDeflate = new PerMessageDeflate({}, true); const extensions = extension.parse('permessage-deflate'); const buf = Buffer.from('foofoo'); perMessageDeflate.accept(extensions['permessage-deflate']); perMessageDeflate.compress(buf, true, (err, compressed1) => { if (err) return done(err); perMessageDeflate.decompress(compressed1, true, (err, data) => { if (err) return done(err); assert.ok(data.equals(buf)); perMessageDeflate.compress(data, true, (err, compressed2) => { if (err) return done(err); assert.ok(compressed2.length < compressed1.length); perMessageDeflate.decompress(compressed2, true, (err, data) => { if (err) return done(err); assert.ok(data.equals(buf)); done(); }); }); }); }); }); it('calls the callback when an error occurs (inflate)', (done) => { const perMessageDeflate = new PerMessageDeflate(); const data = Buffer.from('something invalid'); perMessageDeflate.accept([{}]); perMessageDeflate.decompress(data, true, (err) => { assert.ok(err instanceof Error); assert.strictEqual(err.code, 'Z_DATA_ERROR'); assert.strictEqual(err.errno, -3); done(); }); }); it("doesn't call the callback twice when `maxPayload` is exceeded", (done) => { const perMessageDeflate = new PerMessageDeflate({}, false, 25); const buf = Buffer.from('A'.repeat(50)); perMessageDeflate.accept([{}]); perMessageDeflate.compress(buf, true, (err, data) => { if (err) return done(err); perMessageDeflate.decompress(data, true, (err) => { assert.ok(err instanceof RangeError); assert.strictEqual(err.message, 'Max payload size exceeded'); done(); }); }); }); it('calls the callback if the deflate stream is closed prematurely', (done) => { const perMessageDeflate = new PerMessageDeflate(); const buf = Buffer.from('A'.repeat(50)); perMessageDeflate.accept([{}]); perMessageDeflate.compress(buf, true, (err) => { assert.ok(err instanceof Error); assert.strictEqual( err.message, 'The deflate stream was closed while data was being processed' ); done(); }); process.nextTick(() => perMessageDeflate.cleanup()); }); it('recreates the inflate stream if it ends', (done) => { const perMessageDeflate = new PerMessageDeflate(); const extensions = extension.parse( 'permessage-deflate; client_no_context_takeover; ' + 'server_no_context_takeover' ); const buf = Buffer.from('33343236313533b7000000', 'hex'); const expected = Buffer.from('12345678'); perMessageDeflate.accept(extensions['permessage-deflate']); perMessageDeflate.decompress(buf, true, (err, data) => { assert.ok(data.equals(expected)); perMessageDeflate.decompress(buf, true, (err, data) => { assert.ok(data.equals(expected)); done(); }); }); }); }); });