From 7203924fea26c1bcba9774a23a9f54c642efccd6 Mon Sep 17 00:00:00 2001 From: Greg Alexander Date: Mon, 31 Jul 2017 17:08:44 -0500 Subject: [PATCH 01/97] util: implement %o and %O as formatting specifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementing the %o and %O formatting specifiers for util.format. Based on discussion in issue, this specifier should just call util.inspect to format the value. PR-URL: https://github.com/nodejs/node/pull/14558 Fixes: https://github.com/nodejs/node/issues/14545 Reviewed-By: Refael Ackermann Reviewed-By: Roman Reiss Reviewed-By: Vse Mozhet Byt Reviewed-By: James M Snell Reviewed-By: Evan Lucas Reviewed-By: Timothy Gu Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen Reviewed-By: Yuta Hiroto --- doc/api/util.md | 8 +++ lib/util.js | 11 +++++ test/parallel/test-util-format.js | 82 +++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/doc/api/util.md b/doc/api/util.md index 59f17ec6fea9ad..c03b3f769d78da 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -167,6 +167,14 @@ corresponding argument. Supported placeholders are: * `%f` - Floating point value. * `%j` - JSON. Replaced with the string `'[Circular]'` if the argument contains circular references. +* `%o` - Object. A string representation of an object + with generic JavaScript object formatting. + Similar to `util.inspect()` with options `{ showHidden: true, depth: 4, showProxy: true }`. + This will show the full object including non-enumerable symbols and properties. +* `%O` - Object. A string representation of an object + with generic JavaScript object formatting. + Similar to `util.inspect()` without options. + This will show the full object not including non-enumerable symbols and properties. * `%%` - single percent sign (`'%'`). This does not consume an argument. If the placeholder does not have a corresponding argument, the placeholder is diff --git a/lib/util.js b/lib/util.js index 9db58e5458d8db..44d708923a87ff 100644 --- a/lib/util.js +++ b/lib/util.js @@ -137,6 +137,17 @@ function format(f) { str += f.slice(lastPos, i); str += String(arguments[a++]); break; + case 79: // 'O' + if (lastPos < i) + str += f.slice(lastPos, i); + str += inspect(arguments[a++]); + break; + case 111: // 'o' + if (lastPos < i) + str += f.slice(lastPos, i); + str += inspect(arguments[a++], + { showHidden: true, depth: 4, showProxy: true }); + break; case 37: // '%' if (lastPos < i) str += f.slice(lastPos, i); diff --git a/test/parallel/test-util-format.js b/test/parallel/test-util-format.js index 4527bf40e441fa..649bb63942909f 100644 --- a/test/parallel/test-util-format.js +++ b/test/parallel/test-util-format.js @@ -101,6 +101,84 @@ assert.strictEqual(util.format('%j', '42'), '"42"'); assert.strictEqual(util.format('%j %j', 42, 43), '42 43'); assert.strictEqual(util.format('%j %j', 42), '42 %j'); +// Object format specifier +const obj = { + foo: 'bar', + foobar: 1, + func: function() {} +}; +const nestedObj = { + foo: 'bar', + foobar: { + foo: 'bar', + func: function() {} + } +}; +assert.strictEqual(util.format('%o'), '%o'); +assert.strictEqual(util.format('%o', 42), '42'); +assert.strictEqual(util.format('%o', 'foo'), '\'foo\''); +assert.strictEqual( + util.format('%o', obj), + '{ foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: \n' + + ' { [Function: func]\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular] } } }'); +assert.strictEqual( + util.format('%o', nestedObj), + '{ foo: \'bar\',\n' + + ' foobar: \n' + + ' { foo: \'bar\',\n' + + ' func: \n' + + ' { [Function: func]\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular] } } } }'); +assert.strictEqual( + util.format('%o %o', obj, obj), + '{ foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: \n' + + ' { [Function: func]\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular] } } }' + + ' { foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: \n' + + ' { [Function: func]\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular] } } }'); +assert.strictEqual( + util.format('%o %o', obj), + '{ foo: \'bar\',\n' + + ' foobar: 1,\n' + + ' func: \n' + + ' { [Function: func]\n' + + ' [length]: 0,\n' + + ' [name]: \'func\',\n' + + ' [prototype]: func { [constructor]: [Circular] } } } %o'); + +assert.strictEqual(util.format('%O'), '%O'); +assert.strictEqual(util.format('%O', 42), '42'); +assert.strictEqual(util.format('%O', 'foo'), '\'foo\''); +assert.strictEqual( + util.format('%O', obj), + '{ foo: \'bar\', foobar: 1, func: [Function: func] }'); +assert.strictEqual( + util.format('%O', nestedObj), + '{ foo: \'bar\', foobar: { foo: \'bar\', func: [Function: func] } }'); +assert.strictEqual( + util.format('%O %O', obj, obj), + '{ foo: \'bar\', foobar: 1, func: [Function: func] } ' + + '{ foo: \'bar\', foobar: 1, func: [Function: func] }'); +assert.strictEqual( + util.format('%O %O', obj), + '{ foo: \'bar\', foobar: 1, func: [Function: func] } %O'); + // Various format specifiers assert.strictEqual(util.format('%%s%s', 'foo'), '%sfoo'); assert.strictEqual(util.format('%s:%s'), '%s:%s'); @@ -125,6 +203,10 @@ assert.strictEqual(util.format('%f:%f'), '%f:%f'); assert.strictEqual(util.format('o: %j, a: %j', {}, []), 'o: {}, a: []'); assert.strictEqual(util.format('o: %j, a: %j', {}), 'o: {}, a: %j'); assert.strictEqual(util.format('o: %j, a: %j'), 'o: %j, a: %j'); +assert.strictEqual(util.format('o: %o, a: %O', {}, []), 'o: {}, a: []'); +assert.strictEqual(util.format('o: %o, a: %o', {}), 'o: {}, a: %o'); +assert.strictEqual(util.format('o: %O, a: %O'), 'o: %O, a: %O'); + // Invalid format specifiers assert.strictEqual(util.format('a% b', 'x'), 'a% b x'); From 96147c980c1057ff0323c517ff941ac1be5c7664 Mon Sep 17 00:00:00 2001 From: Bartosz Sosnowski Date: Wed, 2 Aug 2017 15:05:52 +0200 Subject: [PATCH 02/97] test: read proper inspector message size Fix a bug when messages bigger than 64kb where incorrectly parsed by the inspector-helper. PR-URL: https://github.com/nodejs/node/pull/14596 Fixes: https://github.com/nodejs/node/issues/14507 Reviewed-By: Luigi Pinca Reviewed-By: Eugene Ostroukhov Reviewed-By: Refael Ackermann Reviewed-By: Colin Ihrig Reviewed-By: James M Snell --- test/inspector/inspector-helper.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/inspector/inspector-helper.js b/test/inspector/inspector-helper.js index f577717143cbd7..dd58d2a67e5817 100644 --- a/test/inspector/inspector-helper.js +++ b/test/inspector/inspector-helper.js @@ -53,6 +53,7 @@ function sendEnd(socket) { } function parseWSFrame(buffer, handler) { + // Protocol described in https://tools.ietf.org/html/rfc6455#section-5 if (buffer.length < 2) return 0; if (buffer[0] === 0x88 && buffer[1] === 0x00) { @@ -68,7 +69,8 @@ function parseWSFrame(buffer, handler) { dataLen = buffer.readUInt16BE(2); bodyOffset = 4; } else if (dataLen === 127) { - dataLen = buffer.readUInt32BE(2); + assert(buffer[2] === 0 && buffer[3] === 0, 'Inspector message too big'); + dataLen = buffer.readUIntBE(4, 6); bodyOffset = 10; } if (buffer.length < bodyOffset + dataLen) From d89f9f82b0357636f75154be29f98d05649654da Mon Sep 17 00:00:00 2001 From: Jan Krems Date: Wed, 28 Sep 2016 13:20:54 -0700 Subject: [PATCH 03/97] inspector: allow require in Runtime.evaluate Some parts were written by Timothy Gu . PR-URL: https://github.com/nodejs/node/pull/8837 Reviewed-By: Jeremiah Senkpiel Reviewed-By: James M Snell Reviewed-By: Aleksei Koziatinskii Reviewed-By: Ben Noordhuis Reviewed-By: Timothy Gu --- lib/internal/bootstrap_node.js | 17 ++++ src/env.h | 1 + src/inspector_agent.cc | 37 ++++++++ test/inspector/test-inspector.js | 142 +++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 128be6f1e8a360..9b56faa75b6158 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -282,6 +282,7 @@ return console; } }); + setupInspectorCommandLineAPI(); } function installInspectorConsole(globalConsole) { @@ -310,6 +311,22 @@ return wrappedConsole; } + function setupInspectorCommandLineAPI() { + const { addCommandLineAPI } = process.binding('inspector'); + if (!addCommandLineAPI) return; + + const Module = NativeModule.require('module'); + const { makeRequireFunction } = NativeModule.require('internal/module'); + const path = NativeModule.require('path'); + const cwd = tryGetCwd(path); + + const consoleAPIModule = new Module(''); + consoleAPIModule.paths = + Module._nodeModulePaths(cwd).concat(Module.globalPaths); + + addCommandLineAPI('require', makeRequireFunction(consoleAPIModule)); + } + function setupProcessFatal() { const async_wrap = process.binding('async_wrap'); // Arrays containing hook flags and ids for async_hook calls. diff --git a/src/env.h b/src/env.h index e1375080d17f07..ae8deb5e04f960 100644 --- a/src/env.h +++ b/src/env.h @@ -276,6 +276,7 @@ namespace node { V(context, v8::Context) \ V(domain_array, v8::Array) \ V(domains_stack_array, v8::Array) \ + V(inspector_console_api_object, v8::Object) \ V(jsstream_constructor_template, v8::FunctionTemplate) \ V(module_load_list_array, v8::Array) \ V(pbkdf2_constructor_template, v8::ObjectTemplate) \ diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 4d51c3d140f941..bedf74f3b02f61 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -22,6 +22,7 @@ namespace node { namespace inspector { namespace { +using v8::Array; using v8::Context; using v8::External; using v8::Function; @@ -554,6 +555,20 @@ class NodeInspectorClient : public V8InspectorClient { return env_->context(); } + void installAdditionalCommandLineAPI(Local context, + Local target) override { + Local console_api = env_->inspector_console_api_object(); + + Local properties = + console_api->GetOwnPropertyNames(context).ToLocalChecked(); + for (uint32_t i = 0; i < properties->Length(); ++i) { + Local key = properties->Get(context, i).ToLocalChecked(); + target->Set(context, + key, + console_api->Get(context, key).ToLocalChecked()).FromJust(); + } + } + void FatalException(Local error, Local message) { Local context = env_->context(); @@ -682,6 +697,20 @@ bool Agent::StartIoThread(bool wait_for_connect) { return true; } +static void AddCommandLineAPI( + const FunctionCallbackInfo& info) { + auto env = Environment::GetCurrent(info); + Local context = env->context(); + + if (info.Length() != 2 || !info[0]->IsString()) { + return env->ThrowTypeError("inspector.addCommandLineAPI takes " + "exactly 2 arguments: a string and a value."); + } + + Local console_api = env->inspector_console_api_object(); + console_api->Set(context, info[0], info[1]).FromJust(); +} + void Agent::Stop() { if (io_ != nullptr) { io_->Stop(); @@ -784,8 +813,16 @@ void Url(const FunctionCallbackInfo& args) { void Agent::InitInspector(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); + { + auto obj = Object::New(env->isolate()); + auto null = Null(env->isolate()); + CHECK(obj->SetPrototype(context, null).FromJust()); + env->set_inspector_console_api_object(obj); + } + Agent* agent = env->inspector_agent(); env->SetMethod(target, "consoleCall", InspectorConsoleCall); + env->SetMethod(target, "addCommandLineAPI", AddCommandLineAPI); if (agent->debug_options_.wait_for_connect()) env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart); env->SetMethod(target, "connect", ConnectJSBindingsSession); diff --git a/test/inspector/test-inspector.js b/test/inspector/test-inspector.js index d38240c0e67ad0..4b87e0263672d1 100644 --- a/test/inspector/test-inspector.js +++ b/test/inspector/test-inspector.js @@ -34,6 +34,11 @@ function checkBadPath(err, response) { assert(/WebSockets request was expected/.test(err.response)); } +function checkException(message) { + assert.strictEqual(message['exceptionDetails'], undefined, + 'An exception occurred during execution'); +} + function expectMainScriptSource(result) { const expected = helper.mainScriptSource(); const source = result['scriptSource']; @@ -209,6 +214,142 @@ function testI18NCharacters(session) { ]); } +function testCommandLineAPI(session) { + const testModulePath = require.resolve('../fixtures/empty.js'); + const testModuleStr = JSON.stringify(testModulePath); + const printAModulePath = require.resolve('../fixtures/printA.js'); + const printAModuleStr = JSON.stringify(printAModulePath); + const printBModulePath = require.resolve('../fixtures/printB.js'); + const printBModuleStr = JSON.stringify(printBModulePath); + session.sendInspectorCommands([ + [ // we can use `require` outside of a callframe with require in scope + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': 'typeof require("fs").readFile === "function"', + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } + ], + [ // the global require has the same properties as a normal `require` + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': [ + 'typeof require.resolve === "function"', + 'typeof require.extensions === "object"', + 'typeof require.cache === "object"' + ].join(' && '), + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } + ], + [ // `require` twice returns the same value + { + 'method': 'Runtime.evaluate', 'params': { + // 1. We require the same module twice + // 2. We mutate the exports so we can compare it later on + 'expression': ` + Object.assign( + require(${testModuleStr}), + { old: 'yes' } + ) === require(${testModuleStr})`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } + ], + [ // after require the module appears in require.cache + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify( + require.cache[${testModuleStr}].exports + )`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), + { old: 'yes' }); + } + ], + [ // remove module from require.cache + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `delete require.cache[${testModuleStr}]`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } + ], + [ // require again, should get fresh (empty) exports + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify(require(${testModuleStr}))`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); + } + ], + [ // require 2nd module, exports an empty object + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify(require(${printAModuleStr}))`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); + } + ], + [ // both modules end up with the same module.parent + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify({ + parentsEqual: + require.cache[${testModuleStr}].parent === + require.cache[${printAModuleStr}].parent, + parentId: require.cache[${testModuleStr}].parent.id, + })`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), { + parentsEqual: true, + parentId: '' + }); + } + ], + [ // the `require` in the module shadows the command line API's `require` + { + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `( + require(${printBModuleStr}), + require.cache[${printBModuleStr}].parent.id + )`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.notStrictEqual(message['result']['value'], + ''); + } + ], + ]); +} + function testWaitsForFrontendDisconnect(session, harness) { console.log('[test]', 'Verify node waits for the frontend to disconnect'); session.sendInspectorCommands({ 'method': 'Debugger.resume'}) @@ -231,6 +372,7 @@ function runTests(harness) { testSetBreakpointAndResume, testInspectScope, testI18NCharacters, + testCommandLineAPI, testWaitsForFrontendDisconnect ]).expectShutDown(55); } From f73f659186b715671955e16fd148094fd1212be1 Mon Sep 17 00:00:00 2001 From: Aditya Anand Date: Thu, 3 Aug 2017 19:54:36 +0530 Subject: [PATCH 04/97] test: mitigate RegEx exceeding 80 chars Format commit wrapping lines containing RegEx and exceeding 80 chars. PR-URL: https://github.com/nodejs/node/pull/14607 Fixes: https://github.com/nodejs/node/issues/14586 Reviewed-By: Refael Ackermann Reviewed-By: Rich Trott Reviewed-By: Vse Mozhet Byt Reviewed-By: Sakthipriyan Vairamani --- test/addons-napi/test_properties/test.js | 3 ++- test/parallel/test-process-versions.js | 3 ++- test/parallel/test-repl.js | 21 +++++++++++-------- test/parallel/test-v8-serdes.js | 16 +++++++------- test/parallel/test-whatwg-url-properties.js | 12 +++++++---- .../test-zlib-not-string-or-buffer.js | 3 ++- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/test/addons-napi/test_properties/test.js b/test/addons-napi/test_properties/test.js index 0a3bbee853610f..13a546c7b87f81 100644 --- a/test/addons-napi/test_properties/test.js +++ b/test/addons-napi/test_properties/test.js @@ -1,7 +1,8 @@ 'use strict'; const common = require('../../common'); const assert = require('assert'); -const readonlyErrorRE = /^TypeError: Cannot assign to read only property '.*' of object '#'$/; +const readonlyErrorRE = + /^TypeError: Cannot assign to read only property '.*' of object '#'$/; // Testing api calls for defining properties const test_object = require(`./build/${common.buildType}/test_properties`); diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js index 02eeb93cb1779e..2a9a676e8d6c2a 100644 --- a/test/parallel/test-process-versions.js +++ b/test/parallel/test-process-versions.js @@ -29,5 +29,6 @@ assert(commonTemplate.test(process.versions.node)); assert(commonTemplate.test(process.versions.uv)); assert(commonTemplate.test(process.versions.zlib)); -assert(/^\d+\.\d+\.\d+(?:\.\d+)?(?: \(candidate\))?$/.test(process.versions.v8)); +assert(/^\d+\.\d+\.\d+(?:\.\d+)?(?: \(candidate\))?$/ + .test(process.versions.v8)); assert(/^\d+$/.test(process.versions.modules)); diff --git a/test/parallel/test-repl.js b/test/parallel/test-repl.js index f5df2d0ef40e09..d2ff29a4d12e89 100644 --- a/test/parallel/test-repl.js +++ b/test/parallel/test-repl.js @@ -142,7 +142,7 @@ function error_test() { expect: prompt_unix }, // But passing the same string to eval() should throw { client: client_unix, send: 'eval("function test_func() {")', - expect: /\bSyntaxError: Unexpected end of input/ }, + expect: /^SyntaxError: Unexpected end of input/ }, // Can handle multiline template literals { client: client_unix, send: '`io.js', expect: prompt_multiline }, @@ -171,22 +171,22 @@ function error_test() { // invalid input to JSON.parse error is special case of syntax error, // should throw { client: client_unix, send: 'JSON.parse(\'{invalid: \\\'json\\\'}\');', - expect: /\bSyntaxError: Unexpected token i/ }, + expect: /^SyntaxError: Unexpected token i/ }, // end of input to JSON.parse error is special case of syntax error, // should throw { client: client_unix, send: 'JSON.parse(\'066\');', - expect: /\bSyntaxError: Unexpected number/ }, + expect: /^SyntaxError: Unexpected number/ }, // should throw { client: client_unix, send: 'JSON.parse(\'{\');', - expect: /\bSyntaxError: Unexpected end of JSON input/ }, + expect: /^SyntaxError: Unexpected end of JSON input/ }, // invalid RegExps are a special case of syntax error, // should throw { client: client_unix, send: '/(/;', - expect: /\bSyntaxError: Invalid regular expression:/ }, + expect: /^SyntaxError: Invalid regular expression:/ }, // invalid RegExp modifiers are a special case of syntax error, // should throw (GH-4012) { client: client_unix, send: 'new RegExp("foo", "wrong modifier");', - expect: /\bSyntaxError: Invalid flags supplied to RegExp constructor/ }, + expect: /^SyntaxError: Invalid flags supplied to RegExp constructor/ }, // strict mode syntax errors should be caught (GH-5178) { client: client_unix, send: '(function() { "use strict"; return 0755; })()', @@ -194,7 +194,8 @@ function error_test() { { client: client_unix, send: '(function(a, a, b) { "use strict"; return a + b + c; })()', - expect: /\bSyntaxError: Duplicate parameter name not allowed in this context/ + expect: + /\bSyntaxError: Duplicate parameter name not allowed in this context/ }, { client: client_unix, @@ -204,7 +205,8 @@ function error_test() { { client: client_unix, send: '(function() { "use strict"; var x; delete x; })()', - expect: /\bSyntaxError: Delete of an unqualified identifier in strict mode/ + expect: + /\bSyntaxError: Delete of an unqualified identifier in strict mode/ }, { client: client_unix, send: '(function() { "use strict"; eval = 17; })()', @@ -212,7 +214,8 @@ function error_test() { { client: client_unix, send: '(function() { "use strict"; if (true) function f() { } })()', - expect: /\bSyntaxError: In strict mode code, functions can only be declared at top level or inside a block\./ + expect: + /\bSyntaxError: In strict mode code, functions can only be declared at top level or inside a block\./ }, // Named functions can be used: { client: client_unix, send: 'function blah() { return 1; }', diff --git a/test/parallel/test-v8-serdes.js b/test/parallel/test-v8-serdes.js index f59970209b5be5..981e196157812f 100644 --- a/test/parallel/test-v8-serdes.js +++ b/test/parallel/test-v8-serdes.js @@ -20,6 +20,11 @@ const objects = [ circular ]; +const serializerTypeError = + /^TypeError: Class constructor Serializer cannot be invoked without 'new'$/; +const deserializerTypeError = + /^TypeError: Class constructor Deserializer cannot be invoked without 'new'$/; + { const ser = new v8.DefaultSerializer(); ser.writeHeader(); @@ -133,13 +138,6 @@ const objects = [ } { - assert.throws( - () => { v8.Serializer(); }, - /^TypeError: Class constructor Serializer cannot be invoked without 'new'$/ - ); - - assert.throws( - () => { v8.Deserializer(); }, - /^TypeError: Class constructor Deserializer cannot be invoked without 'new'$/ - ); + assert.throws(v8.Serializer, serializerTypeError); + assert.throws(v8.Deserializer, deserializerTypeError); } diff --git a/test/parallel/test-whatwg-url-properties.js b/test/parallel/test-whatwg-url-properties.js index 747206a5e07dc1..d6caae511aed47 100644 --- a/test/parallel/test-whatwg-url-properties.js +++ b/test/parallel/test-whatwg-url-properties.js @@ -43,8 +43,10 @@ assert.strictEqual(url.searchParams, oldParams); // [SameObject] // searchParams is readonly. Under strict mode setting a // non-writable property should throw. // Note: this error message is subject to change in V8 updates -assert.throws(() => url.origin = 'http://foo.bar.com:22', - /TypeError: Cannot set property origin of \[object URL\] which has only a getter$/); +assert.throws( + () => url.origin = 'http://foo.bar.com:22', + /^TypeError: Cannot set property origin of \[object URL\] which has only a getter$/ +); assert.strictEqual(url.origin, 'http://foo.bar.com:21'); assert.strictEqual(url.toString(), 'http://user:pass@foo.bar.com:21/aaa/zzz?l=25#test'); @@ -118,8 +120,10 @@ assert.strictEqual(url.hash, '#abcd'); // searchParams is readonly. Under strict mode setting a // non-writable property should throw. // Note: this error message is subject to change in V8 updates -assert.throws(() => url.searchParams = '?k=88', - /^TypeError: Cannot set property searchParams of \[object URL\] which has only a getter$/); +assert.throws( + () => url.searchParams = '?k=88', + /^TypeError: Cannot set property searchParams of \[object URL\] which has only a getter$/ +); assert.strictEqual(url.searchParams, oldParams); assert.strictEqual(url.toString(), 'https://user2:pass2@foo.bar.org:23/aaa/bbb?k=99#abcd'); diff --git a/test/parallel/test-zlib-not-string-or-buffer.js b/test/parallel/test-zlib-not-string-or-buffer.js index 9bbee0178650ac..16f127b3e557d3 100644 --- a/test/parallel/test-zlib-not-string-or-buffer.js +++ b/test/parallel/test-zlib-not-string-or-buffer.js @@ -7,7 +7,8 @@ require('../common'); const assert = require('assert'); const zlib = require('zlib'); -const expected = /^TypeError: "buffer" argument must be a string, Buffer, TypedArray, or DataView$/; +const expected = + /^TypeError: "buffer" argument must be a string, Buffer, TypedArray, or DataView$/; assert.throws(() => { zlib.deflateSync(undefined); }, expected); assert.throws(() => { zlib.deflateSync(null); }, expected); From 71f2e763534b7d60fdbffacdd13e96e04a2a9acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Lal?= Date: Tue, 1 Aug 2017 10:45:34 +0200 Subject: [PATCH 05/97] test: use ciphers supported by shared OpenSSL On Debian with OpenSSL 1.1 CLI, the ciphers used in those tests were unknown. PR-URL: https://github.com/nodejs/node/pull/14566 Reviewed-By: Ben Noordhuis Reviewed-By: Colin Ihrig Reviewed-By: James M Snell --- test/parallel/test-tls-ecdh-disable.js | 2 +- test/parallel/test-tls-set-ciphers.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-tls-ecdh-disable.js b/test/parallel/test-tls-ecdh-disable.js index d5827d0acca3f0..24ebeb37605115 100644 --- a/test/parallel/test-tls-ecdh-disable.js +++ b/test/parallel/test-tls-ecdh-disable.js @@ -35,7 +35,7 @@ const fs = require('fs'); const options = { key: fs.readFileSync(`${common.fixturesDir}/keys/agent2-key.pem`), cert: fs.readFileSync(`${common.fixturesDir}/keys/agent2-cert.pem`), - ciphers: 'ECDHE-RSA-RC4-SHA', + ciphers: 'ECDHE-RSA-AES128-SHA', ecdhCurve: false }; diff --git a/test/parallel/test-tls-set-ciphers.js b/test/parallel/test-tls-set-ciphers.js index 1a41f6df139bb2..5fb437e9bb661a 100644 --- a/test/parallel/test-tls-set-ciphers.js +++ b/test/parallel/test-tls-set-ciphers.js @@ -36,7 +36,7 @@ const fs = require('fs'); const options = { key: fs.readFileSync(`${common.fixturesDir}/keys/agent2-key.pem`), cert: fs.readFileSync(`${common.fixturesDir}/keys/agent2-cert.pem`), - ciphers: 'DES-CBC3-SHA' + ciphers: 'AES256-SHA' }; const reply = 'I AM THE WALRUS'; // something recognizable From 5e2cce59efe26255bbd7d6f51be2f92bc05d2614 Mon Sep 17 00:00:00 2001 From: Jason Ginchereau Date: Tue, 18 Jul 2017 20:22:35 -0700 Subject: [PATCH 06/97] n-api: optimize number API performance - Add separate APIs for creating different kinds of numbers, because creating a V8 number value from an integer is faster than creating one from a double. - When getting number values, avoid getting the current context because the context will not actually be used and is expensive to obtain. - When creating values, don't use v8::TryCatch (NAPI_PREAMBLE), because these functions have no possibility of executing JS code. Refs: https://github.com/nodejs/node/issues/14379 PR-URL: https://github.com/nodejs/node/pull/14573 Reviewed-By: Timothy Gu Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig --- doc/api/n-api.md | 85 ++++++++++-- src/node_api.cc | 131 +++++++++++------- src/node_api.h | 11 +- .../2_function_arguments/binding.c | 2 +- test/addons-napi/6_object_wrap/myobject.cc | 6 +- test/addons-napi/7_factory_wrap/myobject.cc | 4 +- test/addons-napi/7_factory_wrap/myobject.h | 2 +- test/addons-napi/8_passing_wrapped/binding.cc | 2 +- test/addons-napi/test_async/test_async.cc | 2 +- test/addons-napi/test_buffer/test_buffer.c | 2 +- .../test_constructor/test_constructor.c | 6 +- .../test_conversions/test_conversions.c | 8 +- test/addons-napi/test_general/test_general.c | 2 +- test/addons-napi/test_number/test_number.c | 4 +- test/addons-napi/test_object/test_object.c | 4 +- .../test_properties/test_properties.c | 4 +- .../test_reference/test_reference.c | 6 +- test/addons-napi/test_string/test_string.c | 4 +- 18 files changed, 197 insertions(+), 88 deletions(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 1f3476683e728d..af3696fd10b953 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -1372,12 +1372,81 @@ JavaScript DataView Objects are described in [Section 24.3][] of the ECMAScript Language Specification. ### Functions to convert from C types to N-API -#### *napi_create_number* +#### *napi_create_int32* +```C +napi_status napi_create_int32(napi_env env, int32_t value, napi_value* result) +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] value`: Integer value to be represented in JavaScript. +- `[out] result`: A `napi_value` representing a JavaScript Number. + +Returns `napi_ok` if the API succeeded. + +This API is used to convert from the C `int32_t` type to the JavaScript +Number type. + +The JavaScript Number type is described in +[Section 6.1.6](https://tc39.github.io/ecma262/#sec-ecmascript-language-types-number-type) +of the ECMAScript Language Specification. + +#### *napi_create_uint32* + +```C +napi_status napi_create_uint32(napi_env env, uint32_t value, napi_value* result) +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] value`: Unsigned integer value to be represented in JavaScript. +- `[out] result`: A `napi_value` representing a JavaScript Number. + +Returns `napi_ok` if the API succeeded. + +This API is used to convert from the C `uint32_t` type to the JavaScript +Number type. + +The JavaScript Number type is described in +[Section 6.1.6](https://tc39.github.io/ecma262/#sec-ecmascript-language-types-number-type) +of the ECMAScript Language Specification. + +#### *napi_create_int64* + +```C +napi_status napi_create_int64(napi_env env, int64_t value, napi_value* result) +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] value`: Integer value to be represented in JavaScript. +- `[out] result`: A `napi_value` representing a JavaScript Number. + +Returns `napi_ok` if the API succeeded. + +This API is used to convert from the C `int64_t` type to the JavaScript +Number type. + +The JavaScript Number type is described in +[Section 6.1.6](https://tc39.github.io/ecma262/#sec-ecmascript-language-types-number-type) +of the ECMAScript Language Specification. Note the complete range of `int64_t` +cannot be represented with full precision in JavaScript. Integer values +outside the range of +[`Number.MIN_SAFE_INTEGER`](https://tc39.github.io/ecma262/#sec-number.min_safe_integer) +-(2^53 - 1) - +[`Number.MAX_SAFE_INTEGER`](https://tc39.github.io/ecma262/#sec-number.max_safe_integer) +(2^53 - 1) will lose precision. + +#### *napi_create_double* + ```C -napi_status napi_create_number(napi_env env, double value, napi_value* result) +napi_status napi_create_double(napi_env env, double value, napi_value* result) ``` - `[in] env`: The environment that the API is invoked under. @@ -1386,7 +1455,7 @@ napi_status napi_create_number(napi_env env, double value, napi_value* result) Returns `napi_ok` if the API succeeded. -This API is used to convert from the C double type to the JavaScript +This API is used to convert from the C `double` type to the JavaScript Number type. The JavaScript Number type is described in @@ -2170,7 +2239,7 @@ status = napi_create_object(env, &obj); if (status != napi_ok) return status; // Create a napi_value for 123 -status = napi_create_number(env, 123, &value); +status = napi_create_int32(env, 123, &value); if (status != napi_ok) return status; // obj.myProp = 123 @@ -2244,9 +2313,9 @@ if (status != napi_ok) return status; // Create napi_values for 123 and 456 napi_value fooValue, barValue; -status = napi_create_number(env, 123, &fooValue); +status = napi_create_int32(env, 123, &fooValue); if (status != napi_ok) return status; -status = napi_create_number(env, 456, &barValue); +status = napi_create_int32(env, 456, &barValue); if (status != napi_ok) return status; // Set the properties @@ -2707,7 +2776,7 @@ status = napi_get_named_property(env, global, "AddTwo", &add_two); if (status != napi_ok) return; // const arg = 1337 -status = napi_create_number(env, 1337, &arg); +status = napi_create_int32(env, 1337, &arg); if (status != napi_ok) return; napi_value* argv = &arg; diff --git a/src/node_api.cc b/src/node_api.cc index 6e9f9bbc07e176..32acced231d68c 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -818,9 +818,7 @@ const char* error_messages[] = {nullptr, "The async work item was cancelled", "napi_escape_handle already called on scope"}; -static napi_status napi_clear_last_error(napi_env env) { - CHECK_ENV(env); - +static inline napi_status napi_clear_last_error(napi_env env) { env->last_error.error_code = napi_ok; // TODO(boingoing): Should this be a callback? @@ -829,13 +827,13 @@ static napi_status napi_clear_last_error(napi_env env) { return napi_ok; } -static napi_status napi_set_last_error(napi_env env, napi_status error_code, - uint32_t engine_error_code, - void* engine_reserved) { +static inline +napi_status napi_set_last_error(napi_env env, napi_status error_code, + uint32_t engine_error_code, + void* engine_reserved) { env->last_error.error_code = error_code; env->last_error.engine_error_code = engine_error_code; env->last_error.engine_reserved = engine_reserved; - return error_code; } @@ -1436,42 +1434,42 @@ napi_status napi_get_prototype(napi_env env, } napi_status napi_create_object(napi_env env, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); *result = v8impl::JsValueFromV8LocalValue( v8::Object::New(env->isolate)); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_create_array(napi_env env, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); *result = v8impl::JsValueFromV8LocalValue( v8::Array::New(env->isolate)); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_create_array_with_length(napi_env env, size_t length, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); *result = v8impl::JsValueFromV8LocalValue( v8::Array::New(env->isolate, length)); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_create_string_latin1(napi_env env, const char* str, size_t length, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); auto isolate = env->isolate; @@ -1483,28 +1481,28 @@ napi_status napi_create_string_latin1(napi_env env, CHECK_MAYBE_EMPTY(env, str_maybe, napi_generic_failure); *result = v8impl::JsValueFromV8LocalValue(str_maybe.ToLocalChecked()); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_create_string_utf8(napi_env env, const char* str, size_t length, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); v8::Local s; CHECK_NEW_FROM_UTF8_LEN(env, s, str, length); *result = v8impl::JsValueFromV8LocalValue(s); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_create_string_utf16(napi_env env, const char16_t* str, size_t length, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); auto isolate = env->isolate; @@ -1516,19 +1514,55 @@ napi_status napi_create_string_utf16(napi_env env, CHECK_MAYBE_EMPTY(env, str_maybe, napi_generic_failure); *result = v8impl::JsValueFromV8LocalValue(str_maybe.ToLocalChecked()); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } -napi_status napi_create_number(napi_env env, +napi_status napi_create_double(napi_env env, double value, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); *result = v8impl::JsValueFromV8LocalValue( v8::Number::New(env->isolate, value)); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); +} + +napi_status napi_create_int32(napi_env env, + int32_t value, + napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue( + v8::Integer::New(env->isolate, value)); + + return napi_clear_last_error(env); +} + +napi_status napi_create_uint32(napi_env env, + uint32_t value, + napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue( + v8::Integer::NewFromUnsigned(env->isolate, value)); + + return napi_clear_last_error(env); +} + +napi_status napi_create_int64(napi_env env, + int64_t value, + napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + + *result = v8impl::JsValueFromV8LocalValue( + v8::Number::New(env->isolate, static_cast(value))); + + return napi_clear_last_error(env); } napi_status napi_get_boolean(napi_env env, bool value, napi_value* result) { @@ -1549,7 +1583,7 @@ napi_status napi_get_boolean(napi_env env, bool value, napi_value* result) { napi_status napi_create_symbol(napi_env env, napi_value description, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, result); v8::Isolate* isolate = env->isolate; @@ -1564,7 +1598,7 @@ napi_status napi_create_symbol(napi_env env, v8::Symbol::New(isolate, desc.As())); } - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } static napi_status set_error_code(napi_env env, @@ -1624,7 +1658,7 @@ napi_status napi_create_error(napi_env env, napi_value code, napi_value msg, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, msg); CHECK_ARG(env, result); @@ -1638,14 +1672,14 @@ napi_status napi_create_error(napi_env env, *result = v8impl::JsValueFromV8LocalValue(error_obj); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_create_type_error(napi_env env, napi_value code, napi_value msg, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, msg); CHECK_ARG(env, result); @@ -1659,14 +1693,14 @@ napi_status napi_create_type_error(napi_env env, *result = v8impl::JsValueFromV8LocalValue(error_obj); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_create_range_error(napi_env env, napi_value code, napi_value msg, napi_value* result) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, msg); CHECK_ARG(env, result); @@ -1680,7 +1714,7 @@ napi_status napi_create_range_error(napi_env env, *result = v8impl::JsValueFromV8LocalValue(error_obj); - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_typeof(napi_env env, @@ -1951,14 +1985,13 @@ napi_status napi_get_value_int32(napi_env env, if (val->IsInt32()) { *result = val.As()->Value(); - return napi_clear_last_error(env); - } - - RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); + } else { + RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); - v8::Isolate* isolate = env->isolate; - v8::Local context = isolate->GetCurrentContext(); - *result = val->Int32Value(context).FromJust(); + // Empty context: https://github.com/nodejs/node/issues/14379 + v8::Local context; + *result = val->Int32Value(context).FromJust(); + } return napi_clear_last_error(env); } @@ -1976,14 +2009,13 @@ napi_status napi_get_value_uint32(napi_env env, if (val->IsUint32()) { *result = val.As()->Value(); - return napi_clear_last_error(env); - } - - RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); + } else { + RETURN_STATUS_IF_FALSE(env, val->IsNumber(), napi_number_expected); - v8::Isolate* isolate = env->isolate; - v8::Local context = isolate->GetCurrentContext(); - *result = val->Uint32Value(context).FromJust(); + // Empty context: https://github.com/nodejs/node/issues/14379 + v8::Local context; + *result = val->Uint32Value(context).FromJust(); + } return napi_clear_last_error(env); } @@ -2013,8 +2045,8 @@ napi_status napi_get_value_int64(napi_env env, if (std::isnan(doubleValue)) { *result = 0; } else { - v8::Isolate* isolate = env->isolate; - v8::Local context = isolate->GetCurrentContext(); + // Empty context: https://github.com/nodejs/node/issues/14379 + v8::Local context; *result = val->IntegerValue(context).FromJust(); } @@ -2793,11 +2825,10 @@ napi_status napi_get_buffer_info(napi_env env, napi_value value, void** data, size_t* length) { - NAPI_PREAMBLE(env); + CHECK_ENV(env); CHECK_ARG(env, value); - v8::Local buffer = - v8impl::V8LocalValueFromJsValue(value).As(); + v8::Local buffer = v8impl::V8LocalValueFromJsValue(value); if (data != nullptr) { *data = node::Buffer::Data(buffer); @@ -2806,7 +2837,7 @@ napi_status napi_get_buffer_info(napi_env env, *length = node::Buffer::Length(buffer); } - return GET_RETURN_STATUS(env); + return napi_clear_last_error(env); } napi_status napi_is_arraybuffer(napi_env env, napi_value value, bool* result) { diff --git a/src/node_api.h b/src/node_api.h index 5a2ba946ec8114..cf4128be210fe7 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -127,9 +127,18 @@ NAPI_EXTERN napi_status napi_create_array(napi_env env, napi_value* result); NAPI_EXTERN napi_status napi_create_array_with_length(napi_env env, size_t length, napi_value* result); -NAPI_EXTERN napi_status napi_create_number(napi_env env, +NAPI_EXTERN napi_status napi_create_double(napi_env env, double value, napi_value* result); +NAPI_EXTERN napi_status napi_create_int32(napi_env env, + int32_t value, + napi_value* result); +NAPI_EXTERN napi_status napi_create_uint32(napi_env env, + uint32_t value, + napi_value* result); +NAPI_EXTERN napi_status napi_create_int64(napi_env env, + int64_t value, + napi_value* result); NAPI_EXTERN napi_status napi_create_string_latin1(napi_env env, const char* str, size_t length, diff --git a/test/addons-napi/2_function_arguments/binding.c b/test/addons-napi/2_function_arguments/binding.c index a5ccac7b61c458..92f89fd2ffa199 100644 --- a/test/addons-napi/2_function_arguments/binding.c +++ b/test/addons-napi/2_function_arguments/binding.c @@ -24,7 +24,7 @@ napi_value Add(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_double(env, args[1], &value1)); napi_value sum; - NAPI_CALL(env, napi_create_number(env, value0 + value1, &sum)); + NAPI_CALL(env, napi_create_double(env, value0 + value1, &sum)); return sum; } diff --git a/test/addons-napi/6_object_wrap/myobject.cc b/test/addons-napi/6_object_wrap/myobject.cc index c2557facc06382..56b00ddae49a32 100644 --- a/test/addons-napi/6_object_wrap/myobject.cc +++ b/test/addons-napi/6_object_wrap/myobject.cc @@ -86,7 +86,7 @@ napi_value MyObject::GetValue(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); napi_value num; - NAPI_CALL(env, napi_create_number(env, obj->value_, &num)); + NAPI_CALL(env, napi_create_double(env, obj->value_, &num)); return num; } @@ -116,7 +116,7 @@ napi_value MyObject::PlusOne(napi_env env, napi_callback_info info) { obj->value_ += 1; napi_value num; - NAPI_CALL(env, napi_create_number(env, obj->value_, &num)); + NAPI_CALL(env, napi_create_double(env, obj->value_, &num)); return num; } @@ -140,7 +140,7 @@ napi_value MyObject::Multiply(napi_env env, napi_callback_info info) { const int kArgCount = 1; napi_value argv[kArgCount]; - NAPI_CALL(env, napi_create_number(env, obj->value_ * multiple, argv)); + NAPI_CALL(env, napi_create_double(env, obj->value_ * multiple, argv)); napi_value instance; NAPI_CALL(env, napi_new_instance(env, cons, kArgCount, argv, &instance)); diff --git a/test/addons-napi/7_factory_wrap/myobject.cc b/test/addons-napi/7_factory_wrap/myobject.cc index d6b374d7bb7ff8..4a2b284439ddb9 100644 --- a/test/addons-napi/7_factory_wrap/myobject.cc +++ b/test/addons-napi/7_factory_wrap/myobject.cc @@ -45,7 +45,7 @@ napi_value MyObject::New(napi_env env, napi_callback_info info) { if (valuetype == napi_undefined) { obj->counter_ = 0; } else { - NAPI_CALL(env, napi_get_value_double(env, args[0], &obj->counter_)); + NAPI_CALL(env, napi_get_value_uint32(env, args[0], &obj->counter_)); } obj->env_ = env; @@ -88,7 +88,7 @@ napi_value MyObject::PlusOne(napi_env env, napi_callback_info info) { obj->counter_ += 1; napi_value num; - NAPI_CALL(env, napi_create_number(env, obj->counter_, &num)); + NAPI_CALL(env, napi_create_uint32(env, obj->counter_, &num)); return num; } diff --git a/test/addons-napi/7_factory_wrap/myobject.h b/test/addons-napi/7_factory_wrap/myobject.h index c0b8522c609c4c..28ca94d16e30dd 100644 --- a/test/addons-napi/7_factory_wrap/myobject.h +++ b/test/addons-napi/7_factory_wrap/myobject.h @@ -18,7 +18,7 @@ class MyObject { static napi_ref constructor; static napi_value New(napi_env env, napi_callback_info info); static napi_value PlusOne(napi_env env, napi_callback_info info); - double counter_; + uint32_t counter_; napi_env env_; napi_ref wrapper_; }; diff --git a/test/addons-napi/8_passing_wrapped/binding.cc b/test/addons-napi/8_passing_wrapped/binding.cc index 581638bdfa2b07..c22ac6442f6c4e 100644 --- a/test/addons-napi/8_passing_wrapped/binding.cc +++ b/test/addons-napi/8_passing_wrapped/binding.cc @@ -24,7 +24,7 @@ napi_value Add(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_unwrap(env, args[1], reinterpret_cast(&obj2))); napi_value sum; - NAPI_CALL(env, napi_create_number(env, obj1->Val() + obj2->Val(), &sum)); + NAPI_CALL(env, napi_create_double(env, obj1->Val() + obj2->Val(), &sum)); return sum; } diff --git a/test/addons-napi/test_async/test_async.cc b/test/addons-napi/test_async/test_async.cc index f257b268b93159..ca76fa2d33b132 100644 --- a/test/addons-napi/test_async/test_async.cc +++ b/test/addons-napi/test_async/test_async.cc @@ -52,7 +52,7 @@ void Complete(napi_env env, napi_status status, void* data) { napi_value argv[2]; NAPI_CALL_RETURN_VOID(env, napi_get_null(env, &argv[0])); - NAPI_CALL_RETURN_VOID(env, napi_create_number(env, c->_output, &argv[1])); + NAPI_CALL_RETURN_VOID(env, napi_create_int32(env, c->_output, &argv[1])); napi_value callback; NAPI_CALL_RETURN_VOID(env, napi_get_reference_value(env, c->_callback, &callback)); diff --git a/test/addons-napi/test_buffer/test_buffer.c b/test/addons-napi/test_buffer/test_buffer.c index 880149e1ff8561..0e12bedfb5602f 100644 --- a/test/addons-napi/test_buffer/test_buffer.c +++ b/test/addons-napi/test_buffer/test_buffer.c @@ -55,7 +55,7 @@ napi_value newExternalBuffer(napi_env env, napi_callback_info info) { napi_value getDeleterCallCount(napi_env env, napi_callback_info info) { napi_value callCount; - NAPI_CALL(env, napi_create_number(env, deleterCallCount, &callCount)); + NAPI_CALL(env, napi_create_int32(env, deleterCallCount, &callCount)); return callCount; } diff --git a/test/addons-napi/test_constructor/test_constructor.c b/test/addons-napi/test_constructor/test_constructor.c index 220d564753ca10..a991dab8533bf1 100644 --- a/test/addons-napi/test_constructor/test_constructor.c +++ b/test/addons-napi/test_constructor/test_constructor.c @@ -12,7 +12,7 @@ napi_value GetValue(napi_env env, napi_callback_info info) { NAPI_ASSERT(env, argc == 0, "Wrong number of arguments"); napi_value number; - NAPI_CALL(env, napi_create_number(env, value_, &number)); + NAPI_CALL(env, napi_create_double(env, value_, &number)); return number; } @@ -53,7 +53,7 @@ napi_value GetStaticValue(napi_env env, napi_callback_info info) { NAPI_ASSERT(env, argc == 0, "Wrong number of arguments"); napi_value number; - NAPI_CALL(env, napi_create_number(env, static_value_, &number)); + NAPI_CALL(env, napi_create_double(env, static_value_, &number)); return number; } @@ -61,7 +61,7 @@ napi_value GetStaticValue(napi_env env, napi_callback_info info) { void Init(napi_env env, napi_value exports, napi_value module, void* priv) { napi_value number; - NAPI_CALL_RETURN_VOID(env, napi_create_number(env, value_, &number)); + NAPI_CALL_RETURN_VOID(env, napi_create_double(env, value_, &number)); napi_property_descriptor properties[] = { { "echo", 0, Echo, 0, 0, 0, napi_enumerable, 0 }, diff --git a/test/addons-napi/test_conversions/test_conversions.c b/test/addons-napi/test_conversions/test_conversions.c index 637cff43b82c4d..a8d526c763c9a2 100644 --- a/test/addons-napi/test_conversions/test_conversions.c +++ b/test/addons-napi/test_conversions/test_conversions.c @@ -24,7 +24,7 @@ napi_value AsInt32(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_int32(env, args[0], &value)); napi_value output; - NAPI_CALL(env, napi_create_number(env, value, &output)); + NAPI_CALL(env, napi_create_int32(env, value, &output)); return output; } @@ -38,7 +38,7 @@ napi_value AsUInt32(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_uint32(env, args[0], &value)); napi_value output; - NAPI_CALL(env, napi_create_number(env, value, &output)); + NAPI_CALL(env, napi_create_uint32(env, value, &output)); return output; } @@ -52,7 +52,7 @@ napi_value AsInt64(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_int64(env, args[0], &value)); napi_value output; - NAPI_CALL(env, napi_create_number(env, (double)value, &output)); + NAPI_CALL(env, napi_create_int64(env, (double)value, &output)); return output; } @@ -66,7 +66,7 @@ napi_value AsDouble(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_double(env, args[0], &value)); napi_value output; - NAPI_CALL(env, napi_create_number(env, value, &output)); + NAPI_CALL(env, napi_create_double(env, value, &output)); return output; } diff --git a/test/addons-napi/test_general/test_general.c b/test/addons-napi/test_general/test_general.c index 2d2d13a3624e6e..858611939474b2 100644 --- a/test/addons-napi/test_general/test_general.c +++ b/test/addons-napi/test_general/test_general.c @@ -29,7 +29,7 @@ napi_value testGetVersion(napi_env env, napi_callback_info info) { uint32_t version; napi_value result; NAPI_CALL(env, napi_get_version(env, &version)); - NAPI_CALL(env, napi_create_number(env, version, &result)); + NAPI_CALL(env, napi_create_uint32(env, version, &result)); return result; } diff --git a/test/addons-napi/test_number/test_number.c b/test/addons-napi/test_number/test_number.c index 1054741d2de369..6b28afe18ff9e8 100644 --- a/test/addons-napi/test_number/test_number.c +++ b/test/addons-napi/test_number/test_number.c @@ -18,7 +18,7 @@ napi_value Test(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_double(env, args[0], &input)); napi_value output; - NAPI_CALL(env, napi_create_number(env, input, &output)); + NAPI_CALL(env, napi_create_double(env, input, &output)); return output; } @@ -40,7 +40,7 @@ napi_value TestInt32Truncation(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_int32(env, args[0], &input)); napi_value output; - NAPI_CALL(env, napi_create_number(env, input, &output)); + NAPI_CALL(env, napi_create_int32(env, input, &output)); return output; } diff --git a/test/addons-napi/test_object/test_object.c b/test/addons-napi/test_object/test_object.c index 663e561a35b197..88ac79c170f5ea 100644 --- a/test/addons-napi/test_object/test_object.c +++ b/test/addons-napi/test_object/test_object.c @@ -144,7 +144,7 @@ napi_value New(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_create_object(env, &ret)); napi_value num; - NAPI_CALL(env, napi_create_number(env, 987654321, &num)); + NAPI_CALL(env, napi_create_int32(env, 987654321, &num)); NAPI_CALL(env, napi_set_named_property(env, ret, "test_number", num)); @@ -187,7 +187,7 @@ napi_value Inflate(napi_env env, napi_callback_info info) { double double_val; NAPI_CALL(env, napi_get_value_double(env, value, &double_val)); - NAPI_CALL(env, napi_create_number(env, double_val + 1, &value)); + NAPI_CALL(env, napi_create_double(env, double_val + 1, &value)); NAPI_CALL(env, napi_set_property(env, obj, property_str, value)); } diff --git a/test/addons-napi/test_properties/test_properties.c b/test/addons-napi/test_properties/test_properties.c index 3053fda4864e8f..3f4f0a6bcbba96 100644 --- a/test/addons-napi/test_properties/test_properties.c +++ b/test/addons-napi/test_properties/test_properties.c @@ -10,7 +10,7 @@ napi_value GetValue(napi_env env, napi_callback_info info) { NAPI_ASSERT(env, argc == 0, "Wrong number of arguments"); napi_value number; - NAPI_CALL(env, napi_create_number(env, value_, &number)); + NAPI_CALL(env, napi_create_double(env, value_, &number)); return number; } @@ -61,7 +61,7 @@ napi_value HasNamedProperty(napi_env env, napi_callback_info info) { void Init(napi_env env, napi_value exports, napi_value module, void* priv) { napi_value number; - NAPI_CALL_RETURN_VOID(env, napi_create_number(env, value_, &number)); + NAPI_CALL_RETURN_VOID(env, napi_create_double(env, value_, &number)); napi_value name_value; NAPI_CALL_RETURN_VOID(env, napi_create_string_utf8(env, diff --git a/test/addons-napi/test_reference/test_reference.c b/test/addons-napi/test_reference/test_reference.c index 0d3925ee5d04bc..b16e10d70361b5 100644 --- a/test/addons-napi/test_reference/test_reference.c +++ b/test/addons-napi/test_reference/test_reference.c @@ -8,7 +8,7 @@ static napi_ref test_reference = NULL; napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { napi_value result; - NAPI_CALL(env, napi_create_number(env, finalize_count, &result)); + NAPI_CALL(env, napi_create_int32(env, finalize_count, &result)); return result; } @@ -107,7 +107,7 @@ napi_value IncrementRefcount(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_reference_ref(env, test_reference, &refcount)); napi_value result; - NAPI_CALL(env, napi_create_number(env, refcount, &result)); + NAPI_CALL(env, napi_create_uint32(env, refcount, &result)); return result; } @@ -119,7 +119,7 @@ napi_value DecrementRefcount(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_reference_unref(env, test_reference, &refcount)); napi_value result; - NAPI_CALL(env, napi_create_number(env, refcount, &result)); + NAPI_CALL(env, napi_create_uint32(env, refcount, &result)); return result; } diff --git a/test/addons-napi/test_string/test_string.c b/test/addons-napi/test_string/test_string.c index 5cd6d413a679fb..ec80e2c7b57fa8 100644 --- a/test/addons-napi/test_string/test_string.c +++ b/test/addons-napi/test_string/test_string.c @@ -174,7 +174,7 @@ napi_value Utf16Length(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_string_utf16(env, args[0], NULL, 0, &length)); napi_value output; - NAPI_CALL(env, napi_create_number(env, (double)length, &output)); + NAPI_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); return output; } @@ -196,7 +196,7 @@ napi_value Utf8Length(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_get_value_string_utf8(env, args[0], NULL, 0, &length)); napi_value output; - NAPI_CALL(env, napi_create_number(env, (double)length, &output)); + NAPI_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); return output; } From 2ee3320f2c9f8e22a7cdb4ab683ae671bcc3a208 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 3 Aug 2017 18:08:11 -0700 Subject: [PATCH 07/97] test: improve multiple timers tests PR-URL: https://github.com/nodejs/node/pull/14616 Reviewed-By: Anna Henningsen Reviewed-By: Refael Ackermann --- test/parallel/test-timers-immediate.js | 20 ++----- .../parallel/test-timers-non-integer-delay.js | 12 ++-- ...imeout-removes-other-socket-unref-timer.js | 26 ++++---- test/parallel/test-timers-unref-leak.js | 23 ++----- test/parallel/test-timers-unref.js | 60 +++++++------------ ...test-timers-unrefd-interval-still-fires.js | 23 ++++--- .../test-timers-unrefed-in-beforeexit.js | 21 ++----- test/parallel/test-timers-zero-timeout.js | 14 ++--- 8 files changed, 68 insertions(+), 131 deletions(-) diff --git a/test/parallel/test-timers-immediate.js b/test/parallel/test-timers-immediate.js index f5e0fa00a725cc..0227e38efa9b20 100644 --- a/test/parallel/test-timers-immediate.js +++ b/test/parallel/test-timers-immediate.js @@ -23,9 +23,6 @@ const common = require('../common'); const assert = require('assert'); -let immediateC; -let immediateD; - let mainFinished = false; setImmediate(common.mustCall(function() { @@ -35,17 +32,12 @@ setImmediate(common.mustCall(function() { const immediateB = setImmediate(common.mustNotCall()); -setImmediate(function(x, y, z) { - immediateC = [x, y, z]; -}, 1, 2, 3); - -setImmediate(function(x, y, z, a, b) { - immediateD = [x, y, z, a, b]; -}, 1, 2, 3, 4, 5); +setImmediate(common.mustCall((...args) => { + assert.deepStrictEqual(args, [1, 2, 3]); +}), 1, 2, 3); -process.on('exit', function() { - assert.deepStrictEqual(immediateC, [1, 2, 3], 'immediateC args should match'); - assert.deepStrictEqual(immediateD, [1, 2, 3, 4, 5], '5 args should match'); -}); +setImmediate(common.mustCall((...args) => { + assert.deepStrictEqual(args, [1, 2, 3, 4, 5]); +}), 1, 2, 3, 4, 5); mainFinished = true; diff --git a/test/parallel/test-timers-non-integer-delay.js b/test/parallel/test-timers-non-integer-delay.js index bda4a31d81e495..017ef28d0f86ef 100644 --- a/test/parallel/test-timers-non-integer-delay.js +++ b/test/parallel/test-timers-non-integer-delay.js @@ -20,7 +20,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; -require('../common'); +const common = require('../common'); /* * This test makes sure that non-integer timer delays do not make the process @@ -39,13 +39,11 @@ require('../common'); */ const TIMEOUT_DELAY = 1.1; -const NB_TIMEOUTS_FIRED = 50; +let N = 50; -let nbTimeoutFired = 0; -const interval = setInterval(function() { - ++nbTimeoutFired; - if (nbTimeoutFired === NB_TIMEOUTS_FIRED) { +const interval = setInterval(common.mustCall(() => { + if (--N === 0) { clearInterval(interval); process.exit(0); } -}, TIMEOUT_DELAY); +}, N), TIMEOUT_DELAY); diff --git a/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js b/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js index 5d57bbaae0011e..842c8180701aa1 100644 --- a/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js +++ b/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js @@ -6,6 +6,7 @@ const common = require('../common'); const net = require('net'); +const Countdown = require('../common/countdown'); const clients = []; @@ -19,7 +20,7 @@ const server = net.createServer(function onClient(client) { * the list of unref timers when traversing it, and exposes the * original issue in joyent/node#8897. */ - clients[0].setTimeout(1, function onTimeout() { + clients[0].setTimeout(1, () => { clients[1].setTimeout(0); clients[0].end(); clients[1].end(); @@ -31,19 +32,16 @@ const server = net.createServer(function onClient(client) { } }); -server.listen(0, common.localhostIPv4, function() { - let nbClientsEnded = 0; +server.listen(0, common.localhostIPv4, common.mustCall(() => { + const countdown = new Countdown(2, common.mustCall(() => server.close())); - function addEndedClient(client) { - ++nbClientsEnded; - if (nbClientsEnded === 2) { - server.close(); - } + { + const client = net.connect({ port: server.address().port }); + client.on('end', () => countdown.dec()); } - const client1 = net.connect({ port: this.address().port }); - client1.on('end', addEndedClient); - - const client2 = net.connect({ port: this.address().port }); - client2.on('end', addEndedClient); -}); + { + const client = net.connect({ port: server.address().port }); + client.on('end', () => countdown.dec()); + } +})); diff --git a/test/parallel/test-timers-unref-leak.js b/test/parallel/test-timers-unref-leak.js index 8eef00dd4ffe92..afecf7f15ce1b5 100644 --- a/test/parallel/test-timers-unref-leak.js +++ b/test/parallel/test-timers-unref-leak.js @@ -1,27 +1,14 @@ 'use strict'; -require('../common'); -const assert = require('assert'); +const common = require('../common'); -let called = 0; -let closed = 0; - -const timeout = setTimeout(function() { - called++; -}, 10); +const timeout = setTimeout(common.mustCall(), 10); timeout.unref(); // Wrap `close` method to check if the handle was closed const close = timeout._handle.close; -timeout._handle.close = function() { - closed++; +timeout._handle.close = common.mustCall(function() { return close.apply(this, arguments); -}; +}); // Just to keep process alive and let previous timer's handle die -setTimeout(function() { -}, 50); - -process.on('exit', function() { - assert.strictEqual(called, 1); - assert.strictEqual(closed, 1); -}); +setTimeout(() => {}, 50); diff --git a/test/parallel/test-timers-unref.js b/test/parallel/test-timers-unref.js index 7f66a364e3f24b..0078d2dae352d5 100644 --- a/test/parallel/test-timers-unref.js +++ b/test/parallel/test-timers-unref.js @@ -21,60 +21,55 @@ 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); -let interval_fired = false; -let timeout_fired = false; let unref_interval = false; let unref_timer = false; -let unref_callbacks = 0; let checks = 0; const LONG_TIME = 10 * 1000; const SHORT_TIME = 100; -assert.doesNotThrow(function() { +assert.doesNotThrow(() => { setTimeout(() => {}, 10).unref().ref().unref(); }, 'ref and unref are chainable'); -assert.doesNotThrow(function() { +assert.doesNotThrow(() => { setInterval(() => {}, 10).unref().ref().unref(); }, 'ref and unref are chainable'); -setInterval(function() { - interval_fired = true; -}, LONG_TIME).unref(); +setInterval(common.mustNotCall('Interval should not fire'), LONG_TIME).unref(); +setTimeout(common.mustNotCall('Timer should not fire'), LONG_TIME).unref(); -setTimeout(function() { - timeout_fired = true; -}, LONG_TIME).unref(); - -const interval = setInterval(function() { +const interval = setInterval(common.mustCall(() => { unref_interval = true; clearInterval(interval); -}, SHORT_TIME); +}), SHORT_TIME); interval.unref(); -setTimeout(function() { +setTimeout(common.mustCall(() => { unref_timer = true; -}, SHORT_TIME).unref(); +}), SHORT_TIME).unref(); -const check_unref = setInterval(function() { +const check_unref = setInterval(() => { if (checks > 5 || (unref_interval && unref_timer)) clearInterval(check_unref); checks += 1; }, 100); -setTimeout(function() { - unref_callbacks++; - this.unref(); -}, SHORT_TIME); +{ + const timeout = + setTimeout(common.mustCall(() => { + timeout.unref(); + }), SHORT_TIME); +} -// Should not timeout the test -setInterval(function() { - this.unref(); -}, SHORT_TIME); +{ + // Should not timeout the test + const timeout = + setInterval(() => timeout.unref(), SHORT_TIME); +} // Should not assert on args.Holder()->InternalFieldCount() > 0. See #4261. { @@ -82,16 +77,3 @@ setInterval(function() { process.nextTick(t.unref.bind({})); process.nextTick(t.unref.bind(t)); } - -process.on('exit', function() { - assert.strictEqual(interval_fired, false, - 'Interval should not fire'); - assert.strictEqual(timeout_fired, false, - 'Timeout should not fire'); - assert.strictEqual(unref_timer, true, - 'An unrefd timeout should still fire'); - assert.strictEqual(unref_interval, true, - 'An unrefd interval should still fire'); - assert.strictEqual(unref_callbacks, 1, - 'Callback should only run once'); -}); diff --git a/test/parallel/test-timers-unrefd-interval-still-fires.js b/test/parallel/test-timers-unrefd-interval-still-fires.js index bf16013f004965..a9e9af84304217 100644 --- a/test/parallel/test-timers-unrefd-interval-still-fires.js +++ b/test/parallel/test-timers-unrefd-interval-still-fires.js @@ -5,23 +5,20 @@ const common = require('../common'); const TEST_DURATION = common.platformTimeout(1000); -const N = 3; -let nbIntervalFired = 0; +let N = 3; -const keepOpen = setTimeout(() => { - console.error('[FAIL] Interval fired %d/%d times.', nbIntervalFired, N); - throw new Error('Test timed out. keepOpen was not canceled.'); -}, TEST_DURATION); +const keepOpen = + setTimeout( + common.mustNotCall('Test timed out. keepOpen was not canceled.'), + TEST_DURATION); -const timer = setInterval(() => { - ++nbIntervalFired; - if (nbIntervalFired === N) { +const timer = setInterval(common.mustCall(() => { + if (--N === 0) { clearInterval(timer); - timer._onTimeout = () => { - throw new Error('Unrefd interval fired after being cleared.'); - }; + timer._onTimeout = + common.mustNotCall('Unrefd interal fired after being cleared'); clearTimeout(keepOpen); } -}, 1); +}, N), 1); timer.unref(); diff --git a/test/parallel/test-timers-unrefed-in-beforeexit.js b/test/parallel/test-timers-unrefed-in-beforeexit.js index 530d97674d8bf7..a38b55bf45d599 100644 --- a/test/parallel/test-timers-unrefed-in-beforeexit.js +++ b/test/parallel/test-timers-unrefed-in-beforeexit.js @@ -1,20 +1,7 @@ 'use strict'; -require('../common'); -const assert = require('assert'); +const common = require('../common'); -let once = 0; - -process.on('beforeExit', () => { - if (once > 1) - throw new RangeError('beforeExit should only have been called once!'); - - setTimeout(() => {}, 1).unref(); - once++; -}); - -process.on('exit', (code) => { - if (code !== 0) return; - - assert.strictEqual(once, 1); -}); +process.on('beforeExit', common.mustCall(() => { + setTimeout(common.mustNotCall(), 1).unref(); +})); diff --git a/test/parallel/test-timers-zero-timeout.js b/test/parallel/test-timers-zero-timeout.js index 7feb01854e0d64..61a5b2131bad57 100644 --- a/test/parallel/test-timers-zero-timeout.js +++ b/test/parallel/test-timers-zero-timeout.js @@ -36,18 +36,14 @@ const assert = require('assert'); } { - let ncalled = 0; + let ncalled = 3; - const iv = setInterval(f, 0, 'foo', 'bar', 'baz'); - - function f(a, b, c) { + const f = common.mustCall((a, b, c) => { assert.strictEqual(a, 'foo'); assert.strictEqual(b, 'bar'); assert.strictEqual(c, 'baz'); - if (++ncalled === 3) clearTimeout(iv); - } + if (--ncalled === 0) clearTimeout(iv); + }, ncalled); - process.on('exit', function() { - assert.strictEqual(ncalled, 3); - }); + const iv = setInterval(f, 0, 'foo', 'bar', 'baz'); } From ef8ac7b5ac04aab3a96e0d6524c016afec321e5b Mon Sep 17 00:00:00 2001 From: Guy Margalit Date: Sat, 5 Aug 2017 02:12:24 +0300 Subject: [PATCH 08/97] stream: support readable/writableHWM for Duplex This commits adds support for readableHighWaterMark and writableHighWaterMark in Duplex stream, so that they can be set without accessing the internal state. Fixes: https://github.com/nodejs/node/issues/14555 PR-URL: https://github.com/nodejs/node/pull/14636 Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- doc/api/stream.md | 4 ++ lib/_stream_readable.js | 18 ++++- lib/_stream_writable.js | 18 ++++- ...st-stream-transform-split-highwatermark.js | 71 +++++++++++++++++++ 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-stream-transform-split-highwatermark.js diff --git a/doc/api/stream.md b/doc/api/stream.md index 743bea795525f1..c26ab800faf610 100644 --- a/doc/api/stream.md +++ b/doc/api/stream.md @@ -1752,6 +1752,10 @@ constructor and implement *both* the `readable._read()` and * `writableObjectMode` {boolean} Defaults to `false`. Sets `objectMode` for writable side of the stream. Has no effect if `objectMode` is `true`. + * `readableHighWaterMark` {number} Sets `highWaterMark` for the readable side + of the stream. Has no effect if `highWaterMark` is provided. + * `writableHighWaterMark` {number} Sets `highWaterMark` for the writable side + of the stream. Has no effect if `highWaterMark` is provided. For example: diff --git a/lib/_stream_readable.js b/lib/_stream_readable.js index 463ac3bfbcd33e..1a0bc8902ac802 100644 --- a/lib/_stream_readable.js +++ b/lib/_stream_readable.js @@ -61,18 +61,32 @@ function prependListener(emitter, event, fn) { function ReadableState(options, stream) { options = options || {}; + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream. + // These options can be provided separately as readableXXX and writableXXX. + var isDuplex = stream instanceof Stream.Duplex; + // object stream flag. Used to make read(n) ignore n and to // make all the buffer merging and length checks go away this.objectMode = !!options.objectMode; - if (stream instanceof Stream.Duplex) + if (isDuplex) this.objectMode = this.objectMode || !!options.readableObjectMode; // the point at which it stops calling _read() to fill the buffer // Note: 0 is a valid value, means "don't call _read preemptively ever" var hwm = options.highWaterMark; + var readableHwm = options.readableHighWaterMark; var defaultHwm = this.objectMode ? 16 : 16 * 1024; - this.highWaterMark = (hwm || hwm === 0) ? hwm : defaultHwm; + + if (hwm || hwm === 0) + this.highWaterMark = hwm; + else if (isDuplex && (readableHwm || readableHwm === 0)) + this.highWaterMark = readableHwm; + else + this.highWaterMark = defaultHwm; // cast to ints. this.highWaterMark = Math.floor(this.highWaterMark); diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js index 334f492ba68eef..6e0eaf45b5464d 100644 --- a/lib/_stream_writable.js +++ b/lib/_stream_writable.js @@ -41,19 +41,33 @@ function nop() {} function WritableState(options, stream) { options = options || {}; + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream. + // These options can be provided separately as readableXXX and writableXXX. + var isDuplex = stream instanceof Stream.Duplex; + // object stream flag to indicate whether or not this stream // contains buffers or objects. this.objectMode = !!options.objectMode; - if (stream instanceof Stream.Duplex) + if (isDuplex) this.objectMode = this.objectMode || !!options.writableObjectMode; // the point at which write() starts returning false // Note: 0 is a valid value, means that we always return false if // the entire buffer is not flushed immediately on write() var hwm = options.highWaterMark; + var writableHwm = options.writableHighWaterMark; var defaultHwm = this.objectMode ? 16 : 16 * 1024; - this.highWaterMark = (hwm || hwm === 0) ? hwm : defaultHwm; + + if (hwm || hwm === 0) + this.highWaterMark = hwm; + else if (isDuplex && (writableHwm || writableHwm === 0)) + this.highWaterMark = writableHwm; + else + this.highWaterMark = defaultHwm; // cast to ints. this.highWaterMark = Math.floor(this.highWaterMark); diff --git a/test/parallel/test-stream-transform-split-highwatermark.js b/test/parallel/test-stream-transform-split-highwatermark.js new file mode 100644 index 00000000000000..af2558ec6decfb --- /dev/null +++ b/test/parallel/test-stream-transform-split-highwatermark.js @@ -0,0 +1,71 @@ +'use strict'; +require('../common'); +const assert = require('assert'); + +const { Transform, Readable, Writable } = require('stream'); + +const DEFAULT = 16 * 1024; + +function testTransform(expectedReadableHwm, expectedWritableHwm, options) { + const t = new Transform(options); + assert.strictEqual(t._readableState.highWaterMark, expectedReadableHwm); + assert.strictEqual(t._writableState.highWaterMark, expectedWritableHwm); +} + +// test overriding defaultHwm +testTransform(666, DEFAULT, { readableHighWaterMark: 666 }); +testTransform(DEFAULT, 777, { writableHighWaterMark: 777 }); +testTransform(666, 777, { + readableHighWaterMark: 666, + writableHighWaterMark: 777, +}); + +// test 0 overriding defaultHwm +testTransform(0, DEFAULT, { readableHighWaterMark: 0 }); +testTransform(DEFAULT, 0, { writableHighWaterMark: 0 }); + +// test highWaterMark overriding +testTransform(555, 555, { + highWaterMark: 555, + readableHighWaterMark: 666, +}); +testTransform(555, 555, { + highWaterMark: 555, + writableHighWaterMark: 777, +}); +testTransform(555, 555, { + highWaterMark: 555, + readableHighWaterMark: 666, + writableHighWaterMark: 777, +}); + +// test highWaterMark = 0 overriding +testTransform(0, 0, { + highWaterMark: 0, + readableHighWaterMark: 666, +}); +testTransform(0, 0, { + highWaterMark: 0, + writableHighWaterMark: 777, +}); +testTransform(0, 0, { + highWaterMark: 0, + readableHighWaterMark: 666, + writableHighWaterMark: 777, +}); + +// test undefined, null, NaN +[undefined, null, NaN].forEach((v) => { + testTransform(DEFAULT, DEFAULT, { readableHighWaterMark: v }); + testTransform(DEFAULT, DEFAULT, { writableHighWaterMark: v }); + testTransform(666, DEFAULT, { highWaterMark: v, readableHighWaterMark: 666 }); + testTransform(DEFAULT, 777, { highWaterMark: v, writableHighWaterMark: 777 }); +}); + +// test non Duplex streams ignore the options +{ + const r = new Readable({ readableHighWaterMark: 666 }); + assert.strictEqual(r._readableState.highWaterMark, DEFAULT); + const w = new Writable({ writableHighWaterMark: 777 }); + assert.strictEqual(w._writableState.highWaterMark, DEFAULT); +} From 935d34bd6b11ca478a8a4a3eff9d2c924461567a Mon Sep 17 00:00:00 2001 From: Azard Date: Tue, 8 Aug 2017 03:15:19 +0800 Subject: [PATCH 09/97] test: fix test-readline-interface Previous unit test delay is too short for parallel test on raspberry pi, it will fail sometimes. This PR use common.platformTimeout and widen the time gap. PR-URL: https://github.com/nodejs/node/pull/14677 Ref: https://github.com/nodejs/node/issues/14674 Reviewed-By: Refael Ackermann Reviewed-By: Colin Ihrig Reviewed-By: James M Snell Reviewed-By: Rich Trott --- test/parallel/test-readline-interface.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index c70eac4893340d..0ef7d426086158 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -286,8 +286,8 @@ function isWarned(emitter) { // over the default crlfDelay but within the setting value { const fi = new FakeInput(); - const delay = 200; - const crlfDelay = 500; + const delay = 125; + const crlfDelay = common.platformTimeout(1000); const rli = new readline.Interface({ input: fi, output: fi, From 574cc379b93131587f14fd3bc46bed8e4b4e631e Mon Sep 17 00:00:00 2001 From: nishijayaraj Date: Sat, 5 Aug 2017 17:37:10 +0530 Subject: [PATCH 10/97] benchmark: remove unused parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Functions onOnline and onMessage in benchmark/cluster/echo.js had unused parameters. They were removed. PR-URL: https://github.com/nodejs/node/pull/14640 Reviewed-By: Luigi Pinca Reviewed-By: James M Snell Reviewed-By: Tobias Nießen Reviewed-By: Rich Trott --- benchmark/cluster/echo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/cluster/echo.js b/benchmark/cluster/echo.js index 0733bdbd2077aa..1aa06120e15087 100644 --- a/benchmark/cluster/echo.js +++ b/benchmark/cluster/echo.js @@ -34,7 +34,7 @@ if (cluster.isMaster) { for (var i = 0; i < workers; ++i) cluster.fork().on('online', onOnline).on('message', onMessage); - function onOnline(msg) { + function onOnline() { if (++readies === workers) { bench.start(); broadcast(); @@ -56,7 +56,7 @@ if (cluster.isMaster) { } } - function onMessage(msg) { + function onMessage() { if (++msgCount === expectedPerBroadcast) { msgCount = 0; broadcast(); From 55aba6aee79a863086ee08a1d8eef09c12ca22bf Mon Sep 17 00:00:00 2001 From: Miguel Angel Asencio Hurtado Date: Fri, 4 Aug 2017 17:13:36 +0200 Subject: [PATCH 11/97] test: replace indexOf with includes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs: https://github.com/nodejs/node/issues/12586 PR-URL: https://github.com/nodejs/node/pull/14630 Reviewed-By: Colin Ihrig Reviewed-By: Vse Mozhet Byt Reviewed-By: Tobias Nießen Reviewed-By: Anna Henningsen Reviewed-By: Michaël Zasso Reviewed-By: James M Snell Reviewed-By: Yuta Hiroto Reviewed-By: Timothy Gu Reviewed-By: Alexey Orlenko --- test/async-hooks/init-hooks.js | 4 ++-- test/parallel/test-repl-tab-complete.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/async-hooks/init-hooks.js b/test/async-hooks/init-hooks.js index c99383204ffe03..a5f835cdff3aaa 100644 --- a/test/async-hooks/init-hooks.js +++ b/test/async-hooks/init-hooks.js @@ -67,7 +67,7 @@ class ActivityCollector { const violations = []; function v(msg) { violations.push(msg); } for (const a of this._activities.values()) { - if (types != null && types.indexOf(a.type) < 0) continue; + if (types != null && !types.includes(a.type)) continue; if (a.init && a.init.length > 1) { v('Activity inited twice\n' + activityString(a) + @@ -131,7 +131,7 @@ class ActivityCollector { activitiesOfTypes(types) { if (!Array.isArray(types)) types = [ types ]; - return this.activities.filter((x) => types.indexOf(x.type) >= 0); + return this.activities.filter((x) => types.includes(x.type)); } get activities() { diff --git a/test/parallel/test-repl-tab-complete.js b/test/parallel/test-repl-tab-complete.js index 994421ffc58a6a..85bcaf9e618ebc 100644 --- a/test/parallel/test-repl-tab-complete.js +++ b/test/parallel/test-repl-tab-complete.js @@ -411,7 +411,7 @@ const warningRegEx = new RegExp( }); // no `biu` - assert.strictEqual(data.indexOf('ele.biu'), -1); + assert.strictEqual(data.includes('ele.biu'), false); })); }); From 30837b3b9014048c4e95a10f6a9090a55cf666d6 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Sun, 6 Aug 2017 19:47:32 -0700 Subject: [PATCH 12/97] test: improve check in test-os MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check for `os.networkInterfaces()` in `test-os.js` may be too strict. It's apparently possible for a machine to be configured with multiple IPv4 loopback interfaces. Increase specificity of filter to check on only the object we expect. PR-URL: https://github.com/nodejs/node/pull/14655 Fixes: https://github.com/nodejs/node/issues/14654 Reviewed-By: Luigi Pinca Reviewed-By: Tobias Nießen Reviewed-By: Colin Ihrig Reviewed-By: Refael Ackermann Reviewed-By: James M Snell --- test/parallel/test-os.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-os.js b/test/parallel/test-os.js index 09920ebbaa33d4..180d869001f5a6 100644 --- a/test/parallel/test-os.js +++ b/test/parallel/test-os.js @@ -116,7 +116,8 @@ const interfaces = os.networkInterfaces(); switch (platform) { case 'linux': { - const filter = (e) => e.address === '127.0.0.1'; + const filter = + (e) => e.address === '127.0.0.1' && e.netmask === '255.0.0.0'; const actual = interfaces.lo.filter(filter); const expected = [{ address: '127.0.0.1', netmask: '255.0.0.0', mac: '00:00:00:00:00:00', family: 'IPv4', From 06ba2dae30370e844d9d6d18ab776875b15077cc Mon Sep 17 00:00:00 2001 From: Nick Stanish Date: Tue, 8 Aug 2017 22:48:47 -0500 Subject: [PATCH 13/97] test: fix conversion of microseconds in test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/14706 Fixes: https://github.com/nodejs/node/issues/8728 Reviewed-By: Rich Trott Reviewed-By: Luigi Pinca Reviewed-By: Tobias Nießen Reviewed-By: Colin Ihrig Reviewed-By: Timothy Gu --- test/pummel/test-process-cpuUsage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/pummel/test-process-cpuUsage.js b/test/pummel/test-process-cpuUsage.js index 20b2fadef9fae0..1d5aa861b64ad8 100644 --- a/test/pummel/test-process-cpuUsage.js +++ b/test/pummel/test-process-cpuUsage.js @@ -17,14 +17,14 @@ while (Date.now() - now < RUN_FOR_MS); // Get a diff reading from when we started. const diff = process.cpuUsage(start); -const MICROSECONDS_PER_SECOND = 1000 * 1000; +const MICROSECONDS_PER_MILLISECOND = 1000; // Diff usages should be >= 0, <= ~RUN_FOR_MS millis. // Let's be generous with the slop factor, defined above, in case other things // are happening on this CPU. The <= check may be invalid if the node process // is making use of multiple CPUs, in which case, just remove it. assert(diff.user >= 0); -assert(diff.user <= SLOP_FACTOR * RUN_FOR_MS * MICROSECONDS_PER_SECOND); +assert(diff.user <= SLOP_FACTOR * RUN_FOR_MS * MICROSECONDS_PER_MILLISECOND); assert(diff.system >= 0); -assert(diff.system <= SLOP_FACTOR * RUN_FOR_MS * MICROSECONDS_PER_SECOND); +assert(diff.system <= SLOP_FACTOR * RUN_FOR_MS * MICROSECONDS_PER_MILLISECOND); From a0e05e884e60f9e8193c12b80f143c98e04b8fce Mon Sep 17 00:00:00 2001 From: Daniel Bevenius Date: Mon, 3 Jul 2017 06:27:58 +0200 Subject: [PATCH 14/97] tools: fix tools/addon-verify.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current implementation of addon-verify.js is including the code for the "Function arguments" section in test/addons/01_callbacks and there is no directory generated or the "Function arguments section". This continues and leads to the last section, "AtExit", code to be excluded. There is an test/addons/07_atexit_hooks but it contains code from the "Passing wrapped objects around" section. This commit modifies addon-verify to associate headers with code and then iterates over the set and generates the files as a separate step. PR-URL: https://github.com/nodejs/node/pull/14048 Reviewed-By: Michaël Zasso --- doc/api/addons.md | 2 +- tools/doc/addon-verify.js | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/doc/api/addons.md b/doc/api/addons.md index e973ec41113d72..c44d31dc1bc09d 100644 --- a/doc/api/addons.md +++ b/doc/api/addons.md @@ -1125,7 +1125,7 @@ Test in JavaScript by running: ```js // test.js -const addon = require('./build/Release/addon'); +require('./build/Release/addon'); ``` [Embedder's Guide]: https://github.com/v8/v8/wiki/Embedder's%20Guide diff --git a/tools/doc/addon-verify.js b/tools/doc/addon-verify.js index 9af040b33986f5..2e72abb77f925f 100644 --- a/tools/doc/addon-verify.js +++ b/tools/doc/addon-verify.js @@ -11,29 +11,29 @@ const verifyDir = path.resolve(rootDir, 'test', 'addons'); const contents = fs.readFileSync(doc).toString(); const tokens = marked.lexer(contents); -let files = null; let id = 0; -// Just to make sure that all examples will be processed -tokens.push({ type: 'heading' }); - -for (var i = 0; i < tokens.length; i++) { - var token = tokens[i]; +let currentHeader; +const addons = {}; +tokens.forEach((token) => { if (token.type === 'heading' && token.text) { - const blockName = token.text; - if (files && Object.keys(files).length !== 0) { - verifyFiles(files, - blockName, - console.log.bind(null, 'wrote'), - function(err) { if (err) throw err; }); - } - files = {}; - } else if (token.type === 'code') { + currentHeader = token.text; + addons[currentHeader] = { + files: {} + }; + } + if (token.type === 'code') { var match = token.text.match(/^\/\/\s+(.*\.(?:cc|h|js))[\r\n]/); - if (match === null) - continue; - files[match[1]] = token.text; + if (match !== null) { + addons[currentHeader].files[match[1]] = token.text; + } } +}); +for (var header in addons) { + verifyFiles(addons[header].files, + header, + console.log.bind(null, 'wrote'), + function(err) { if (err) throw err; }); } function once(fn) { From e07dfffad023ccf2d44f8c6fac905387ae3f3ec4 Mon Sep 17 00:00:00 2001 From: Daniel Bevenius Date: Mon, 3 Jul 2017 06:58:45 +0200 Subject: [PATCH 15/97] doc: remove undef NDEBUG from addons.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When working on commit 0d95a0b60a87533c54b517d132e16401fe28909b ("test: remove undef NDEBUG from at-exit addons test) I searched for usages of undef NDEBUG but did not include the doc directory (but I did include the test directory) and missed this one. Commit 1f02569f8db9cb0101807df4982534738f0161b2 ("tools: fix tools/addon-verify.js") enables the code in the "AtExit" section to be included in the test/addons diretory and this code will again be tested. PR-URL: https://github.com/nodejs/node/pull/14048 Reviewed-By: Michaël Zasso --- doc/api/addons.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/api/addons.md b/doc/api/addons.md index c44d31dc1bc09d..7f0529e5264454 100644 --- a/doc/api/addons.md +++ b/doc/api/addons.md @@ -1073,7 +1073,6 @@ The following `addon.cc` implements AtExit: ```cpp // addon.cc -#undef NDEBUG #include #include #include From 4e15a6b76ab21926203f4fa484f59d5597568b9d Mon Sep 17 00:00:00 2001 From: Daniel Bevenius Date: Mon, 3 Jul 2017 07:56:43 +0200 Subject: [PATCH 16/97] doc: fix order of AtExit callbacks in addons.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sanity_check AtExit callback needs to come last to verify that the other callbacks have been completed. This was not noticed before as this code was not been executed. PR-URL: https://github.com/nodejs/node/pull/14048 Reviewed-By: Michaël Zasso --- doc/api/addons.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/addons.md b/doc/api/addons.md index 7f0529e5264454..f09b2e7ee60f3f 100644 --- a/doc/api/addons.md +++ b/doc/api/addons.md @@ -1109,10 +1109,10 @@ static void sanity_check(void*) { } void init(Local exports) { - AtExit(sanity_check); AtExit(at_exit_cb2, cookie); AtExit(at_exit_cb2, cookie); AtExit(at_exit_cb1, exports->GetIsolate()); + AtExit(sanity_check); } NODE_MODULE(addon, init) From 8433b1ad3786d5a4914294815544e0393eab4f40 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Mon, 7 Aug 2017 17:11:06 -0700 Subject: [PATCH 17/97] lib: use Timer.now() in readline module Using Date.now() introduces problems when operating under load or otherwise with constrained resources. Use Timer.now() to mitigate. The problem was identified in `test-readline-interface` where under heavy load, `\r` and `\n` were received so far apart that they were treated as separate line endings rather than a single line ending. Switching to `Timer.now()` prevented this from happening. PR-URL: https://github.com/nodejs/node/pull/14681 Refs: https://github.com/nodejs/node/issues/14674 Reviewed-By: Colin Ihrig Reviewed-By: Refael Ackermann --- lib/readline.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 7b3b2f18732dc0..85333d4a6c095f 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -46,6 +46,8 @@ const { kClearScreenDown } = CSI; +const now = process.binding('timer_wrap').Timer.now; + const kHistorySize = 30; const kMincrlfDelay = 100; // \r\n, \n, or \r followed by something other than \n @@ -395,7 +397,7 @@ Interface.prototype._normalWrite = function(b) { } var string = this._decoder.write(b); if (this._sawReturnAt && - Date.now() - this._sawReturnAt <= this.crlfDelay) { + now() - this._sawReturnAt <= this.crlfDelay) { string = string.replace(/^\n/, ''); this._sawReturnAt = 0; } @@ -408,7 +410,7 @@ Interface.prototype._normalWrite = function(b) { this._line_buffer = null; } if (newPartContainsEnding) { - this._sawReturnAt = string.endsWith('\r') ? Date.now() : 0; + this._sawReturnAt = string.endsWith('\r') ? now() : 0; // got one or more newlines; process into "line" events var lines = string.split(lineEnding); @@ -900,14 +902,14 @@ Interface.prototype._ttyWrite = function(s, key) { switch (key.name) { case 'return': // carriage return, i.e. \r - this._sawReturnAt = Date.now(); + this._sawReturnAt = now(); this._line(); break; case 'enter': // When key interval > crlfDelay if (this._sawReturnAt === 0 || - Date.now() - this._sawReturnAt > this.crlfDelay) { + now() - this._sawReturnAt > this.crlfDelay) { this._line(); } this._sawReturnAt = 0; From c94f346b932e5bda66c31de27b3302e495000b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Mon, 5 Jun 2017 14:05:36 +0200 Subject: [PATCH 18/97] net: use rest parameters instead of arguments In v8 6.0, rest parameters are significantly faster than other ways to create an array of the arguments, even for small numbers. PR-URL: https://github.com/nodejs/node/pull/13472 Refs: https://github.com/nodejs/node/issues/13430 Reviewed-By: Matteo Collina Reviewed-By: Refael Ackermann Reviewed-By: James M Snell Reviewed-By: Joyee Cheung --- lib/net.js | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/lib/net.js b/lib/net.js index f07b8cd2c72f09..220847ac11cbda 100644 --- a/lib/net.js +++ b/lib/net.js @@ -91,11 +91,7 @@ function createServer(options, connectionListener) { // connect(port, [host], [cb]) // connect(path, [cb]); // -function connect() { - var args = new Array(arguments.length); - for (var i = 0; i < arguments.length; i++) - args[i] = arguments[i]; - // TODO(joyeecheung): use destructuring when V8 is fast enough +function connect(...args) { var normalized = normalizeArgs(args); var options = normalized[0]; debug('createConnection', normalized); @@ -948,19 +944,15 @@ function internalConnect( } -Socket.prototype.connect = function() { +Socket.prototype.connect = function(...args) { let normalized; // If passed an array, it's treated as an array of arguments that have // already been normalized (so we don't normalize more than once). This has // been solved before in https://github.com/nodejs/node/pull/12342, but was // reverted as it had unintended side effects. - if (Array.isArray(arguments[0]) && arguments[0][normalizedArgsSymbol]) { - normalized = arguments[0]; + if (Array.isArray(args[0]) && args[0][normalizedArgsSymbol]) { + normalized = args[0]; } else { - var args = new Array(arguments.length); - for (var i = 0; i < arguments.length; i++) - args[i] = arguments[i]; - // TODO(joyeecheung): use destructuring when V8 is fast enough normalized = normalizeArgs(args); } var options = normalized[0]; @@ -1420,11 +1412,7 @@ function listenInCluster(server, address, port, addressType, } -Server.prototype.listen = function() { - var args = new Array(arguments.length); - for (var i = 0; i < arguments.length; i++) - args[i] = arguments[i]; - // TODO(joyeecheung): use destructuring when V8 is fast enough +Server.prototype.listen = function(...args) { var normalized = normalizeArgs(args); var options = normalized[0]; var cb = normalized[1]; From d3b072276b0e11364bf0a25547ec9c1bc1e4e373 Mon Sep 17 00:00:00 2001 From: Mandeep Singh Date: Sun, 16 Jul 2017 11:32:04 +0530 Subject: [PATCH 19/97] doc: add docs for AssertionError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/nodejs/node/issues/12274 PR-URL: https://github.com/nodejs/node/pull/14261 Reviewed-By: Vse Mozhet Byt Reviewed-By: Gibson Fahnestock Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen Reviewed-By: James M Snell Reviewed-By: Khaidi Chu --- doc/api/errors.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/api/errors.md b/doc/api/errors.md index 570832237c485d..da63708dd49923 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -358,6 +358,18 @@ loop tick. System-level errors are generated as augmented `Error` instances, which are detailed [here](#errors_system_errors). +## Class: AssertionError + +A subclass of `Error` that indicates the failure of an assertion. Such errors +commonly indicate inequality of actual and expected value. + +For example: + +```js +assert.strictEqual(1, 2); +// AssertionError [ERR_ASSERTION]: 1 === 2 +``` + ## Class: RangeError A subclass of `Error` that indicates that a provided argument was not within the From 58742729da0646fc50685e9a7a1b1bd062e5848f Mon Sep 17 00:00:00 2001 From: Prakash Palaniappan Date: Thu, 27 Jul 2017 09:40:15 -0400 Subject: [PATCH 20/97] test: set module loading error for aix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In test/parallel/test-module-loading-error.js, an attempt is made to load a text file as a native executable. This results in an error message in a platform specific manner. AIX was not included in the list of platforms. This fix introduces the AIX error messages. PR-URL: https://github.com/nodejs/node/pull/14511 Reviewed-By: Tobias Nießen Reviewed-By: James M Snell Reviewed-By: Luigi Pinca Reviewed-By: Colin Ihrig Reviewed-By: Gireesh Punathil Reviewed-By: Gibson Fahnestock --- test/parallel/test-module-loading-error.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-module-loading-error.js b/test/parallel/test-module-loading-error.js index 5cdf9182608632..0816c5f94a5ec5 100644 --- a/test/parallel/test-module-loading-error.js +++ b/test/parallel/test-module-loading-error.js @@ -28,7 +28,9 @@ const errorMessagesByPlatform = { win32: ['%1 is not a valid Win32 application'], linux: ['file too short', 'Exec format error'], sunos: ['unknown file type', 'not an ELF file'], - darwin: ['file too short'] + darwin: ['file too short'], + aix: ['Cannot load module', + 'Cannot run a file that does not have a valid format.'] }; // If we don't know a priori what the error would be, we accept anything. const errorMessages = errorMessagesByPlatform[process.platform] || ['']; From 2716b626b085bad6675852e6afa5bff629cbdbd3 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 8 Aug 2017 18:41:19 +0200 Subject: [PATCH 21/97] async_hooks: CHECK that resource is not empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This condition can be triggered through the public C++ embedder API. PR-URL: https://github.com/nodejs/node/pull/14694 Reviewed-By: Tobias Nießen Reviewed-By: Ben Noordhuis Reviewed-By: Colin Ihrig Reviewed-By: Refael Ackermann --- src/async-wrap.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/async-wrap.cc b/src/async-wrap.cc index f336a49307557c..b6588a20ad4071 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -646,6 +646,8 @@ void AsyncWrap::EmitAsyncInit(Environment* env, Local type, double async_id, double trigger_id) { + CHECK(!object.IsEmpty()); + CHECK(!type.IsEmpty()); AsyncHooks* async_hooks = env->async_hooks(); // Nothing to execute, so can continue normally. From d604173a66105e82d14f04ff4cf72310f619f6bc Mon Sep 17 00:00:00 2001 From: dcharbonnier Date: Thu, 3 Aug 2017 19:37:09 +0200 Subject: [PATCH 22/97] doc: clarify the position argument for fs.read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What happen to the file position after a read using a position null or integer was not clear and you can assume that the cursor of the file descriptor is updated even if position is an integer. PR-URL: https://github.com/nodejs/node/pull/14631 Fixes: https://github.com/https://github.com/nodejs/node/issues/8397 Reviewed-By: James M Snell Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen --- doc/api/fs.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index e151f0fed89a15..0107b0a2311c85 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1640,7 +1640,9 @@ Read data from the file specified by `fd`. `length` is an integer specifying the number of bytes to read. `position` is an integer specifying where to begin reading from in the file. -If `position` is `null`, data will be read from the current file position. +If `position` is `null`, data will be read from the current file position, +and the file position will be updated for subsequent reads. +If `position` is an integer, the file position will remain unchanged. The callback is given the three arguments, `(err, bytesRead, buffer)`. From 9888bb123840a7ec02009b988ad4187f90463e8c Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Fri, 4 Aug 2017 09:44:11 -0700 Subject: [PATCH 23/97] doc: improve fs.read() doc text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/14631 Reviewed-By: James M Snell Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen --- doc/api/fs.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index 0107b0a2311c85..6aeee5d55e412f 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1639,9 +1639,9 @@ Read data from the file specified by `fd`. `length` is an integer specifying the number of bytes to read. -`position` is an integer specifying where to begin reading from in the file. -If `position` is `null`, data will be read from the current file position, -and the file position will be updated for subsequent reads. +`position` is an argument specifying where to begin reading from in the file. +If `position` is `null`, data will be read from the current file position, +and the file position will be updated. If `position` is an integer, the file position will remain unchanged. The callback is given the three arguments, `(err, bytesRead, buffer)`. From 5d99d7dff2769afeec5072e106e43d20cc9ea4d6 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Thu, 3 Aug 2017 14:10:37 -0700 Subject: [PATCH 24/97] test: add block scoping to test-readline-interface Use block-scoping in test-readline-interface to avoid side effects and make tests more modular. (Some contain race conditions and will need to be moved to the sequential directory if they can't be refactored to avoid the race condition.) Backport-PR-URL: https://github.com/nodejs/node/pull/14748 Backport-Reviewed-By: Anna Henningsen PR-URL: https://github.com/nodejs/node/pull/14615 Reviewed-By: Colin Ihrig Reviewed-By: Yuta Hiroto Reviewed-By: James M Snell --- test/parallel/test-readline-interface.js | 726 +++++++++++++---------- 1 file changed, 405 insertions(+), 321 deletions(-) diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 0ef7d426086158..20cc20bfc00288 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -87,177 +87,222 @@ function isWarned(emitter) { } [ true, false ].forEach(function(terminal) { - let fi; - let rli; - let called; - // disable history - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal, - historySize: 0 }); - assert.strictEqual(rli.historySize, 0); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal, historySize: 0 } + ); + assert.strictEqual(rli.historySize, 0); - fi.emit('data', 'asdf\n'); - assert.deepStrictEqual(rli.history, terminal ? [] : undefined); - rli.close(); + fi.emit('data', 'asdf\n'); + assert.deepStrictEqual(rli.history, terminal ? [] : undefined); + rli.close(); + } // default history size 30 - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal}); - assert.strictEqual(rli.historySize, 30); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + assert.strictEqual(rli.historySize, 30); - fi.emit('data', 'asdf\n'); - assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : undefined); - rli.close(); + fi.emit('data', 'asdf\n'); + assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : undefined); + rli.close(); + } // sending a full line - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - called = false; - rli.on('line', function(line) { - called = true; - assert.strictEqual(line, 'asdf'); - }); - fi.emit('data', 'asdf\n'); - assert.ok(called); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + let called = false; + rli.on('line', function(line) { + called = true; + assert.strictEqual(line, 'asdf'); + }); + fi.emit('data', 'asdf\n'); + assert.ok(called); + } // sending a blank line - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - called = false; - rli.on('line', function(line) { - called = true; - assert.strictEqual(line, ''); - }); - fi.emit('data', '\n'); - assert.ok(called); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + let called = false; + rli.on('line', function(line) { + called = true; + assert.strictEqual(line, ''); + }); + fi.emit('data', '\n'); + assert.ok(called); + } // sending a single character with no newline - fi = new FakeInput(); - rli = new readline.Interface(fi, {}); - called = false; - rli.on('line', function(line) { - called = true; - }); - fi.emit('data', 'a'); - assert.ok(!called); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface(fi, {}); + let called = false; + rli.on('line', function(line) { + called = true; + }); + fi.emit('data', 'a'); + assert.ok(!called); + rli.close(); + } // sending a single character with no newline and then a newline - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - called = false; - rli.on('line', function(line) { - called = true; - assert.strictEqual(line, 'a'); - }); - fi.emit('data', 'a'); - assert.ok(!called); - fi.emit('data', '\n'); - assert.ok(called); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + let called = false; + rli.on('line', function(line) { + called = true; + assert.strictEqual(line, 'a'); + }); + fi.emit('data', 'a'); + assert.ok(!called); + fi.emit('data', '\n'); + assert.ok(called); + rli.close(); + } // sending multiple newlines at once - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - let expectedLines = ['foo', 'bar', 'baz']; - let callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', `${expectedLines.join('\n')}\n`); - assert.strictEqual(callCount, expectedLines.length); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar', 'baz']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', `${expectedLines.join('\n')}\n`); + assert.strictEqual(callCount, expectedLines.length); + rli.close(); + } // sending multiple newlines at once that does not end with a new line - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - expectedLines = ['foo', 'bar', 'baz', 'bat']; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', expectedLines.join('\n')); - assert.strictEqual(callCount, expectedLines.length - 1); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar', 'baz', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', expectedLines.join('\n')); + assert.strictEqual(callCount, expectedLines.length - 1); + rli.close(); + } // sending multiple newlines at once that does not end with a new(empty) // line and a `end` event - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - expectedLines = ['foo', 'bar', 'baz', '']; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - rli.on('close', function() { - callCount++; - }); - fi.emit('data', expectedLines.join('\n')); - fi.emit('end'); - assert.strictEqual(callCount, expectedLines.length); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar', 'baz', '']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + rli.on('close', function() { + callCount++; + }); + fi.emit('data', expectedLines.join('\n')); + fi.emit('end'); + assert.strictEqual(callCount, expectedLines.length); + rli.close(); + } // sending multiple newlines at once that does not end with a new line // and a `end` event(last line is) // \r\n should emit one line event, not two - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - expectedLines = ['foo', 'bar', 'baz', 'bat']; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', expectedLines.join('\r\n')); - assert.strictEqual(callCount, expectedLines.length - 1); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar', 'baz', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', expectedLines.join('\r\n')); + assert.strictEqual(callCount, expectedLines.length - 1); + rli.close(); + } // \r\n should emit one line event when split across multiple writes. - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - expectedLines = ['foo', 'bar', 'baz', 'bat']; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - expectedLines.forEach(function(line) { - fi.emit('data', `${line}\r`); - fi.emit('data', '\n'); - }); - assert.strictEqual(callCount, expectedLines.length); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar', 'baz', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + expectedLines.forEach(function(line) { + fi.emit('data', `${line}\r`); + fi.emit('data', '\n'); + }); + assert.strictEqual(callCount, expectedLines.length); + rli.close(); + } // \r should behave like \n when alone - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: true }); - expectedLines = ['foo', 'bar', 'baz', 'bat']; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', expectedLines.join('\r')); - assert.strictEqual(callCount, expectedLines.length - 1); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: true } + ); + const expectedLines = ['foo', 'bar', 'baz', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', expectedLines.join('\r')); + assert.strictEqual(callCount, expectedLines.length - 1); + rli.close(); + } // \r at start of input should output blank line - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: true }); - expectedLines = ['', 'foo' ]; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', '\rfoo\r'); - assert.strictEqual(callCount, expectedLines.length); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: true } + ); + const expectedLines = ['', 'foo' ]; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', '\rfoo\r'); + assert.strictEqual(callCount, expectedLines.length); + rli.close(); + } // Emit two line events when the delay // between \r and \n exceeds crlfDelay @@ -331,189 +376,221 @@ function isWarned(emitter) { // \t when there is no completer function should behave like an ordinary // character - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: true }); - called = false; - rli.on('line', function(line) { - assert.strictEqual(line, '\t'); - assert.strictEqual(called, false); - called = true; - }); - fi.emit('data', '\t'); - fi.emit('data', '\n'); - assert.ok(called); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: true } + ); + let called = false; + rli.on('line', function(line) { + assert.strictEqual(line, '\t'); + assert.strictEqual(called, false); + called = true; + }); + fi.emit('data', '\t'); + fi.emit('data', '\n'); + assert.ok(called); + rli.close(); + } // \t does not become part of the input when there is a completer function - fi = new FakeInput(); - const completer = (line) => [[], line]; - rli = new readline.Interface({ - input: fi, - output: fi, - terminal: true, - completer: completer - }); - called = false; - rli.on('line', function(line) { - assert.strictEqual(line, 'foo'); - assert.strictEqual(called, false); - called = true; - }); - for (const character of '\tfo\to\t') { - fi.emit('data', character); + { + const fi = new FakeInput(); + const completer = (line) => [[], line]; + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + completer: completer + }); + let called = false; + rli.on('line', function(line) { + assert.strictEqual(line, 'foo'); + assert.strictEqual(called, false); + called = true; + }); + for (const character of '\tfo\to\t') { + fi.emit('data', character); + } + fi.emit('data', '\n'); + assert.ok(called); + rli.close(); } - fi.emit('data', '\n'); - assert.ok(called); - rli.close(); // constructor throws if completer is not a function or undefined - fi = new FakeInput(); - assert.throws(function() { - readline.createInterface({ - input: fi, - completer: 'string is not valid' - }); - }, function(err) { - if (err instanceof TypeError) { - if (/Argument "completer" must be a function/.test(err)) { - return true; + { + const fi = new FakeInput(); + assert.throws(function() { + readline.createInterface({ + input: fi, + completer: 'string is not valid' + }); + }, function(err) { + if (err instanceof TypeError) { + if (/Argument "completer" must be a function/.test(err)) { + return true; + } } - } - return false; - }); + return false; + }); + } // duplicate lines are removed from history when // `options.removeHistoryDuplicates` is `true` - fi = new FakeInput(); - rli = new readline.Interface({ - input: fi, - output: fi, - terminal: true, - removeHistoryDuplicates: true - }); - expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat']; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', `${expectedLines.join('\n')}\n`); - assert.strictEqual(callCount, expectedLines.length); - fi.emit('keypress', '.', { name: 'up' }); // 'bat' - assert.strictEqual(rli.line, expectedLines[--callCount]); - fi.emit('keypress', '.', { name: 'up' }); // 'bar' - assert.notStrictEqual(rli.line, expectedLines[--callCount]); - assert.strictEqual(rli.line, expectedLines[--callCount]); - fi.emit('keypress', '.', { name: 'up' }); // 'baz' - assert.strictEqual(rli.line, expectedLines[--callCount]); - fi.emit('keypress', '.', { name: 'up' }); // 'foo' - assert.notStrictEqual(rli.line, expectedLines[--callCount]); - assert.strictEqual(rli.line, expectedLines[--callCount]); - assert.strictEqual(callCount, 0); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + removeHistoryDuplicates: true + }); + const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', `${expectedLines.join('\n')}\n`); + assert.strictEqual(callCount, expectedLines.length); + fi.emit('keypress', '.', { name: 'up' }); // 'bat' + assert.strictEqual(rli.line, expectedLines[--callCount]); + fi.emit('keypress', '.', { name: 'up' }); // 'bar' + assert.notStrictEqual(rli.line, expectedLines[--callCount]); + assert.strictEqual(rli.line, expectedLines[--callCount]); + fi.emit('keypress', '.', { name: 'up' }); // 'baz' + assert.strictEqual(rli.line, expectedLines[--callCount]); + fi.emit('keypress', '.', { name: 'up' }); // 'foo' + assert.notStrictEqual(rli.line, expectedLines[--callCount]); + assert.strictEqual(rli.line, expectedLines[--callCount]); + assert.strictEqual(callCount, 0); + rli.close(); + } // duplicate lines are not removed from history when // `options.removeHistoryDuplicates` is `false` - fi = new FakeInput(); - rli = new readline.Interface({ - input: fi, - output: fi, - terminal: true, - removeHistoryDuplicates: false - }); - expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat']; - callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', `${expectedLines.join('\n')}\n`); - assert.strictEqual(callCount, expectedLines.length); - fi.emit('keypress', '.', { name: 'up' }); // 'bat' - assert.strictEqual(rli.line, expectedLines[--callCount]); - fi.emit('keypress', '.', { name: 'up' }); // 'bar' - assert.notStrictEqual(rli.line, expectedLines[--callCount]); - assert.strictEqual(rli.line, expectedLines[--callCount]); - fi.emit('keypress', '.', { name: 'up' }); // 'baz' - assert.strictEqual(rli.line, expectedLines[--callCount]); - fi.emit('keypress', '.', { name: 'up' }); // 'bar' - assert.strictEqual(rli.line, expectedLines[--callCount]); - fi.emit('keypress', '.', { name: 'up' }); // 'foo' - assert.strictEqual(rli.line, expectedLines[--callCount]); - assert.strictEqual(callCount, 0); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: true, + removeHistoryDuplicates: false + }); + const expectedLines = ['foo', 'bar', 'baz', 'bar', 'bat', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', `${expectedLines.join('\n')}\n`); + assert.strictEqual(callCount, expectedLines.length); + fi.emit('keypress', '.', { name: 'up' }); // 'bat' + assert.strictEqual(rli.line, expectedLines[--callCount]); + fi.emit('keypress', '.', { name: 'up' }); // 'bar' + assert.notStrictEqual(rli.line, expectedLines[--callCount]); + assert.strictEqual(rli.line, expectedLines[--callCount]); + fi.emit('keypress', '.', { name: 'up' }); // 'baz' + assert.strictEqual(rli.line, expectedLines[--callCount]); + fi.emit('keypress', '.', { name: 'up' }); // 'bar' + assert.strictEqual(rli.line, expectedLines[--callCount]); + fi.emit('keypress', '.', { name: 'up' }); // 'foo' + assert.strictEqual(rli.line, expectedLines[--callCount]); + assert.strictEqual(callCount, 0); + rli.close(); + } // sending a multi-byte utf8 char over multiple writes - const buf = Buffer.from('☮', 'utf8'); - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - callCount = 0; - rli.on('line', function(line) { - callCount++; - assert.strictEqual(line, buf.toString('utf8')); - }); - [].forEach.call(buf, function(i) { - fi.emit('data', Buffer.from([i])); - }); - assert.strictEqual(callCount, 0); - fi.emit('data', '\n'); - assert.strictEqual(callCount, 1); - rli.close(); + { + const buf = Buffer.from('☮', 'utf8'); + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + let callCount = 0; + rli.on('line', function(line) { + callCount++; + assert.strictEqual(line, buf.toString('utf8')); + }); + [].forEach.call(buf, function(i) { + fi.emit('data', Buffer.from([i])); + }); + assert.strictEqual(callCount, 0); + fi.emit('data', '\n'); + assert.strictEqual(callCount, 1); + rli.close(); + } // Regression test for repl freeze, #1968: // check that nothing fails if 'keypress' event throws. - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: true }); - const keys = []; - fi.on('keypress', function(key) { - keys.push(key); - if (key === 'X') { - throw new Error('bad thing happened'); - } - }); - try { - fi.emit('data', 'fooX'); - } catch (e) { } - fi.emit('data', 'bar'); - assert.strictEqual(keys.join(''), 'fooXbar'); - rli.close(); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: true } + ); + const keys = []; + fi.on('keypress', function(key) { + keys.push(key); + if (key === 'X') { + throw new Error('bad thing happened'); + } + }); + try { + fi.emit('data', 'fooX'); + } catch (e) { } + fi.emit('data', 'bar'); + assert.strictEqual(keys.join(''), 'fooXbar'); + rli.close(); + } // calling readline without `new` - fi = new FakeInput(); - rli = readline.Interface({ input: fi, output: fi, terminal: terminal }); - called = false; - rli.on('line', function(line) { - called = true; - assert.strictEqual(line, 'asdf'); - }); - fi.emit('data', 'asdf\n'); - assert.ok(called); - rli.close(); + { + const fi = new FakeInput(); + const rli = readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + let called = false; + rli.on('line', function(line) { + called = true; + assert.strictEqual(line, 'asdf'); + }); + fi.emit('data', 'asdf\n'); + assert.ok(called); + rli.close(); + } if (terminal) { // question - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - expectedLines = ['foo']; - rli.question(expectedLines[0], function() { + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo']; + rli.question(expectedLines[0], function() { + rli.close(); + }); + const cursorPos = rli._getCursorPos(); + assert.strictEqual(cursorPos.rows, 0); + assert.strictEqual(cursorPos.cols, expectedLines[0].length); rli.close(); - }); - let cursorPos = rli._getCursorPos(); - assert.strictEqual(cursorPos.rows, 0); - assert.strictEqual(cursorPos.cols, expectedLines[0].length); - rli.close(); + } // sending a multi-line question - fi = new FakeInput(); - rli = new readline.Interface({ input: fi, output: fi, terminal: terminal }); - expectedLines = ['foo', 'bar']; - rli.question(expectedLines.join('\n'), function() { + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar']; + rli.question(expectedLines.join('\n'), function() { + rli.close(); + }); + const cursorPos = rli._getCursorPos(); + assert.strictEqual(cursorPos.rows, expectedLines.length - 1); + assert.strictEqual(cursorPos.cols, expectedLines.slice(-1)[0].length); rli.close(); - }); - cursorPos = rli._getCursorPos(); - assert.strictEqual(cursorPos.rows, expectedLines.length - 1); - assert.strictEqual(cursorPos.cols, expectedLines.slice(-1)[0].length); - rli.close(); + } } // isFullWidthCodePoint() should return false for non-numeric values @@ -563,7 +640,10 @@ function isWarned(emitter) { .getStringWidth('\u001b[31m\u001b[39m'), 0); assert.strictEqual(internalReadline.getStringWidth('> '), 2); - assert.deepStrictEqual(fi.listeners(terminal ? 'keypress' : 'data'), []); + { + const fi = new FakeInput(); + assert.deepStrictEqual(fi.listeners(terminal ? 'keypress' : 'data'), []); + } // check EventEmitter memory leak for (let i = 0; i < 12; i++) { @@ -577,35 +657,39 @@ function isWarned(emitter) { } // can create a new readline Interface with a null output arugument - fi = new FakeInput(); - rli = new readline.Interface({input: fi, output: null, terminal: terminal }); + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: null, terminal: terminal } + ); - called = false; - rli.on('line', function(line) { - called = true; - assert.strictEqual(line, 'asdf'); - }); - fi.emit('data', 'asdf\n'); - assert.ok(called); + let called = false; + rli.on('line', function(line) { + called = true; + assert.strictEqual(line, 'asdf'); + }); + fi.emit('data', 'asdf\n'); + assert.ok(called); - assert.doesNotThrow(function() { - rli.setPrompt('ddd> '); - }); + assert.doesNotThrow(function() { + rli.setPrompt('ddd> '); + }); - assert.doesNotThrow(function() { - rli.prompt(); - }); + assert.doesNotThrow(function() { + rli.prompt(); + }); - assert.doesNotThrow(function() { - rli.write('really shouldnt be seeing this'); - }); + assert.doesNotThrow(function() { + rli.write('really shouldnt be seeing this'); + }); - assert.doesNotThrow(function() { - rli.question('What do you think of node.js? ', function(answer) { - console.log('Thank you for your valuable feedback:', answer); - rli.close(); + assert.doesNotThrow(function() { + rli.question('What do you think of node.js? ', function(answer) { + console.log('Thank you for your valuable feedback:', answer); + rli.close(); + }); }); - }); + } { const expected = terminal ? From 3f1bb0a5517552911479e2142f89a333ca5d7000 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Mon, 7 Aug 2017 17:17:31 -0700 Subject: [PATCH 25/97] test: split out load-sensitive readline tests Two test cases in `test-readline-interface` are sensitive to resource constraints (probably due to `\r` and `\n` not arriving within the appropriate delay to be treated as a single line ending). Move those tests to `sequential`. PR-URL: https://github.com/nodejs/node/pull/14681 Fixes: https://github.com/https://github.com/nodejs/node/issues/14674 Reviewed-By: Colin Ihrig Reviewed-By: Refael Ackermann --- test/parallel/test-readline-interface.js | 38 +--------- test/sequential/test-readline-interface.js | 86 ++++++++++++++++++++++ 2 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 test/sequential/test-readline-interface.js diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 20cc20bfc00288..bddbc2053ac3cb 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -22,6 +22,7 @@ // Flags: --expose_internals 'use strict'; const common = require('../common'); + const assert = require('assert'); const readline = require('readline'); const internalReadline = require('internal/readline'); @@ -233,43 +234,6 @@ function isWarned(emitter) { // sending multiple newlines at once that does not end with a new line // and a `end` event(last line is) - // \r\n should emit one line event, not two - { - const fi = new FakeInput(); - const rli = new readline.Interface( - { input: fi, output: fi, terminal: terminal } - ); - const expectedLines = ['foo', 'bar', 'baz', 'bat']; - let callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - fi.emit('data', expectedLines.join('\r\n')); - assert.strictEqual(callCount, expectedLines.length - 1); - rli.close(); - } - - // \r\n should emit one line event when split across multiple writes. - { - const fi = new FakeInput(); - const rli = new readline.Interface( - { input: fi, output: fi, terminal: terminal } - ); - const expectedLines = ['foo', 'bar', 'baz', 'bat']; - let callCount = 0; - rli.on('line', function(line) { - assert.strictEqual(line, expectedLines[callCount]); - callCount++; - }); - expectedLines.forEach(function(line) { - fi.emit('data', `${line}\r`); - fi.emit('data', '\n'); - }); - assert.strictEqual(callCount, expectedLines.length); - rli.close(); - } - // \r should behave like \n when alone { const fi = new FakeInput(); diff --git a/test/sequential/test-readline-interface.js b/test/sequential/test-readline-interface.js new file mode 100644 index 00000000000000..915bcdd0c0750c --- /dev/null +++ b/test/sequential/test-readline-interface.js @@ -0,0 +1,86 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Flags: --expose_internals +'use strict'; +require('../common'); + +// These test cases are in `sequential` rather than the analogous test file in +// `parallel` because they become unrelaible under load. The unreliability under +// load was determined empirically when the test cases were in `parallel` by +// running: +// tools/test.py -j 96 --repeat 192 test/parallel/test-readline-interface.js + +const assert = require('assert'); +const readline = require('readline'); +const EventEmitter = require('events').EventEmitter; +const inherits = require('util').inherits; + +function FakeInput() { + EventEmitter.call(this); +} +inherits(FakeInput, EventEmitter); +FakeInput.prototype.resume = () => {}; +FakeInput.prototype.pause = () => {}; +FakeInput.prototype.write = () => {}; +FakeInput.prototype.end = () => {}; + +[ true, false ].forEach(function(terminal) { + // sending multiple newlines at once that does not end with a new line + // and a `end` event(last line is) + + // \r\n should emit one line event, not two + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar', 'baz', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + fi.emit('data', expectedLines.join('\r\n')); + assert.strictEqual(callCount, expectedLines.length - 1); + rli.close(); + } + + // \r\n should emit one line event when split across multiple writes. + { + const fi = new FakeInput(); + const rli = new readline.Interface( + { input: fi, output: fi, terminal: terminal } + ); + const expectedLines = ['foo', 'bar', 'baz', 'bat']; + let callCount = 0; + rli.on('line', function(line) { + assert.strictEqual(line, expectedLines[callCount]); + callCount++; + }); + expectedLines.forEach(function(line) { + fi.emit('data', `${line}\r`); + fi.emit('data', '\n'); + }); + assert.strictEqual(callCount, expectedLines.length); + rli.close(); + } +}); From 033773c17bd20fb53b1fd9fdbfa2ada019580104 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 10 Aug 2017 20:48:18 +0200 Subject: [PATCH 26/97] src: add overlooked handle to cleanup The `Environment::destroy_ids_timer_handle` should be cleaned up by `Environment::CleanupHandles()`. Fix that by adding it to the list. This partially fixes a cctest. Ref: https://github.com/nodejs/node/issues/14206 PR-URL: https://github.com/nodejs/node/pull/14749 Reviewed-By: Refael Ackermann Reviewed-By: Ben Noordhuis Reviewed-By: Ben Noordhuis Reviewed-By: Daniel Bevenius --- src/env.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/env.cc b/src/env.cc index 0087f719dc00db..5bd75abab34512 100644 --- a/src/env.cc +++ b/src/env.cc @@ -76,6 +76,10 @@ void Environment::Start(int argc, reinterpret_cast(&idle_check_handle_), close_and_finish, nullptr); + RegisterHandleCleanup( + reinterpret_cast(&destroy_ids_timer_handle_), + close_and_finish, + nullptr); if (start_profiler_idle_notifier) { StartProfilerIdleNotifier(); From 0bc3124c801cc2741a10b2b5e0e8dc8d83d837ba Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 10 Aug 2017 20:50:27 +0200 Subject: [PATCH 27/97] test: properly order freeing resources in cctest The `IsolateData` instance is created before the `Environment` instance, so free in reverse order. Fixes: https://github.com/nodejs/node/issues/14206 PR-URL: https://github.com/nodejs/node/pull/14749 Reviewed-By: Refael Ackermann Reviewed-By: Ben Noordhuis Reviewed-By: Ben Noordhuis Reviewed-By: Daniel Bevenius --- test/cctest/test_environment.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc index db08743261c843..4ae9296c12e760 100644 --- a/test/cctest/test_environment.cc +++ b/test/cctest/test_environment.cc @@ -41,9 +41,9 @@ class EnvironmentTest : public NodeTestFixture { } ~Env() { - FreeIsolateData(isolate_data_); environment_->CleanupHandles(); FreeEnvironment(environment_); + FreeIsolateData(isolate_data_); } Environment* operator*() const { From 8ff2a5c3384b431ba71c035428a66fcdbee90022 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 10 Aug 2017 20:51:33 +0200 Subject: [PATCH 28/97] Revert "test: add DISABLED_ prefix to commented out test" This reverts commit 75bf8a9db9e6005c6b5a5beb086a9def6e1c1b76. Ref: https://github.com/nodejs/node/issues/14206 Ref: https://github.com/nodejs/node/pull/14317 PR-URL: https://github.com/nodejs/node/pull/14749 Reviewed-By: Refael Ackermann Reviewed-By: Ben Noordhuis Reviewed-By: Ben Noordhuis Reviewed-By: Daniel Bevenius --- test/cctest/test_environment.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc index 4ae9296c12e760..8d22e14e000227 100644 --- a/test/cctest/test_environment.cc +++ b/test/cctest/test_environment.cc @@ -85,7 +85,8 @@ TEST_F(EnvironmentTest, AtExitWithArgument) { EXPECT_EQ(arg, cb_1_arg); } -TEST_F(EnvironmentTest, DISABLED_MultipleEnvironmentsPerIsolate) { +/* +TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) { const v8::HandleScope handle_scope(isolate_); const Argv argv; Env env1 {handle_scope, isolate_, argv}; @@ -100,6 +101,7 @@ TEST_F(EnvironmentTest, DISABLED_MultipleEnvironmentsPerIsolate) { RunAtExit(*env2); EXPECT_TRUE(called_cb_2); } +*/ static void at_exit_callback1(void* arg) { called_cb_1 = true; From a781bb45081f25cfcdd1221e5b734a0228c16d2c Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 10 Aug 2017 20:51:59 +0200 Subject: [PATCH 29/97] Revert "test: disable MultipleEnvironmentsPerIsolate" This reverts commit 95ab966a742d23d7271c7b4c36fb84aa2bbece59. Ref: https://github.com/nodejs/node/issues/14206 Ref: https://github.com/nodejs/node/pull/14246 PR-URL: https://github.com/nodejs/node/pull/14749 Reviewed-By: Refael Ackermann Reviewed-By: Ben Noordhuis Reviewed-By: Ben Noordhuis Reviewed-By: Daniel Bevenius --- test/cctest/test_environment.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc index 8d22e14e000227..aee8e795ecb6ab 100644 --- a/test/cctest/test_environment.cc +++ b/test/cctest/test_environment.cc @@ -85,7 +85,6 @@ TEST_F(EnvironmentTest, AtExitWithArgument) { EXPECT_EQ(arg, cb_1_arg); } -/* TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) { const v8::HandleScope handle_scope(isolate_); const Argv argv; @@ -101,7 +100,6 @@ TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) { RunAtExit(*env2); EXPECT_TRUE(called_cb_2); } -*/ static void at_exit_callback1(void* arg) { called_cb_1 = true; From c5ee34e39b2e855bd70f83c83120c20e1c5a5f41 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sun, 30 Jul 2017 18:09:13 +0800 Subject: [PATCH 30/97] encoding: rudimentary TextDecoder support w/o ICU Also split up the tests. Backport-PR-URL: https://github.com/nodejs/node/pull/14786 Backport-Reviewed-By: Anna Henningsen PR-URL: https://github.com/nodejs/node/pull/14489 Reviewed-By: James M Snell Reviewed-By: Refael Ackermann --- doc/api/errors.md | 7 + doc/api/intl.md | 2 +- doc/api/util.md | 61 +-- lib/internal/encoding.js | 366 ++++++++++++------ lib/internal/errors.js | 1 + ...g.js => test-whatwg-encoding-internals.js} | 119 +----- .../test-whatwg-encoding-textdecoder.js | 104 +++++ .../test-whatwg-encoding-textencoder.js | 36 ++ 8 files changed, 428 insertions(+), 268 deletions(-) rename test/parallel/{test-whatwg-encoding.js => test-whatwg-encoding-internals.js} (56%) create mode 100644 test/parallel/test-whatwg-encoding-textdecoder.js create mode 100644 test/parallel/test-whatwg-encoding-textencoder.js diff --git a/doc/api/errors.md b/doc/api/errors.md index da63708dd49923..d9ddc299c9d9df 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -712,6 +712,12 @@ only used in the [WHATWG URL API][] for strict compliance with the specification native Node.js APIs, `func(undefined)` and `func()` are treated identically, and the [`ERR_INVALID_ARG_TYPE`][] error code may be used instead. + +### ERR_NO_ICU + +Used when an attempt is made to use features that require [ICU][], while +Node.js is not compiled with ICU support. + ### ERR_SOCKET_ALREADY_BOUND Used when an attempt is made to bind a socket that has already been bound. @@ -795,6 +801,7 @@ are most likely an indication of a bug within Node.js itself. [`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable [`process.on('uncaughtException')`]: process.html#process_event_uncaughtexception [`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback +[ICU]: intl.html#intl_internationalization_support [Node.js Error Codes]: #nodejs-error-codes [V8's stack trace API]: https://github.com/v8/v8/wiki/Stack-Trace-API [WHATWG URL API]: url.html#url_the_whatwg_url_api diff --git a/doc/api/intl.md b/doc/api/intl.md index 753932c91c25f7..faa5429bffdf06 100644 --- a/doc/api/intl.md +++ b/doc/api/intl.md @@ -52,7 +52,7 @@ option: | [WHATWG URL Parser][] | partial (no IDN support) | full | full | full | [`require('buffer').transcode()`][] | none (function does not exist) | full | full | full | [REPL][] | partial (inaccurate line editing) | full | full | full -| [`require('util').TextDecoder`][] | none (class does not exist) | partial/full (depends on OS) | partial (Unicode-only) | full +| [`require('util').TextDecoder`][] | partial (basic encodings support) | partial/full (depends on OS) | partial (Unicode-only) | full *Note*: The "(not locale-aware)" designation denotes that the function carries out its operation just like the non-`Locale` version of the function, if one diff --git a/doc/api/util.md b/doc/api/util.md index c03b3f769d78da..ee32211d9bbdd4 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -544,7 +544,7 @@ added: v8.0.0 A Symbol that can be used to declare custom promisified variants of functions, see [Custom promisified functions][]. -### Class: util.TextDecoder +## Class: util.TextDecoder @@ -563,23 +563,33 @@ while (buffer = getNextChunkSomehow()) { string += decoder.decode(); // end-of-stream ``` -#### WHATWG Supported Encodings +### WHATWG Supported Encodings Per the [WHATWG Encoding Standard][], the encodings supported by the `TextDecoder` API are outlined in the tables below. For each encoding, -one or more aliases may be used. Support for some encodings is enabled -only when Node.js is using the full ICU data (see [Internationalization][]). -`util.TextDecoder` is `undefined` when ICU is not enabled during build. +one or more aliases may be used. -##### Encodings Supported By Default +Different Node.js build configurations support different sets of encodings. +While a very basic set of encodings is supported even on Node.js builds without +ICU enabled, support for some encodings is provided only when Node.js is built +with ICU and using the full ICU data (see [Internationalization][]). + +#### Encodings Supported Without ICU | Encoding | Aliases | | ----------- | --------------------------------- | -| `'utf8'` | `'unicode-1-1-utf-8'`, `'utf-8'` | -| `'utf-16be'`| | +| `'utf-8'` | `'unicode-1-1-utf-8'`, `'utf8'` | | `'utf-16le'`| `'utf-16'` | -##### Encodings Requiring Full-ICU +#### Encodings Supported by Default (With ICU) + +| Encoding | Aliases | +| ----------- | --------------------------------- | +| `'utf-8'` | `'unicode-1-1-utf-8'`, `'utf8'` | +| `'utf-16le'`| `'utf-16'` | +| `'utf-16be'`| | + +#### Encodings Requiring Full ICU Data | Encoding | Aliases | | ----------------- | -------------------------------- | @@ -621,13 +631,14 @@ only when Node.js is using the full ICU data (see [Internationalization][]). *Note*: The `'iso-8859-16'` encoding listed in the [WHATWG Encoding Standard][] is not supported. -#### new TextDecoder([encoding[, options]]) +### new TextDecoder([encoding[, options]]) * `encoding` {string} Identifies the `encoding` that this `TextDecoder` instance supports. Defaults to `'utf-8'`. * `options` {Object} * `fatal` {boolean} `true` if decoding failures are fatal. Defaults to - `false`. + `false`. This option is only supported when ICU is enabled (see + [Internationalization][]). * `ignoreBOM` {boolean} When `true`, the `TextDecoder` will include the byte order mark in the decoded result. When `false`, the byte order mark will be removed from the output. This option is only used when `encoding` is @@ -636,7 +647,7 @@ is not supported. Creates an new `TextDecoder` instance. The `encoding` may specify one of the supported encodings or an alias. -#### textDecoder.decode([input[, options]]) +### textDecoder.decode([input[, options]]) * `input` {ArrayBuffer|DataView|TypedArray} An `ArrayBuffer`, `DataView` or Typed Array instance containing the encoded data. @@ -652,27 +663,27 @@ internally and emitted after the next call to `textDecoder.decode()`. If `textDecoder.fatal` is `true`, decoding errors that occur will result in a `TypeError` being thrown. -#### textDecoder.encoding +### textDecoder.encoding -* Value: {string} +* {string} The encoding supported by the `TextDecoder` instance. -#### textDecoder.fatal +### textDecoder.fatal -* Value: {boolean} +* {boolean} The value will be `true` if decoding errors result in a `TypeError` being thrown. -#### textDecoder.ignoreBOM +### textDecoder.ignoreBOM -* Value: {boolean} +* {boolean} The value will be `true` if the decoding result will include the byte order mark. -### Class: util.TextEncoder +## Class: util.TextEncoder @@ -680,21 +691,27 @@ added: v8.3.0 > Stability: 1 - Experimental An implementation of the [WHATWG Encoding Standard][] `TextEncoder` API. All -instances of `TextEncoder` only support `UTF-8` encoding. +instances of `TextEncoder` only support UTF-8 encoding. ```js const encoder = new TextEncoder(); const uint8array = encoder.encode('this is some data'); ``` -#### textEncoder.encode([input]) +### textEncoder.encode([input]) * `input` {string} The text to encode. Defaults to an empty string. * Returns: {Uint8Array} -UTF-8 Encodes the `input` string and returns a `Uint8Array` containing the +UTF-8 encodes the `input` string and returns a `Uint8Array` containing the encoded bytes. +### textDecoder.encoding + +* {string} + +The encoding supported by the `TextEncoder` instance. Always set to `'utf-8'`. + ## Deprecated APIs The following APIs have been deprecated and should no longer be used. Existing diff --git a/lib/internal/encoding.js b/lib/internal/encoding.js index 22ae5c6c0db1ab..de4ebcf196284b 100644 --- a/lib/internal/encoding.js +++ b/lib/internal/encoding.js @@ -28,11 +28,12 @@ const { encodeUtf8String } = process.binding('buffer'); -const { - decode: _decode, - getConverter, - hasConverter -} = process.binding('icu'); +var Buffer; +function lazyBuffer() { + if (Buffer === undefined) + Buffer = require('buffer').Buffer; + return Buffer; +} const CONVERTER_FLAGS_FLUSH = 0x1; const CONVERTER_FLAGS_FATAL = 0x2; @@ -284,122 +285,14 @@ function getEncodingFromLabel(label) { return encodings.get(trimAsciiWhitespace(label.toLowerCase())); } -function hasTextDecoder(encoding = 'utf-8') { - if (typeof encoding !== 'string') - throw new errors.Error('ERR_INVALID_ARG_TYPE', 'encoding', 'string'); - return hasConverter(getEncodingFromLabel(encoding)); -} - -var Buffer; -function lazyBuffer() { - if (Buffer === undefined) - Buffer = require('buffer').Buffer; - return Buffer; -} - -class TextDecoder { - constructor(encoding = 'utf-8', options = {}) { - if (!warned) { - warned = true; - process.emitWarning(experimental, 'ExperimentalWarning'); - } - - encoding = `${encoding}`; - if (typeof options !== 'object') - throw new errors.Error('ERR_INVALID_ARG_TYPE', 'options', 'object'); - - const enc = getEncodingFromLabel(encoding); - if (enc === undefined) - throw new errors.RangeError('ERR_ENCODING_NOT_SUPPORTED', encoding); - - var flags = 0; - if (options !== null) { - flags |= options.fatal ? CONVERTER_FLAGS_FATAL : 0; - flags |= options.ignoreBOM ? CONVERTER_FLAGS_IGNORE_BOM : 0; - } - - const handle = getConverter(enc, flags); - if (handle === undefined) - throw new errors.Error('ERR_ENCODING_NOT_SUPPORTED', encoding); - - this[kHandle] = handle; - this[kFlags] = flags; - this[kEncoding] = enc; - } - - get encoding() { - if (this == null || this[kDecoder] !== true) - throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); - return this[kEncoding]; - } - - get fatal() { - if (this == null || this[kDecoder] !== true) - throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); - return (this[kFlags] & CONVERTER_FLAGS_FATAL) === CONVERTER_FLAGS_FATAL; - } - - get ignoreBOM() { - if (this == null || this[kDecoder] !== true) - throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); - return (this[kFlags] & CONVERTER_FLAGS_IGNORE_BOM) === - CONVERTER_FLAGS_IGNORE_BOM; - } - - decode(input = empty, options = {}) { - if (this == null || this[kDecoder] !== true) - throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); - if (isArrayBuffer(input)) { - input = lazyBuffer().from(input); - } else if (!ArrayBuffer.isView(input)) { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'input', - ['ArrayBuffer', 'ArrayBufferView']); - } - if (typeof options !== 'object') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); - } - - var flags = 0; - if (options !== null) - flags |= options.stream ? 0 : CONVERTER_FLAGS_FLUSH; - - const ret = _decode(this[kHandle], input, flags); - if (typeof ret === 'number') { - const err = new errors.TypeError('ERR_ENCODING_INVALID_ENCODED_DATA', - this.encoding); - err.errno = ret; - throw err; - } - return ret.toString('ucs2'); - } - - [inspect](depth, opts) { - if (this == null || this[kDecoder] !== true) - throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); - if (typeof depth === 'number' && depth < 0) - return opts.stylize('[Object]', 'special'); - var ctor = getConstructorOf(this); - var obj = Object.create({ - constructor: ctor === null ? TextDecoder : ctor - }); - obj.encoding = this.encoding; - obj.fatal = this.fatal; - obj.ignoreBOM = this.ignoreBOM; - if (opts.showHidden) { - obj[kFlags] = this[kFlags]; - obj[kHandle] = this[kHandle]; - } - // Lazy to avoid circular dependency - return require('util').inspect(obj, opts); - } -} - class TextEncoder { constructor() { if (!warned) { warned = true; process.emitWarning(experimental, 'ExperimentalWarning'); } + + this[kEncoder] = true; } get encoding() { @@ -429,20 +322,8 @@ class TextEncoder { } } -Object.defineProperties( - TextDecoder.prototype, { - [kDecoder]: { enumerable: false, value: true, configurable: false }, - 'decode': { enumerable: true }, - 'encoding': { enumerable: true }, - 'fatal': { enumerable: true }, - 'ignoreBOM': { enumerable: true }, - [Symbol.toStringTag]: { - configurable: true, - value: 'TextDecoder' - } }); Object.defineProperties( TextEncoder.prototype, { - [kEncoder]: { enumerable: false, value: true, configurable: false }, 'encode': { enumerable: true }, 'encoding': { enumerable: true }, [Symbol.toStringTag]: { @@ -450,6 +331,237 @@ Object.defineProperties( value: 'TextEncoder' } }); +const { hasConverter, TextDecoder } = + process.binding('config').hasIntl ? + makeTextDecoderICU() : + makeTextDecoderJS(); + +function hasTextDecoder(encoding = 'utf-8') { + if (typeof encoding !== 'string') + throw new errors.Error('ERR_INVALID_ARG_TYPE', 'encoding', 'string'); + return hasConverter(getEncodingFromLabel(encoding)); +} + +function makeTextDecoderICU() { + const { + decode: _decode, + getConverter, + hasConverter + } = process.binding('icu'); + + class TextDecoder { + constructor(encoding = 'utf-8', options = {}) { + if (!warned) { + warned = true; + process.emitWarning(experimental, 'ExperimentalWarning'); + } + + encoding = `${encoding}`; + if (typeof options !== 'object') + throw new errors.Error('ERR_INVALID_ARG_TYPE', 'options', 'object'); + + const enc = getEncodingFromLabel(encoding); + if (enc === undefined) + throw new errors.RangeError('ERR_ENCODING_NOT_SUPPORTED', encoding); + + var flags = 0; + if (options !== null) { + flags |= options.fatal ? CONVERTER_FLAGS_FATAL : 0; + flags |= options.ignoreBOM ? CONVERTER_FLAGS_IGNORE_BOM : 0; + } + + const handle = getConverter(enc, flags); + if (handle === undefined) + throw new errors.Error('ERR_ENCODING_NOT_SUPPORTED', encoding); + + this[kDecoder] = true; + this[kHandle] = handle; + this[kFlags] = flags; + this[kEncoding] = enc; + } + + + decode(input = empty, options = {}) { + if (this == null || this[kDecoder] !== true) + throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); + if (isArrayBuffer(input)) { + input = lazyBuffer().from(input); + } else if (!ArrayBuffer.isView(input)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'input', + ['ArrayBuffer', 'ArrayBufferView']); + } + if (typeof options !== 'object') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); + } + + var flags = 0; + if (options !== null) + flags |= options.stream ? 0 : CONVERTER_FLAGS_FLUSH; + + const ret = _decode(this[kHandle], input, flags); + if (typeof ret === 'number') { + const err = new errors.TypeError('ERR_ENCODING_INVALID_ENCODED_DATA', + this.encoding); + err.errno = ret; + throw err; + } + return ret.toString('ucs2'); + } + } + + return { hasConverter, TextDecoder }; +} + +function makeTextDecoderJS() { + var StringDecoder; + function lazyStringDecoder() { + if (StringDecoder === undefined) + ({ StringDecoder } = require('string_decoder')); + return StringDecoder; + } + + const kBOMSeen = Symbol('BOM seen'); + + function hasConverter(encoding) { + return encoding === 'utf-8' || encoding === 'utf-16le'; + } + + class TextDecoder { + constructor(encoding = 'utf-8', options = {}) { + if (!warned) { + warned = true; + process.emitWarning(experimental, 'ExperimentalWarning'); + } + + encoding = `${encoding}`; + if (typeof options !== 'object') + throw new errors.Error('ERR_INVALID_ARG_TYPE', 'options', 'object'); + + const enc = getEncodingFromLabel(encoding); + if (enc === undefined || !hasConverter(enc)) + throw new errors.RangeError('ERR_ENCODING_NOT_SUPPORTED', encoding); + + var flags = 0; + if (options !== null) { + if (options.fatal) { + throw new errors.TypeError('ERR_NO_ICU', '"fatal" option'); + } + flags |= options.ignoreBOM ? CONVERTER_FLAGS_IGNORE_BOM : 0; + } + + this[kDecoder] = true; + // StringDecoder will normalize WHATWG encoding to Node.js encoding. + this[kHandle] = new (lazyStringDecoder())(enc); + this[kFlags] = flags; + this[kEncoding] = enc; + this[kBOMSeen] = false; + } + + decode(input = empty, options = {}) { + if (this == null || this[kDecoder] !== true) + throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); + if (isArrayBuffer(input)) { + input = lazyBuffer().from(input); + } else if (ArrayBuffer.isView(input)) { + input = lazyBuffer().from(input.buffer, input.byteOffset, + input.byteLength); + } else { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'input', + ['ArrayBuffer', 'ArrayBufferView']); + } + if (typeof options !== 'object') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); + } + + if (this[kFlags] & CONVERTER_FLAGS_FLUSH) { + this[kBOMSeen] = false; + } + + if (options !== null && options.stream) { + this[kFlags] &= ~CONVERTER_FLAGS_FLUSH; + } else { + this[kFlags] |= CONVERTER_FLAGS_FLUSH; + } + + if (!this[kBOMSeen] && !(this[kFlags] & CONVERTER_FLAGS_IGNORE_BOM)) { + if (this[kEncoding] === 'utf-8') { + if (input.length >= 3 && + input[0] === 0xEF && input[1] === 0xBB && input[2] === 0xBF) { + input = input.slice(3); + } + } else if (this[kEncoding] === 'utf-16le') { + if (input.length >= 2 && input[0] === 0xFF && input[1] === 0xFE) { + input = input.slice(2); + } + } + this[kBOMSeen] = true; + } + + if (this[kFlags] & CONVERTER_FLAGS_FLUSH) { + return this[kHandle].end(input); + } + + return this[kHandle].write(input); + } + } + + return { hasConverter, TextDecoder }; +} + +// Mix in some shared properties. +{ + Object.defineProperties( + TextDecoder.prototype, + Object.getOwnPropertyDescriptors({ + get encoding() { + if (this == null || this[kDecoder] !== true) + throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); + return this[kEncoding]; + }, + + get fatal() { + if (this == null || this[kDecoder] !== true) + throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); + return (this[kFlags] & CONVERTER_FLAGS_FATAL) === CONVERTER_FLAGS_FATAL; + }, + + get ignoreBOM() { + if (this == null || this[kDecoder] !== true) + throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); + return (this[kFlags] & CONVERTER_FLAGS_IGNORE_BOM) === + CONVERTER_FLAGS_IGNORE_BOM; + }, + + [inspect](depth, opts) { + if (this == null || this[kDecoder] !== true) + throw new errors.TypeError('ERR_INVALID_THIS', 'TextDecoder'); + if (typeof depth === 'number' && depth < 0) + return opts.stylize('[Object]', 'special'); + var ctor = getConstructorOf(this); + var obj = Object.create({ + constructor: ctor === null ? TextDecoder : ctor + }); + obj.encoding = this.encoding; + obj.fatal = this.fatal; + obj.ignoreBOM = this.ignoreBOM; + if (opts.showHidden) { + obj[kFlags] = this[kFlags]; + obj[kHandle] = this[kHandle]; + } + // Lazy to avoid circular dependency + return require('util').inspect(obj, opts); + } + })); + Object.defineProperties(TextDecoder.prototype, { + decode: { enumerable: true }, + [inspect]: { enumerable: false }, + [Symbol.toStringTag]: { + configurable: true, + value: 'TextDecoder' + } + }); +} + module.exports = { getEncodingFromLabel, hasTextDecoder, diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 3ee34cf428939a..3b54dcea934f83 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -148,6 +148,7 @@ E('ERR_MISSING_ARGS', missingArgs); E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function'); E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object'); E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support'); +E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU'); E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s'); E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound'); E('ERR_SOCKET_BAD_TYPE', diff --git a/test/parallel/test-whatwg-encoding.js b/test/parallel/test-whatwg-encoding-internals.js similarity index 56% rename from test/parallel/test-whatwg-encoding.js rename to test/parallel/test-whatwg-encoding-internals.js index c181df860ca149..d5bf07acb31803 100644 --- a/test/parallel/test-whatwg-encoding.js +++ b/test/parallel/test-whatwg-encoding-internals.js @@ -1,129 +1,12 @@ // Flags: --expose-internals 'use strict'; -const common = require('../common'); +require('../common'); const assert = require('assert'); -const { TextEncoder, TextDecoder } = require('util'); -const { customInspectSymbol: inspect } = require('internal/util'); const { getEncodingFromLabel } = require('internal/encoding'); -const encoded = Buffer.from([0xef, 0xbb, 0xbf, 0x74, 0x65, - 0x73, 0x74, 0xe2, 0x82, 0xac]); - -if (!common.hasIntl) { - common.skip('WHATWG Encoding tests because ICU is not present.'); -} - -// Make Sure TextDecoder and TextEncoder exist -assert(TextDecoder); -assert(TextEncoder); - -// Test TextEncoder -const enc = new TextEncoder(); -assert(enc); -const buf = enc.encode('\ufefftest€'); - -assert.strictEqual(Buffer.compare(buf, encoded), 0); - - -// Test TextDecoder, UTF-8, fatal: false, ignoreBOM: false -{ - ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { - const dec = new TextDecoder(i); - const res = dec.decode(buf); - assert.strictEqual(res, 'test€'); - }); - - ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { - const dec = new TextDecoder(i); - let res = ''; - res += dec.decode(buf.slice(0, 8), { stream: true }); - res += dec.decode(buf.slice(8)); - assert.strictEqual(res, 'test€'); - }); -} - -// Test TextDecoder, UTF-8, fatal: false, ignoreBOM: true -{ - ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { - const dec = new TextDecoder(i, { ignoreBOM: true }); - const res = dec.decode(buf); - assert.strictEqual(res, '\ufefftest€'); - }); - - ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { - const dec = new TextDecoder(i, { ignoreBOM: true }); - let res = ''; - res += dec.decode(buf.slice(0, 8), { stream: true }); - res += dec.decode(buf.slice(8)); - assert.strictEqual(res, '\ufefftest€'); - }); -} - -// Test TextDecoder, UTF-8, fatal: true, ignoreBOM: false -{ - ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { - const dec = new TextDecoder(i, { fatal: true }); - assert.throws(() => dec.decode(buf.slice(0, 8)), - common.expectsError({ - code: 'ERR_ENCODING_INVALID_ENCODED_DATA', - type: TypeError, - message: - /^The encoded data was not valid for encoding utf-8$/ - })); - }); - - ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { - const dec = new TextDecoder(i, { fatal: true }); - assert.doesNotThrow(() => dec.decode(buf.slice(0, 8), { stream: true })); - assert.doesNotThrow(() => dec.decode(buf.slice(8))); - }); -} - -// Test TextDecoder, UTF-16le -{ - const dec = new TextDecoder('utf-16le'); - const res = dec.decode(Buffer.from('test€', 'utf-16le')); - assert.strictEqual(res, 'test€'); -} - -// Test TextDecoder, UTF-16be -{ - const dec = new TextDecoder('utf-16be'); - const res = dec.decode(Buffer.from([0x00, 0x74, 0x00, 0x65, 0x00, - 0x73, 0x00, 0x74, 0x20, 0xac])); - assert.strictEqual(res, 'test€'); -} - -{ - const fn = TextDecoder.prototype[inspect]; - fn.call(new TextDecoder(), Infinity, {}); - - [{}, [], true, 1, '', new TextEncoder()].forEach((i) => { - assert.throws(() => fn.call(i, Infinity, {}), - common.expectsError({ - code: 'ERR_INVALID_THIS', - message: 'Value of "this" must be of type TextDecoder' - })); - }); -} - -{ - const fn = TextEncoder.prototype[inspect]; - fn.call(new TextEncoder(), Infinity, {}); - - [{}, [], true, 1, '', new TextDecoder()].forEach((i) => { - assert.throws(() => fn.call(i, Infinity, {}), - common.expectsError({ - code: 'ERR_INVALID_THIS', - message: 'Value of "this" must be of type TextEncoder' - })); - }); -} - // Test Encoding Mappings { - const mappings = { 'utf-8': [ 'unicode-1-1-utf-8', diff --git a/test/parallel/test-whatwg-encoding-textdecoder.js b/test/parallel/test-whatwg-encoding-textdecoder.js new file mode 100644 index 00000000000000..440ccc38124ec6 --- /dev/null +++ b/test/parallel/test-whatwg-encoding-textdecoder.js @@ -0,0 +1,104 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { TextDecoder, TextEncoder } = require('util'); +const { customInspectSymbol: inspect } = require('internal/util'); + +const buf = Buffer.from([0xef, 0xbb, 0xbf, 0x74, 0x65, + 0x73, 0x74, 0xe2, 0x82, 0xac]); + +// Make Sure TextDecoder exist +assert(TextDecoder); + +// Test TextDecoder, UTF-8, fatal: false, ignoreBOM: false +{ + ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { + const dec = new TextDecoder(i); + const res = dec.decode(buf); + assert.strictEqual(res, 'test€'); + }); + + ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { + const dec = new TextDecoder(i); + let res = ''; + res += dec.decode(buf.slice(0, 8), { stream: true }); + res += dec.decode(buf.slice(8)); + assert.strictEqual(res, 'test€'); + }); +} + +// Test TextDecoder, UTF-8, fatal: false, ignoreBOM: true +{ + ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { + const dec = new TextDecoder(i, { ignoreBOM: true }); + const res = dec.decode(buf); + assert.strictEqual(res, '\ufefftest€'); + }); + + ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { + const dec = new TextDecoder(i, { ignoreBOM: true }); + let res = ''; + res += dec.decode(buf.slice(0, 8), { stream: true }); + res += dec.decode(buf.slice(8)); + assert.strictEqual(res, '\ufefftest€'); + }); +} + +// Test TextDecoder, UTF-8, fatal: true, ignoreBOM: false +if (common.hasIntl) { + ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { + const dec = new TextDecoder(i, { fatal: true }); + assert.throws(() => dec.decode(buf.slice(0, 8)), + common.expectsError({ + code: 'ERR_ENCODING_INVALID_ENCODED_DATA', + type: TypeError, + message: 'The encoded data was not valid for encoding utf-8' + })); + }); + + ['unicode-1-1-utf-8', 'utf8', 'utf-8'].forEach((i) => { + const dec = new TextDecoder(i, { fatal: true }); + assert.doesNotThrow(() => dec.decode(buf.slice(0, 8), { stream: true })); + assert.doesNotThrow(() => dec.decode(buf.slice(8))); + }); +} else { + assert.throws( + () => new TextDecoder('utf-8', { fatal: true }), + common.expectsError({ + code: 'ERR_NO_ICU', + type: TypeError, + message: '"fatal" option is not supported on Node.js compiled without ICU' + })); +} + +// Test TextDecoder, UTF-16le +{ + const dec = new TextDecoder('utf-16le'); + const res = dec.decode(Buffer.from('test€', 'utf-16le')); + assert.strictEqual(res, 'test€'); +} + +// Test TextDecoder, UTF-16be +if (common.hasIntl) { + const dec = new TextDecoder('utf-16be'); + const res = dec.decode(Buffer.from('test€', 'utf-16le').swap16()); + assert.strictEqual(res, 'test€'); +} + +{ + const fn = TextDecoder.prototype[inspect]; + assert.doesNotThrow(() => { + fn.call(new TextDecoder(), Infinity, {}); + }); + + [{}, [], true, 1, '', new TextEncoder()].forEach((i) => { + assert.throws(() => fn.call(i, Infinity, {}), + common.expectsError({ + code: 'ERR_INVALID_THIS', + type: TypeError, + message: 'Value of "this" must be of type TextDecoder' + })); + }); +} diff --git a/test/parallel/test-whatwg-encoding-textencoder.js b/test/parallel/test-whatwg-encoding-textencoder.js new file mode 100644 index 00000000000000..cf2769bb0ce577 --- /dev/null +++ b/test/parallel/test-whatwg-encoding-textencoder.js @@ -0,0 +1,36 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { TextDecoder, TextEncoder } = require('util'); +const { customInspectSymbol: inspect } = require('internal/util'); + +const encoded = Buffer.from([0xef, 0xbb, 0xbf, 0x74, 0x65, + 0x73, 0x74, 0xe2, 0x82, 0xac]); + +// Make Sure TextEncoder exists +assert(TextEncoder); + +// Test TextEncoder +const enc = new TextEncoder(); +assert(enc); +const buf = enc.encode('\ufefftest€'); + +assert.strictEqual(Buffer.compare(buf, encoded), 0); + +{ + const fn = TextEncoder.prototype[inspect]; + assert.doesNotThrow(() => { + fn.call(new TextEncoder(), Infinity, {}); + }); + + [{}, [], true, 1, '', new TextDecoder()].forEach((i) => { + assert.throws(() => fn.call(i, Infinity, {}), + common.expectsError({ + code: 'ERR_INVALID_THIS', + type: TypeError, + message: 'Value of "this" must be of type TextEncoder' + })); + }); +} From 977e22857a496e5f99480e443f2dc761dbc0913c Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Tue, 8 Aug 2017 09:53:55 -0700 Subject: [PATCH 31/97] test: make test-tls-connect checks more strict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check the error code on expected errors so that the introduction of different errors in refactoring is caught. While at it, re-order modules alphabetically per test-writing guide. PR-URL: https://github.com/nodejs/node/pull/14695 Reviewed-By: Refael Ackermann Reviewed-By: Tobias Nießen Reviewed-By: Luigi Pinca Reviewed-By: Colin Ihrig --- test/parallel/test-tls-connect.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-tls-connect.js b/test/parallel/test-tls-connect.js index 5e73196bc1f8a2..304b55f1d6de3a 100644 --- a/test/parallel/test-tls-connect.js +++ b/test/parallel/test-tls-connect.js @@ -25,9 +25,10 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const tls = require('tls'); +const assert = require('assert'); const fs = require('fs'); const path = require('path'); +const tls = require('tls'); // https://github.com/joyent/node/issues/1218 // uncatchable exception on TLS connection error @@ -38,7 +39,10 @@ const path = require('path'); const options = {cert: cert, key: key, port: common.PORT}; const conn = tls.connect(options, common.mustNotCall()); - conn.on('error', common.mustCall()); + conn.on( + 'error', + common.mustCall((e) => { assert.strictEqual(e.code, 'ECONNREFUSED'); }) + ); } // SSL_accept/SSL_connect error handling @@ -53,5 +57,8 @@ const path = require('path'); ciphers: 'rick-128-roll' }, common.mustNotCall()); - conn.on('error', common.mustCall()); + conn.on( + 'error', + common.mustCall((e) => { assert.strictEqual(e.code, 'ECONNREFUSED'); }) + ); } From 139b08863eb9ab115ed3435c52408f44693f33cf Mon Sep 17 00:00:00 2001 From: Gareth Ellis Date: Fri, 4 Aug 2017 13:35:13 +0200 Subject: [PATCH 32/97] benchmark: Correct constructor for freelist Updates to use current constructor for freelist, which was changed under pr #12644 Ref: https://github.com/nodejs/node/pull/12644 PR-URL: https://github.com/nodejs/node/pull/14627 Reviewed-By: Luigi Pinca Reviewed-By: James M Snell Reviewed-By: Gibson Fahnestock Reviewed-By: Colin Ihrig Reviewed-By: Anna Henningsen --- benchmark/misc/freelist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/misc/freelist.js b/benchmark/misc/freelist.js index f30b814e7ea983..8c4aeb05ef0472 100644 --- a/benchmark/misc/freelist.js +++ b/benchmark/misc/freelist.js @@ -9,7 +9,7 @@ var bench = common.createBenchmark(main, { }); function main(conf) { - const FreeList = require('internal/freelist').FreeList; + const FreeList = require('internal/freelist'); var n = conf.n; var poolSize = 1000; var list = new FreeList('test', poolSize, Object); From 2d806f4f71edafb305605e82b54e283e9e395d17 Mon Sep 17 00:00:00 2001 From: Alexey Kozyatinskiy Date: Tue, 1 Aug 2017 14:17:31 -0700 Subject: [PATCH 33/97] deps: cherry-pick f19b889 from V8 upstream Original commit message: [inspector] support for cases when embedder doesn't call contextDestroyed Node.js doesn't have good place to call contextDestroyed. We need to cleanup everything on our side to allow clients to not call contextDestroyed method. R=dgozman@chromium.org,eostroukhov@chromium.com Bug: none Change-Id: Ibe3f01fd18afbfa579e5db66ab6f174d5fad7c82 Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel;master.tryserver.chromium.linux:linux_chromium_rel_ng Reviewed-on: https://chromium-review.googlesource.com/575519 Reviewed-by: Dmitry Gozman Commit-Queue: Aleksey Kozyatinskiy Cr-Original-Commit-Position: refs/heads/master@{#46849} Reviewed-on: https://chromium-review.googlesource.com/596549 Cr-Commit-Position: refs/heads/master@{#47060} Ref: https://chromium.googlesource.com/v8/v8.git/+/f19b889be801bdebc04c49090e37c787f7ba8805 PR-URL: https://github.com/nodejs/node/pull/14465 Reviewed-By: Jan Krems Reviewed-By: Timothy Gu Reviewed-By: Aleksey Kozyatinskiy --- deps/v8/src/inspector/inspected-context.cc | 41 +++++++++++++++++++ deps/v8/src/inspector/inspected-context.h | 3 ++ deps/v8/src/inspector/v8-inspector-impl.cc | 4 ++ deps/v8/src/inspector/v8-inspector-impl.h | 1 + deps/v8/test/inspector/inspector-test.cc | 9 ++++ ...estroyed-on-context-collected-expected.txt | 7 ++++ .../context-destroyed-on-context-collected.js | 14 +++++++ 7 files changed, 79 insertions(+) create mode 100644 deps/v8/test/inspector/runtime/context-destroyed-on-context-collected-expected.txt create mode 100644 deps/v8/test/inspector/runtime/context-destroyed-on-context-collected.js diff --git a/deps/v8/src/inspector/inspected-context.cc b/deps/v8/src/inspector/inspected-context.cc index 27766f200a5d21..1a5f49c0b54ce1 100644 --- a/deps/v8/src/inspector/inspected-context.cc +++ b/deps/v8/src/inspector/inspected-context.cc @@ -15,6 +15,39 @@ namespace v8_inspector { +class InspectedContext::WeakCallbackData { + public: + WeakCallbackData(InspectedContext* context, V8InspectorImpl* inspector, + int groupId, int contextId) + : m_context(context), + m_inspector(inspector), + m_groupId(groupId), + m_contextId(contextId) {} + + static void resetContext(const v8::WeakCallbackInfo& data) { + // InspectedContext is alive here because weak handler is still alive. + data.GetParameter()->m_context->m_weakCallbackData = nullptr; + data.GetParameter()->m_context->m_context.Reset(); + data.SetSecondPassCallback(&callContextCollected); + } + + static void callContextCollected( + const v8::WeakCallbackInfo& data) { + // InspectedContext can be dead here since anything can happen between first + // and second pass callback. + WeakCallbackData* callbackData = data.GetParameter(); + callbackData->m_inspector->contextCollected(callbackData->m_groupId, + callbackData->m_contextId); + delete callbackData; + } + + private: + InspectedContext* m_context; + V8InspectorImpl* m_inspector; + int m_groupId; + int m_contextId; +}; + InspectedContext::InspectedContext(V8InspectorImpl* inspector, const V8ContextInfo& info, int contextId) : m_inspector(inspector), @@ -26,6 +59,11 @@ InspectedContext::InspectedContext(V8InspectorImpl* inspector, m_auxData(toString16(info.auxData)), m_reported(false) { v8::debug::SetContextId(info.context, contextId); + m_weakCallbackData = + new WeakCallbackData(this, m_inspector, m_contextGroupId, m_contextId); + m_context.SetWeak(m_weakCallbackData, + &InspectedContext::WeakCallbackData::resetContext, + v8::WeakCallbackType::kParameter); if (!info.hasMemoryOnConsole) return; v8::Context::Scope contextScope(info.context); v8::Local global = info.context->Global(); @@ -39,6 +77,9 @@ InspectedContext::InspectedContext(V8InspectorImpl* inspector, } InspectedContext::~InspectedContext() { + // If we destory InspectedContext before weak callback is invoked then we need + // to delete data here. + if (!m_context.IsEmpty()) delete m_weakCallbackData; } // static diff --git a/deps/v8/src/inspector/inspected-context.h b/deps/v8/src/inspector/inspected-context.h index d74de00ddfe3b2..422725046a17e5 100644 --- a/deps/v8/src/inspector/inspected-context.h +++ b/deps/v8/src/inspector/inspected-context.h @@ -44,6 +44,8 @@ class InspectedContext { friend class V8InspectorImpl; InspectedContext(V8InspectorImpl*, const V8ContextInfo&, int contextId); + class WeakCallbackData; + V8InspectorImpl* m_inspector; v8::Global m_context; int m_contextId; @@ -53,6 +55,7 @@ class InspectedContext { const String16 m_auxData; bool m_reported; std::unique_ptr m_injectedScript; + WeakCallbackData* m_weakCallbackData; DISALLOW_COPY_AND_ASSIGN(InspectedContext); }; diff --git a/deps/v8/src/inspector/v8-inspector-impl.cc b/deps/v8/src/inspector/v8-inspector-impl.cc index 3c55507c5a0a55..56ed0babf0e921 100644 --- a/deps/v8/src/inspector/v8-inspector-impl.cc +++ b/deps/v8/src/inspector/v8-inspector-impl.cc @@ -219,6 +219,10 @@ void V8InspectorImpl::contextCreated(const V8ContextInfo& info) { void V8InspectorImpl::contextDestroyed(v8::Local context) { int contextId = InspectedContext::contextId(context); int groupId = contextGroupId(context); + contextCollected(groupId, contextId); +} + +void V8InspectorImpl::contextCollected(int groupId, int contextId) { m_contextIdToGroupIdMap.erase(contextId); ConsoleStorageMap::iterator storageIt = m_consoleStorageMap.find(groupId); diff --git a/deps/v8/src/inspector/v8-inspector-impl.h b/deps/v8/src/inspector/v8-inspector-impl.h index d5a8c6e569928c..804804e0ab1a44 100644 --- a/deps/v8/src/inspector/v8-inspector-impl.h +++ b/deps/v8/src/inspector/v8-inspector-impl.h @@ -74,6 +74,7 @@ class V8InspectorImpl : public V8Inspector { const StringView& state) override; void contextCreated(const V8ContextInfo&) override; void contextDestroyed(v8::Local) override; + void contextCollected(int contextGroupId, int contextId); void resetContextGroup(int contextGroupId) override; void idleStarted() override; void idleFinished() override; diff --git a/deps/v8/test/inspector/inspector-test.cc b/deps/v8/test/inspector/inspector-test.cc index 2e105c54d90121..0395cc3483f38f 100644 --- a/deps/v8/test/inspector/inspector-test.cc +++ b/deps/v8/test/inspector/inspector-test.cc @@ -619,6 +619,9 @@ class InspectorExtension : public IsolateData::SetupGlobalTask { inspector->Set(ToV8String(isolate, "fireContextDestroyed"), v8::FunctionTemplate::New( isolate, &InspectorExtension::FireContextDestroyed)); + inspector->Set( + ToV8String(isolate, "freeContext"), + v8::FunctionTemplate::New(isolate, &InspectorExtension::FreeContext)); inspector->Set(ToV8String(isolate, "setMaxAsyncTaskStacks"), v8::FunctionTemplate::New( isolate, &InspectorExtension::SetMaxAsyncTaskStacks)); @@ -658,6 +661,12 @@ class InspectorExtension : public IsolateData::SetupGlobalTask { data->inspector()->ContextDestroyed(context); } + static void FreeContext(const v8::FunctionCallbackInfo& args) { + v8::Local context = args.GetIsolate()->GetCurrentContext(); + IsolateData* data = IsolateData::FromContext(context); + data->FreeContext(context); + } + static void SetMaxAsyncTaskStacks( const v8::FunctionCallbackInfo& args) { if (args.Length() != 1 || !args[0]->IsInt32()) { diff --git a/deps/v8/test/inspector/runtime/context-destroyed-on-context-collected-expected.txt b/deps/v8/test/inspector/runtime/context-destroyed-on-context-collected-expected.txt new file mode 100644 index 00000000000000..9a5e1708c1d589 --- /dev/null +++ b/deps/v8/test/inspector/runtime/context-destroyed-on-context-collected-expected.txt @@ -0,0 +1,7 @@ +Tests that contextDesrtoyed nofitication is fired when context is collected. +{ + method : Runtime.executionContextDestroyed + params : { + executionContextId : + } +} diff --git a/deps/v8/test/inspector/runtime/context-destroyed-on-context-collected.js b/deps/v8/test/inspector/runtime/context-destroyed-on-context-collected.js new file mode 100644 index 00000000000000..9f715937c6db0d --- /dev/null +++ b/deps/v8/test/inspector/runtime/context-destroyed-on-context-collected.js @@ -0,0 +1,14 @@ +// Copyright 2017 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +let {session, contextGroup, Protocol} = + InspectorTest.start('Tests that contextDesrtoyed nofitication is fired when context is collected.'); + +(async function test() { + await Protocol.Runtime.enable(); + Protocol.Runtime.onExecutionContextDestroyed(InspectorTest.logMessage); + contextGroup.addScript('inspector.freeContext()'); + await Protocol.HeapProfiler.collectGarbage(); + InspectorTest.completeTest(); +})(); From 029567a460ae300568ab11ef9593fb569af9425f Mon Sep 17 00:00:00 2001 From: Eugene Ostroukhov Date: Mon, 24 Apr 2017 10:06:17 -0700 Subject: [PATCH 34/97] inspector: support extra contexts This enables inspector support for contexts created using the vm module. PR-URL: https://github.com/nodejs/node/pull/14465 Reviewed-By: Jan Krems Reviewed-By: Timothy Gu Reviewed-By: Aleksey Kozyatinskiy --- src/env-inl.h | 3 ++ src/inspector_agent.cc | 14 ++++++-- src/inspector_agent.h | 2 ++ test/inspector/test-contexts.js | 63 +++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 test/inspector/test-contexts.js diff --git a/src/env-inl.h b/src/env-inl.h index 6753979ef57bf5..561c76257e09bc 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -259,6 +259,9 @@ inline void Environment::TickInfo::set_index(uint32_t value) { inline void Environment::AssignToContext(v8::Local context) { context->SetAlignedPointerInEmbedderData(kContextEmbedderDataIndex, this); +#if HAVE_INSPECTOR + inspector_agent()->ContextCreated(context); +#endif // HAVE_INSPECTOR } inline Environment* Environment::GetCurrent(v8::Isolate* isolate) { diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index bedf74f3b02f61..0f9caa32f2a22e 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -12,6 +12,7 @@ #include "libplatform/libplatform.h" #include +#include #include #include @@ -500,6 +501,7 @@ class NodeInspectorClient : public V8InspectorClient { terminated_(false), running_nested_loop_(false) { client_ = V8Inspector::create(env->isolate(), this); + contextCreated(env->context(), "Node.js Main Context"); } void runMessageLoopOnPause(int context_group_id) override { @@ -627,7 +629,8 @@ class NodeInspectorClient : public V8InspectorClient { Agent::Agent(Environment* env) : parent_env_(env), client_(nullptr), platform_(nullptr), - enabled_(false) {} + enabled_(false), + next_context_number_(1) {} // Destructor needs to be defined here in implementation file as the header // does not have full definition of some classes. @@ -641,7 +644,6 @@ bool Agent::Start(v8::Platform* platform, const char* path, client_ = std::unique_ptr( new NodeInspectorClient(parent_env_, platform)); - client_->contextCreated(parent_env_->context(), "Node.js Main Context"); platform_ = platform; CHECK_EQ(0, uv_async_init(uv_default_loop(), &start_io_thread_async, @@ -841,6 +843,14 @@ void Agent::RequestIoThreadStart() { uv_async_send(&start_io_thread_async); } +void Agent::ContextCreated(Local context) { + if (client_ == nullptr) // This happens for a main context + return; + std::ostringstream name; + name << "VM Context " << next_context_number_++; + client_->contextCreated(context, name.str()); +} + } // namespace inspector } // namespace node diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 80967212cd7aef..cf9a8bff8645ec 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -96,6 +96,7 @@ class Agent { void RequestIoThreadStart(); DebugOptions& options() { return debug_options_; } + void ContextCreated(v8::Local context); private: node::Environment* parent_env_; @@ -105,6 +106,7 @@ class Agent { bool enabled_; std::string path_; DebugOptions debug_options_; + int next_context_number_; }; } // namespace inspector diff --git a/test/inspector/test-contexts.js b/test/inspector/test-contexts.js new file mode 100644 index 00000000000000..54acfab0d5cace --- /dev/null +++ b/test/inspector/test-contexts.js @@ -0,0 +1,63 @@ +'use strict'; + +// Flags: --expose-gc + +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const { strictEqual } = require('assert'); +const { runInNewContext } = require('vm'); +const { Session } = require('inspector'); + +const session = new Session(); +session.connect(); + +function notificationPromise(method) { + return new Promise((resolve) => session.once(method, resolve)); +} + +async function testContextCreatedAndDestroyed() { + console.log('Testing context created/destroyed notifications'); + const mainContextPromise = + notificationPromise('Runtime.executionContextCreated'); + + session.post('Runtime.enable'); + let contextCreated = await mainContextPromise; + strictEqual('Node.js Main Context', + contextCreated.params.context.name, + JSON.stringify(contextCreated)); + + const secondContextCreatedPromise = + notificationPromise('Runtime.executionContextCreated'); + + let contextDestroyed = null; + session.once('Runtime.executionContextDestroyed', + (notification) => contextDestroyed = notification); + + runInNewContext('1 + 1', {}); + + contextCreated = await secondContextCreatedPromise; + strictEqual('VM Context 1', + contextCreated.params.context.name, + JSON.stringify(contextCreated)); + + // GC is unpredictable... + while (!contextDestroyed) + global.gc(); + + strictEqual(contextCreated.params.context.id, + contextDestroyed.params.executionContextId, + JSON.stringify(contextDestroyed)); +} + +async function testBreakpointHit() { + console.log('Testing breakpoint is hit in a new context'); + session.post('Debugger.enable'); + + const pausedPromise = notificationPromise('Debugger.paused'); + runInNewContext('debugger', {}); + await pausedPromise; +} + +common.crashOnUnhandledRejection(); +testContextCreatedAndDestroyed().then(testBreakpointHit); From 7eb9f6f6e4936a3e8adef4baad93b1604770488b Mon Sep 17 00:00:00 2001 From: Daniel Bevenius Date: Fri, 11 Aug 2017 08:59:56 +0200 Subject: [PATCH 35/97] test: make totalLen snake case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For consistency, use snake case (total_len) for the local totalLen variable. PR-URL: https://github.com/nodejs/node/pull/14765 Reviewed-By: Anna Henningsen Reviewed-By: Ben Noordhuis Reviewed-By: Michaël Zasso Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen Reviewed-By: Alexey Orlenko Reviewed-By: Gibson Fahnestock --- test/cctest/node_test_fixture.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/cctest/node_test_fixture.h b/test/cctest/node_test_fixture.h index f5a25c70b5f44c..747ac225b4b53d 100644 --- a/test/cctest/node_test_fixture.h +++ b/test/cctest/node_test_fixture.h @@ -36,12 +36,12 @@ struct Argv { Argv(const std::initializer_list &args) { nr_args_ = args.size(); - int totalLen = 0; + int total_len = 0; for (auto it = args.begin(); it != args.end(); ++it) { - totalLen += strlen(*it) + 1; + total_len += strlen(*it) + 1; } argv_ = static_cast(malloc(nr_args_ * sizeof(char*))); - argv_[0] = static_cast(malloc(totalLen)); + argv_[0] = static_cast(malloc(total_len)); int i = 0; int offset = 0; for (auto it = args.begin(); it != args.end(); ++it, ++i) { From b87fae927d08e3b7eeaefb50a6c09eb9d3d8cf21 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 10 Aug 2017 21:07:21 +0200 Subject: [PATCH 36/97] src: remove duplicate loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two identical `while` loops after each other can be folded into a single one. PR-URL: https://github.com/nodejs/node/pull/14750 Reviewed-By: Ben Noordhuis Reviewed-By: Daniel Bevenius Reviewed-By: James M Snell Reviewed-By: Evan Lucas Reviewed-By: Luigi Pinca Reviewed-By: Timothy Gu Reviewed-By: Michaël Zasso Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen Reviewed-By: Khaidi Chu --- src/env.cc | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/env.cc b/src/env.cc index 5bd75abab34512..076198cd626a81 100644 --- a/src/env.cc +++ b/src/env.cc @@ -105,9 +105,6 @@ void Environment::CleanupHandles() { while (handle_cleanup_waiting_ != 0) uv_run(event_loop(), UV_RUN_ONCE); - - while (handle_cleanup_waiting_ != 0) - uv_run(event_loop(), UV_RUN_ONCE); } void Environment::StartProfilerIdleNotifier() { From d7d22ead2b2fd28208475e87e7dd8ed9859e1ac5 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 10 Aug 2017 16:56:04 +0200 Subject: [PATCH 37/97] src: add comments for cares library init refcount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref: https://github.com/nodejs/node/pull/14738 PR-URL: https://github.com/nodejs/node/pull/14743 Reviewed-By: Timothy Gu Reviewed-By: Ben Noordhuis Reviewed-By: Tobias Nießen Reviewed-By: Khaidi Chu Reviewed-By: Colin Ihrig Reviewed-By: James M Snell --- src/cares_wrap.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 53a005444cc498..7869651a456c0a 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -489,6 +489,8 @@ void ChannelWrap::Setup() { int r; if (!library_inited_) { + // Multiple calls to ares_library_init() increase a reference counter, + // so this is a no-op except for the first call to it. r = ares_library_init(ARES_LIB_INIT_ALL); if (r != ARES_SUCCESS) return env()->ThrowError(ToErrorCodeString(r)); @@ -516,8 +518,10 @@ void ChannelWrap::Setup() { ChannelWrap::~ChannelWrap() { - if (library_inited_) + if (library_inited_) { + // This decreases the reference counter increased by ares_library_init(). ares_library_cleanup(); + } ares_destroy(channel_); CleanupTimer(); From 9bbbf12827d00819a42d5efd0823b38543bbebab Mon Sep 17 00:00:00 2001 From: XadillaX Date: Thu, 10 Aug 2017 20:29:53 +0800 Subject: [PATCH 38/97] test: remove redundant `using` in cctest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/14739 Reviewed-By: Refael Ackermann Reviewed-By: Daniel Bevenius Reviewed-By: James M Snell Reviewed-By: James M Snell Reviewed-By: Benjamin Gruenbaum Reviewed-By: Luigi Pinca Reviewed-By: Timothy Gu Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen Reviewed-By: Alexey Orlenko --- test/cctest/node_test_fixture.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/cctest/node_test_fixture.h b/test/cctest/node_test_fixture.h index 747ac225b4b53d..e52b1b5dfd47ca 100644 --- a/test/cctest/node_test_fixture.h +++ b/test/cctest/node_test_fixture.h @@ -8,13 +8,6 @@ #include "v8.h" #include "libplatform/libplatform.h" -using node::Environment; -using node::IsolateData; -using node::CreateIsolateData; -using node::CreateEnvironment; -using node::AtExit; -using node::RunAtExit; - class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator { public: virtual void* Allocate(size_t length) { From 0eab77c86fffcc7f71666d27a4ce2878bce0b828 Mon Sep 17 00:00:00 2001 From: Gibson Fahnestock Date: Mon, 17 Jul 2017 15:01:03 +0800 Subject: [PATCH 39/97] test: allow inspector to reopen with same port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test checks that if you open the inspector with '0' (pick a random free port), close it, then reopen it, you get a different port. However this isn't necessarily true. PR-URL: https://github.com/nodejs/node/pull/14320 Fixes: https://github.com/nodejs/node/issues/14316 Refs: https://github.com/nodejs/node/issues/14316#issuecomment-315680755 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Tobias Nießen Reviewed-By: Luigi Pinca Reviewed-By: James M Snell Reviewed-By: Colin Ihrig --- test/parallel/test-inspector-open.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/parallel/test-inspector-open.js b/test/parallel/test-inspector-open.js index 346393d6fac646..26647c7e276716 100644 --- a/test/parallel/test-inspector-open.js +++ b/test/parallel/test-inspector-open.js @@ -68,7 +68,6 @@ function tryToCloseWhenClosed(msg) { function reopenAfterClose(msg) { assert.strictEqual(msg.cmd, 'url'); const port = url.parse(msg.url).port; - assert.notStrictEqual(port, firstPort); ping(port, (err) => { assert.ifError(err); process.exit(); From 917ace283fb6defaba8846d05b05118cdf1cfae2 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 8 Aug 2017 19:09:28 +0200 Subject: [PATCH 40/97] n-api: add napi_get_node_version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `napi_get_node_version`, to help with feature-detecting Node.js as an environment. PR-URL: https://github.com/nodejs/node/pull/14696 Reviewed-By: Kyle Farnung Reviewed-By: James M Snell Reviewed-By: Colin Ihrig Reviewed-By: Tobias Nießen --- doc/api/n-api.md | 31 ++++++++++++++++++++ src/node.cc | 3 +- src/node_api.cc | 14 +++++++++ src/node_api.h | 4 +++ src/node_api_types.h | 7 +++++ src/node_version.h | 4 +++ test/addons-napi/test_general/test.js | 5 ++++ test/addons-napi/test_general/test_general.c | 20 +++++++++++++ 8 files changed, 87 insertions(+), 1 deletion(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index af3696fd10b953..9e7fdd9301332e 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -3285,6 +3285,35 @@ callback invocation, even if it has been successfully cancelled. ## Version Management +### napi_get_node_version + + +```C +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t patch; + const char* release; +} napi_node_version; + +NAPI_EXTERN +napi_status napi_get_node_version(napi_env env, + const napi_node_version** version); +``` + +- `[in] env`: The environment that the API is invoked under. +- `[out] version`: A pointer to version information for Node itself. + +Returns `napi_ok` if the API succeeded. + +This function fills the `version` struct with the major, minor and patch version +of Node that is currently running, and the `release` field with the +value of [`process.release.name`][`process.release`]. + +The returned buffer is statically allocated and does not need to be freed. + ### napi_get_version -`options...` are interpreted as if they had been specified on the command line -before the actual command line (so they can be overridden). Node will exit with -an error if an option that is not allowed in the environment is used, such as -`-p` or a script file. +A space-separated list of command line options. `options...` are interpreted as +if they had been specified on the command line before the actual command line +(so they can be overridden). Node will exit with an error if an option that is +not allowed in the environment is used, such as `-p` or a script file. Node options that are allowed are: - `--enable-fips` diff --git a/doc/node.1 b/doc/node.1 index ca142a2cab152b..753bf0f78d0b87 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -278,10 +278,10 @@ When set to \fI1\fR, process warnings are silenced. .TP .BR NODE_OPTIONS =\fIoptions...\fR -\fBoptions...\fR are interpreted as if they had been specified on the command -line before the actual command line (so they can be overridden). Node will exit -with an error if an option that is not allowed in the environment is used, such -as \fB-p\fR or a script file. +A space-separated list of command line options. \fBoptions...\fR are interpreted +as if they had been specified on the command line before the actual command line +(so they can be overridden). Node will exit with an error if an option that is +not allowed in the environment is used, such as \fB-p\fR or a script file. .TP .BR NODE_PATH =\fIpath\fR[:\fI...\fR] diff --git a/src/node.cc b/src/node.cc index a80cf4452cceb0..551f9fbf396949 100644 --- a/src/node.cc +++ b/src/node.cc @@ -3713,6 +3713,7 @@ static void PrintHelp() { "NODE_NO_WARNINGS set to 1 to silence process warnings\n" #if !defined(NODE_WITHOUT_NODE_OPTIONS) "NODE_OPTIONS set CLI options in the environment\n" + " via a space-separated list\n" #endif #ifdef _WIN32 "NODE_PATH ';'-separated list of directories\n" From b3c1c6ff7f65c6bf8c08585d11b8da9779e0d371 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 28 Jun 2017 20:34:19 +0200 Subject: [PATCH 51/97] benchmark: fix and extend assert benchmarks The benchmarks had the strict and non strict labels switched. This is fixed and the benchmarks were extended to check more possible input types and function calls. PR-URL: https://github.com/nodejs/node/pull/14147 Refs: https://github.com/nodejs/node/pull/13973 Reviewed-By: Refael Ackermann Reviewed-By: James M Snell --- benchmark/assert/deepequal-buffer.js | 35 +++++- benchmark/assert/deepequal-object.js | 73 +++++++++++ .../deepequal-prims-and-objs-big-array-set.js | 119 ++++++++++++++++++ .../deepequal-prims-and-objs-big-array.js | 55 -------- .../deepequal-prims-and-objs-big-loop.js | 27 +++- benchmark/assert/deepequal-typedarrays.js | 29 ++++- benchmark/assert/throws.js | 57 +++++++++ 7 files changed, 328 insertions(+), 67 deletions(-) create mode 100644 benchmark/assert/deepequal-object.js create mode 100644 benchmark/assert/deepequal-prims-and-objs-big-array-set.js delete mode 100644 benchmark/assert/deepequal-prims-and-objs-big-array.js create mode 100644 benchmark/assert/throws.js diff --git a/benchmark/assert/deepequal-buffer.js b/benchmark/assert/deepequal-buffer.js index 2a7d9e3bed7c38..9e86aa231e69d2 100644 --- a/benchmark/assert/deepequal-buffer.js +++ b/benchmark/assert/deepequal-buffer.js @@ -1,10 +1,16 @@ 'use strict'; const common = require('../common.js'); const assert = require('assert'); + const bench = common.createBenchmark(main, { - n: [1e3], - len: [1e2], - method: ['strict', 'nonstrict'] + n: [1e5], + len: [1e2, 1e4], + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ] }); function main(conf) { @@ -12,14 +18,16 @@ function main(conf) { const len = +conf.len; var i; - const data = Buffer.allocUnsafe(len); + const data = Buffer.allocUnsafe(len + 1); const actual = Buffer.alloc(len); const expected = Buffer.alloc(len); + const expectedWrong = Buffer.alloc(len + 1); data.copy(actual); data.copy(expected); + data.copy(expectedWrong); switch (conf.method) { - case 'strict': + case 'deepEqual': bench.start(); for (i = 0; i < n; ++i) { // eslint-disable-next-line no-restricted-properties @@ -27,13 +35,28 @@ function main(conf) { } bench.end(n); break; - case 'nonstrict': + case 'deepStrictEqual': bench.start(); for (i = 0; i < n; ++i) { assert.deepStrictEqual(actual, expected); } bench.end(n); break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; default: throw new Error('Unsupported method'); } diff --git a/benchmark/assert/deepequal-object.js b/benchmark/assert/deepequal-object.js new file mode 100644 index 00000000000000..d1a6cb69944055 --- /dev/null +++ b/benchmark/assert/deepequal-object.js @@ -0,0 +1,73 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e6], + size: [1e2, 1e3, 1e4], + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ] +}); + +function createObj(source, add = '') { + return source.map((n) => ({ + foo: 'yarp', + nope: { + bar: `123${add}`, + a: [1, 2, 3], + baz: n + } + })); +} + +function main(conf) { + const size = +conf.size; + // TODO: Fix this "hack" + const n = (+conf.n) / size; + var i; + + const source = Array.apply(null, Array(size)); + const actual = createObj(source); + const expected = createObj(source); + const expectedWrong = createObj(source, '4'); + + switch (conf.method) { + case 'deepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(actual, expected); + } + bench.end(n); + break; + case 'deepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.deepStrictEqual(actual, expected); + } + bench.end(n); + break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; + default: + throw new Error('Unsupported method'); + } +} diff --git a/benchmark/assert/deepequal-prims-and-objs-big-array-set.js b/benchmark/assert/deepequal-prims-and-objs-big-array-set.js new file mode 100644 index 00000000000000..1e356cea51efd6 --- /dev/null +++ b/benchmark/assert/deepequal-prims-and-objs-big-array-set.js @@ -0,0 +1,119 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const primValues = { + 'null': null, + 'undefined': undefined, + 'string': 'a', + 'number': 1, + 'boolean': true, + 'object': { 0: 'a' }, + 'array': [1, 2, 3], + 'new-array': new Array([1, 2, 3]) +}; + +const bench = common.createBenchmark(main, { + prim: Object.keys(primValues), + n: [25], + len: [1e5], + method: [ + 'deepEqual_Array', + 'deepStrictEqual_Array', + 'notDeepEqual_Array', + 'notDeepStrictEqual_Array', + 'deepEqual_Set', + 'deepStrictEqual_Set', + 'notDeepEqual_Set', + 'notDeepStrictEqual_Set' + ] +}); + +function main(conf) { + const prim = primValues[conf.prim]; + const n = +conf.n; + const len = +conf.len; + const actual = []; + const expected = []; + const expectedWrong = []; + var i; + + for (var x = 0; x < len; x++) { + actual.push(prim); + expected.push(prim); + expectedWrong.push(prim); + } + expectedWrong.pop(); + expectedWrong.push('b'); + + // Note: primitives are only added once to a set + const actualSet = new Set(actual); + const expectedSet = new Set(expected); + const expectedWrongSet = new Set(expectedWrong); + + switch (conf.method) { + case 'deepEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(actual, expected); + } + bench.end(n); + break; + case 'deepStrictEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + assert.deepStrictEqual(actual, expected); + } + bench.end(n); + break; + case 'notDeepEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual_Array': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'deepEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(actualSet, expectedSet); + } + bench.end(n); + break; + case 'deepStrictEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + assert.deepStrictEqual(actualSet, expectedSet); + } + bench.end(n); + break; + case 'notDeepEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actualSet, expectedWrongSet); + } + bench.end(n); + break; + case 'notDeepStrictEqual_Set': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actualSet, expectedWrongSet); + } + bench.end(n); + break; + default: + throw new Error('Unsupported method'); + } +} diff --git a/benchmark/assert/deepequal-prims-and-objs-big-array.js b/benchmark/assert/deepequal-prims-and-objs-big-array.js deleted file mode 100644 index 69eda8af087dfa..00000000000000 --- a/benchmark/assert/deepequal-prims-and-objs-big-array.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; -const common = require('../common.js'); -const assert = require('assert'); - -const primValues = { - 'null': null, - 'undefined': undefined, - 'string': 'a', - 'number': 1, - 'boolean': true, - 'object': { 0: 'a' }, - 'array': [1, 2, 3], - 'new-array': new Array([1, 2, 3]) -}; - -const bench = common.createBenchmark(main, { - prim: Object.keys(primValues), - n: [25], - len: [1e5], - method: ['strict', 'nonstrict'] -}); - -function main(conf) { - const prim = primValues[conf.prim]; - const n = +conf.n; - const len = +conf.len; - const actual = []; - const expected = []; - var i; - - for (var x = 0; x < len; x++) { - actual.push(prim); - expected.push(prim); - } - - switch (conf.method) { - case 'strict': - bench.start(); - for (i = 0; i < n; ++i) { - // eslint-disable-next-line no-restricted-properties - assert.deepEqual(actual, expected); - } - bench.end(n); - break; - case 'nonstrict': - bench.start(); - for (i = 0; i < n; ++i) { - assert.deepStrictEqual(actual, expected); - } - bench.end(n); - break; - default: - throw new Error('Unsupported method'); - } -} diff --git a/benchmark/assert/deepequal-prims-and-objs-big-loop.js b/benchmark/assert/deepequal-prims-and-objs-big-loop.js index 781c5ad754e723..2800b51c491bcf 100644 --- a/benchmark/assert/deepequal-prims-and-objs-big-loop.js +++ b/benchmark/assert/deepequal-prims-and-objs-big-loop.js @@ -16,7 +16,12 @@ const primValues = { const bench = common.createBenchmark(main, { prim: Object.keys(primValues), n: [1e6], - method: ['strict', 'nonstrict'] + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ] }); function main(conf) { @@ -24,11 +29,12 @@ function main(conf) { const n = +conf.n; const actual = prim; const expected = prim; + const expectedWrong = 'b'; var i; // Creates new array to avoid loop invariant code motion switch (conf.method) { - case 'strict': + case 'deepEqual': bench.start(); for (i = 0; i < n; ++i) { // eslint-disable-next-line no-restricted-properties @@ -36,13 +42,28 @@ function main(conf) { } bench.end(n); break; - case 'nonstrict': + case 'deepStrictEqual': bench.start(); for (i = 0; i < n; ++i) { assert.deepStrictEqual([actual], [expected]); } bench.end(n); break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual([actual], [expectedWrong]); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual([actual], [expectedWrong]); + } + bench.end(n); + break; default: throw new Error('Unsupported method'); } diff --git a/benchmark/assert/deepequal-typedarrays.js b/benchmark/assert/deepequal-typedarrays.js index 00c6ca5adf2835..5d51d53d13e70f 100644 --- a/benchmark/assert/deepequal-typedarrays.js +++ b/benchmark/assert/deepequal-typedarrays.js @@ -1,6 +1,7 @@ 'use strict'; const common = require('../common.js'); const assert = require('assert'); + const bench = common.createBenchmark(main, { type: [ 'Int8Array', @@ -14,7 +15,12 @@ const bench = common.createBenchmark(main, { 'Uint8ClampedArray', ], n: [1], - method: ['strict', 'nonstrict'], + method: [ + 'deepEqual', + 'deepStrictEqual', + 'notDeepEqual', + 'notDeepStrictEqual' + ], len: [1e6] }); @@ -26,10 +32,12 @@ function main(conf) { const actual = new clazz(len); const expected = new clazz(len); + const expectedWrong = Buffer.alloc(len); + expectedWrong[100] = 123; var i; switch (conf.method) { - case 'strict': + case 'deepEqual': bench.start(); for (i = 0; i < n; ++i) { // eslint-disable-next-line no-restricted-properties @@ -37,13 +45,28 @@ function main(conf) { } bench.end(n); break; - case 'nonstrict': + case 'deepStrictEqual': bench.start(); for (i = 0; i < n; ++i) { assert.deepStrictEqual(actual, expected); } bench.end(n); break; + case 'notDeepEqual': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-properties + assert.notDeepEqual(actual, expectedWrong); + } + bench.end(n); + break; + case 'notDeepStrictEqual': + bench.start(); + for (i = 0; i < n; ++i) { + assert.notDeepStrictEqual(actual, expectedWrong); + } + bench.end(n); + break; default: throw new Error('Unsupported method'); } diff --git a/benchmark/assert/throws.js b/benchmark/assert/throws.js new file mode 100644 index 00000000000000..9043799256e196 --- /dev/null +++ b/benchmark/assert/throws.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e6], + method: [ + 'doesNotThrow', + 'throws', + 'throws_TypeError', + 'throws_RegExp' + ] +}); + +function main(conf) { + const n = +conf.n; + const throws = () => { throw new TypeError('foobar'); }; + const doesNotThrow = () => { return 'foobar'; }; + const regExp = /foobar/; + const message = 'failure'; + var i; + + switch (conf.method) { + case 'doesNotThrow': + bench.start(); + for (i = 0; i < n; ++i) { + assert.doesNotThrow(doesNotThrow); + } + bench.end(n); + break; + case 'throws': + bench.start(); + for (i = 0; i < n; ++i) { + // eslint-disable-next-line no-restricted-syntax + assert.throws(throws); + } + bench.end(n); + break; + case 'throws_TypeError': + bench.start(); + for (i = 0; i < n; ++i) { + assert.throws(throws, TypeError, message); + } + bench.end(n); + break; + case 'throws_RegExp': + bench.start(); + for (i = 0; i < n; ++i) { + assert.throws(throws, regExp, message); + } + bench.end(n); + break; + default: + throw new Error(`Unsupported method ${conf.method}`); + } +} From 4811fea553621cd4974cb747c7a58478fcc7db82 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 13 Aug 2017 22:05:13 +0200 Subject: [PATCH 52/97] doc: add missing `changes:` metadata for streams Ref: https://github.com/nodejs/node/pull/14636 PR-URL: https://github.com/nodejs/node/pull/14810 Reviewed-By: Jeremiah Senkpiel --- doc/api/stream.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/api/stream.md b/doc/api/stream.md index c26ab800faf610..7d42bd2d71ca6a 100644 --- a/doc/api/stream.md +++ b/doc/api/stream.md @@ -1740,6 +1740,13 @@ constructor and implement *both* the `readable._read()` and `writable._write()` methods. #### new stream.Duplex(options) + * `options` {Object} Passed to both Writable and Readable constructors. Also has the following fields: From 9e51802f53c7ece8642b0ef822b9b9f9f5cda777 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 13 Aug 2017 22:06:00 +0200 Subject: [PATCH 53/97] doc: add missing `changes:` metadata for util Ref: https://github.com/nodejs/node/pull/14558 PR-URL: https://github.com/nodejs/node/pull/14810 Reviewed-By: Jeremiah Senkpiel --- doc/api/util.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/api/util.md b/doc/api/util.md index 95d66533b39e31..c853d04716c310 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -150,6 +150,10 @@ property take precedence over `--trace-deprecation` and ## util.format(format[, ...args]) * `format` {string} A `printf`-like format string. From e0001dc6019fd11e88fc1ba2d757292c407eb140 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Oct 2016 11:38:52 -0700 Subject: [PATCH 54/97] http: move utcDate to internal/http.js Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- lib/_http_outgoing.js | 19 ++----------------- lib/internal/http.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) mode change 100644 => 100755 lib/_http_outgoing.js mode change 100644 => 100755 lib/internal/http.js diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js old mode 100644 new mode 100755 index 31973fceadb6b8..03a121ce7edfaa --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -23,9 +23,9 @@ const assert = require('assert').ok; const Stream = require('stream'); -const timers = require('timers'); const util = require('util'); const internalUtil = require('internal/util'); +const internalHttp = require('internal/http'); const Buffer = require('buffer').Buffer; const common = require('_http_common'); const checkIsHttpToken = common._checkIsHttpToken; @@ -36,6 +36,7 @@ const nextTick = require('internal/process/next_tick').nextTick; const CRLF = common.CRLF; const debug = common.debug; +const utcDate = internalHttp.utcDate; var RE_FIELDS = /^(?:Connection|Transfer-Encoding|Content-Length|Date|Expect|Trailer|Upgrade)$/i; @@ -63,24 +64,8 @@ function isCookieField(s) { return true; } -var dateCache; -function utcDate() { - if (!dateCache) { - var d = new Date(); - dateCache = d.toUTCString(); - timers.enroll(utcDate, 1000 - d.getMilliseconds()); - timers._unrefActive(utcDate); - } - return dateCache; -} -utcDate._onTimeout = function _onTimeout() { - dateCache = undefined; -}; - - function noopPendingOutput(amount) {} - function OutgoingMessage() { Stream.call(this); diff --git a/lib/internal/http.js b/lib/internal/http.js old mode 100644 new mode 100755 index 0b12bc7c8fbe85..71e32498f359a1 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -1,5 +1,21 @@ 'use strict'; +const timers = require('timers'); + +var dateCache; +function utcDate() { + if (!dateCache) { + const d = new Date(); + dateCache = d.toUTCString(); + timers.enroll(utcDate, 1000 - d.getMilliseconds()); + timers._unrefActive(utcDate); + } + return dateCache; +} +utcDate._onTimeout = function() { + dateCache = undefined; +}; + function ondrain() { if (this._httpMessage) this._httpMessage.emit('drain'); } @@ -7,4 +23,5 @@ function ondrain() { module.exports = { outHeadersKey: Symbol('outHeadersKey'), ondrain, + utcDate }; From e84c9d71767ba3d669c1d4313bf34c77c5fd40f3 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 4 Nov 2016 12:37:36 -0700 Subject: [PATCH 55/97] tls: add tlsSocket.disableRenegotiation() Allows TLS renegotiation to be disabled per `TLSSocket` instance. Per HTTP/2, TLS renegotiation is forbidden after the initial connection prefix is exchanged. Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- doc/api/tls.md | 8 +++ lib/_tls_wrap.js | 10 +++ .../test-tls-disable-renegotiation.js | 67 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100755 test/parallel/test-tls-disable-renegotiation.js diff --git a/doc/api/tls.md b/doc/api/tls.md index 80cd2e87263566..2b27189e7604d4 100644 --- a/doc/api/tls.md +++ b/doc/api/tls.md @@ -552,6 +552,14 @@ added: v0.11.4 Returns `true` if the peer certificate was signed by one of the CAs specified when creating the `tls.TLSSocket` instance, otherwise `false`. +### tlsSocket.disableRenegotiation() + + +Disables TLS renegotiation for this `TLSSocket` instance. Once called, attempts +to renegotiate will trigger an `'error'` event on the `TLSSocket`. + ### tlsSocket.encrypted + +Enable the experimental `'http2'` module. + ### `--napi-modules` + +* Extends: {EventEmitter} + +Instances of the `http2.Http2Session` class represent an active communications +session between an HTTP/2 client and server. Instances of this class are *not* +intended to be constructed directly by user code. + +Each `Http2Session` instance will exhibit slightly different behaviors +depending on whether it is operating as a server or a client. The +`http2session.type` property can be used to determine the mode in which an +`Http2Session` is operating. On the server side, user code should rarely +have occasion to work with the `Http2Session` object directly, with most +actions typically taken through interactions with either the `Http2Server` or +`Http2Stream` objects. + +#### Http2Session and Sockets + +Every `Http2Session` instance is associated with exactly one [`net.Socket`][] or +[`tls.TLSSocket`][] when it is created. When either the `Socket` or the +`Http2Session` are destroyed, both will be destroyed. + +Because the of the specific serialization and processing requirements imposed +by the HTTP/2 protocol, it is not recommended for user code to read data from +or write data to a `Socket` instance bound to a `Http2Session`. Doing so can +put the HTTP/2 session into an indeterminate state causing the session and +the socket to become unusable. + +Once a `Socket` has been bound to an `Http2Session`, user code should rely +solely on the API of the `Http2Session`. + +#### Event: 'close' + + +The `'close'` event is emitted once the `Http2Session` has been terminated. + +#### Event: 'connect' + + +The `'connect'` event is emitted once the `Http2Session` has been successfully +connected to the remote peer and communication may begin. + +*Note*: User code will typically not listen for this event directly. + +#### Event: 'error' + + +The `'error'` event is emitted when an error occurs during the processing of +an `Http2Session`. + +#### Event: 'frameError' + + +The `'frameError'` event is emitted when an error occurs while attempting to +send a frame on the session. If the frame that could not be sent is associated +with a specific `Http2Stream`, an attempt to emit `'frameError'` event on the +`Http2Stream` is made. + +When invoked, the handler function will receive three arguments: + +* An integer identifying the frame type. +* An integer identifying the error code. +* An integer identifying the stream (or 0 if the frame is not associated with + a stream). + +If the `'frameError'` event is associated with a stream, the stream will be +closed and destroyed immediately following the `'frameError'` event. If the +event is not associated with a stream, the `Http2Session` will be shutdown +immediately following the `'frameError'` event. + +#### Event: 'goaway' + + +The `'goaway'` event is emitted when a GOAWAY frame is received. When invoked, +the handler function will receive three arguments: + +* `errorCode` {number} The HTTP/2 error code specified in the GOAWAY frame. +* `lastStreamID` {number} The ID of the last stream the remote peer successfully + processed (or `0` if no ID is specified). +* `opaqueData` {Buffer} If additional opaque data was included in the GOAWAY + frame, a `Buffer` instance will be passed containing that data. + +*Note*: The `Http2Session` instance will be shutdown automatically when the +`'goaway'` event is emitted. + +#### Event: 'localSettings' + + +The `'localSettings'` event is emitted when an acknowledgement SETTINGS frame +has been received. When invoked, the handler function will receive a copy of +the local settings. + +*Note*: When using `http2session.settings()` to submit new settings, the +modified settings do not take effect until the `'localSettings'` event is +emitted. + +```js +session.settings({ enablePush: false }); + +session.on('localSettings', (settings) => { + /** use the new settings **/ +}); +``` + +#### Event: 'remoteSettings' + + +The `'remoteSettings'` event is emitted when a new SETTINGS frame is received +from the connected peer. When invoked, the handle function will receive a copy +of the remote settings. + +```js +session.on('remoteSettings', (settings) => { + /** use the new settings **/ +}); +``` + +#### Event: 'stream' + + +The `'stream'` event is emitted when a new `Http2Stream` is created. When +invoked, the handler function will receive a reference to the `Http2Stream` +object, a [Headers Object][], and numeric flags associated with the creation +of the stream. + +```js +const http2 = require('http2'); +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_TYPE +} = http2.constants; +session.on('stream', (stream, headers, flags) => { + const method = headers[HTTP2_HEADER_METHOD]; + const path = headers[HTTP2_HEADER_PATH]; + // ... + stream.respond({ + [HTTP2_HEADER_STATUS]: 200, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + stream.write('hello '); + stream.end('world'); +}); +``` + +On the server side, user code will typically not listen for this event directly, +and would instead register a handler for the `'stream'` event emitted by the +`net.Server` or `tls.Server` instances returned by `http2.createServer()` and +`http2.createSecureServer()`, respectively, as in the example below: + +```js +const http2 = require('http2'); + +// Create a plain-text HTTP/2 server +const server = http2.createServer(); + +server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('

Hello World

'); +}); + +server.listen(80); +``` + +#### Event: 'socketError' + + +The `'socketError'` event is emitted when an `'error'` is emitted on the +`Socket` instance bound to the `Http2Session`. If this event is not handled, +the `'error'` event will be re-emitted on the `Socket`. + +Likewise, when an `'error'` event is emitted on the `Http2Session`, a +`'sessionError'` event will be emitted on the `Socket`. If that event is +not handled, the `'error'` event will be re-emitted on the `Http2Session`. + +#### Event: 'timeout' + + +After the `http2session.setTimeout()` method is used to set the timeout period +for this `Http2Session`, the `'timeout'` event is emitted if there is no +activity on the `Http2Session` after the configured number of milliseconds. + +```js +session.setTimeout(2000); +session.on('timeout', () => { /** .. **/ }); +``` + +#### http2session.destroy() + + +* Returns: {undefined} + +Immediately terminates the `Http2Session` and the associated `net.Socket` or +`tls.TLSSocket`. + +#### http2session.destroyed + + +* Value: {boolean} + +Will be `true` if this `Http2Session` instance has been destroyed and must no +longer be used, otherwise `false`. + +#### http2session.localSettings + + +* Value: {[Settings Object][]} + +A prototype-less object describing the current local settings of this +`Http2Session`. The local settings are local to *this* `Http2Session` instance. + +#### http2session.pendingSettingsAck + + +* Value: {boolean} + +Indicates whether or not the `Http2Session` is currently waiting for an +acknowledgement for a sent SETTINGS frame. Will be `true` after calling the +`http2session.settings()` method. Will be `false` once all sent SETTINGS +frames have been acknowledged. + +#### http2session.remoteSettings + + +* Value: {[Settings Object][]} + +A prototype-less object describing the current remote settings of this +`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer. + +#### http2session.request(headers[, options]) + + +* `headers` {[Headers Object][]} +* `options` {Object} + * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should + be closed initially, such as when sending a `GET` request that should not + expect a payload body. + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + the created stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of the newly created stream. + Defaults to `false`. + * `parent` {number} Specifies the numeric identifier of a stream the newly + created stream is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). +* Returns: {ClientHttp2Stream} + +For HTTP/2 Client `Http2Session` instances only, the `http2session.request()` +creates and returns an `Http2Stream` instance that can be used to send an +HTTP/2 request to the connected server. + +This method is only available if `http2session.type` is equal to +`http2.constants.NGHTTP2_SESSION_CLIENT`. + +```js +const http2 = require('http2'); +const clientSession = http2.connect('https://localhost:1234'); +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS +} = http2.constants; + +const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' }); +req.on('response', (headers) => { + console.log(HTTP2_HEADER_STATUS); + req.on('data', (chunk) => { /** .. **/ }); + req.on('end', () => { /** .. **/ }); +}); +``` + +#### http2session.rstStream(stream, code) + + +* stream {Http2Stream} +* code {number} Unsigned 32-bit integer identifying the error code. Defaults to + `http2.constant.NGHTTP2_NO_ERROR` (`0x00`) +* Returns: {undefined} + +Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing the given +`Http2Stream` to be closed on both sides using [error code][] `code`. + +#### http2session.setTimeout(msecs, callback) + + +* `msecs` {number} +* `callback` {Function} +* Returns: {undefined} + +Used to set a callback function that is called when there is no activity on +the `Http2Session` after `msecs` milliseconds. The given `callback` is +registered as a listener on the `'timeout'` event. + +#### http2session.shutdown(options[, callback]) + + +* `options` {Object} + * `graceful` {boolean} `true` to attempt a polite shutdown of the + `Http2Session`. + * `errorCode` {number} The HTTP/2 [error code][] to return. Note that this is + *not* the same thing as an HTTP Response Status Code. Defaults to `0x00` + (No Error). + * `lastStreamID` {number} The Stream ID of the last successfully processed + `Http2Stream` on this `Http2Session`. + * `opaqueData` {Buffer} A `Buffer` instance containing arbitrary additional + data to send to the peer upon disconnection. This is used, typically, to + provide additional data for debugging failures, if necessary. +* `callback` {Function} A callback that is invoked after the session shutdown + has been completed. +* Returns: {undefined} + +Attempts to shutdown this `Http2Session` using HTTP/2 defined procedures. +If specified, the given `callback` function will be invoked once the shutdown +process has completed. + +Note that calling `http2session.shutdown()` does *not* destroy the session or +tear down the `Socket` connection. It merely prompts both sessions to begin +preparing to cease activity. + +During a "graceful" shutdown, the session will first send a `GOAWAY` frame to +the connected peer identifying the last processed stream as 232-1. +Then, on the next tick of the event loop, a second `GOAWAY` frame identifying +the most recently processed stream identifier is sent. This process allows the +remote peer to begin preparing for the connection to be terminated. + +```js +session.shutdown({ + graceful: true, + opaqueData: Buffer.from('add some debugging data here') +}, () => session.destroy()); +``` + +#### http2session.socket + + +* Value: {net.Socket|tls.TLSSocket} + +A reference to the [`net.Socket`][] or [`tls.TLSSocket`][] to which this +`Http2Session` instance is bound. + +*Note*: It is not recommended for user code to interact directly with a +`Socket` bound to an `Http2Session`. See [Http2Session and Sockets][] for +details. + +#### http2session.state + + +* Value: {Object} + * `effectiveLocalWindowSize` {number} + * `effectiveRecvDataLength` {number} + * `nextStreamID` {number} + * `localWindowSize` {number} + * `lastProcStreamID` {number} + * `remoteWindowSize` {number} + * `outboundQueueSize` {number} + * `deflateDynamicTableSize` {number} + * `inflateDynamicTableSize` {number} + +An object describing the current status of this `Http2Session`. + +#### http2session.priority(stream, options) + + +* `stream` {Http2Stream} +* `options` {Object} + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + the given stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of the given stream. Defaults + to `false`. + * `parent` {number} Specifies the numeric identifier of a stream the given + stream is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). + * `silent` {boolean} When `true`, changes the priority locally without + sending a `PRIORITY` frame to the connected peer. +* Returns: {undefined} + +Updates the priority for the given `Http2Stream` instance. + +#### http2session.settings(settings) + + +* `settings` {[Settings Object][]} +* Returns {undefined} + +Updates the current local settings for this `Http2Session` and sends a new +`SETTINGS` frame to the connected HTTP/2 peer. + +Once called, the `http2session.pendingSettingsAck` property will be `true` +while the session is waiting for the remote peer to acknowledge the new +settings. + +*Note*: The new settings will not become effective until the SETTINGS +acknowledgement is received and the `'localSettings'` event is emitted. It +is possible to send multiple SETTINGS frames while acknowledgement is still +pending. + +#### http2session.type + + +* Value: {number} + +The `http2session.type` will be equal to +`http2.constants.NGHTTP2_SESSION_SERVER` if this `Http2Session` instance is a +server, and `http2.constants.NGHTTP2_SESSION_CLIENT` if the instance is a +client. + +### Class: Http2Stream + + +* Extends: {Duplex} + +Each instance of the `Http2Stream` class represents a bidirectional HTTP/2 +communications stream over an `Http2Session` instance. Any single `Http2Session` +may have up to 231-1 `Http2Stream` instances over its lifetime. + +User code will not construct `Http2Stream` instances directly. Rather, these +are created, managed, and provided to user code through the `Http2Session` +instance. On the server, `Http2Stream` instances are created either in response +to an incoming HTTP request (and handed off to user code via the `'stream'` +event), or in response to a call to the `http2stream.pushStream()` method. +On the client, `Http2Stream` instances are created and returned when either the +`http2session.request()` method is called, or in response to an incoming +`'push'` event. + +*Note*: The `Http2Stream` class is a base for the [`ServerHttp2Stream`][] and +[`ClientHttp2Stream`][] classes, each of which are used specifically by either +the Server or Client side, respectively. + +All `Http2Stream` instances are [`Duplex`][] streams. The `Writable` side of the +`Duplex` is used to send data to the connected peer, while the `Readable` side +is used to receive data sent by the connected peer. + +#### Http2Stream Lifecycle + +##### Creation + +On the server side, instances of [`ServerHttp2Stream`][] are created either +when: + +* A new HTTP/2 `HEADERS` frame with a previously unused stream ID is received; +* The `http2stream.pushStream()` method is called. + +On the client side, instances of [`ClientHttp2Stream`[] are created when the +`http2session.request()` method is called. + +*Note*: On the client, the `Http2Stream` instance returned by +`http2session.request()` may not be immediately ready for use if the parent +`Http2Session` has not yet been fully established. In such cases, operations +called on the `Http2Stream` will be buffered until the `'ready'` event is +emitted. User code should rarely, if ever, have need to handle the `'ready'` +event directly. The ready status of an `Http2Stream` can be determined by +checking the value of `http2stream.id`. If the value is `undefined`, the stream +is not yet ready for use. + +##### Destruction + +All [`Http2Stream`][] instances are destroyed either when: + +* An `RST_STREAM` frame for the stream is received by the connected peer. +* The `http2stream.rstStream()` or `http2session.rstStream()` methods are + called. +* The `http2stream.destroy()` or `http2session.destroy()` methods are called. + +When an `Http2Stream` instance is destroyed, an attempt will be made to send an +`RST_STREAM` frame will be sent to the connected peer. + +Once the `Http2Stream` instance is destroyed, the `'streamClosed'` event will +be emitted. Because `Http2Stream` is an instance of `stream.Duplex`, the +`'end'` event will also be emitted if the stream data is currently flowing. +The `'error'` event may also be emitted if `http2stream.destroy()` was called +with an `Error` passed as the first argument. + +After the `Http2Stream` has been destroyed, the `http2stream.destroyed` +property will be `true` and the `http2stream.rstCode` property will specify the +`RST_STREAM` error code. The `Http2Stream` instance is no longer usable once +destroyed. + +#### Event: 'aborted' + + +The `'aborted'` event is emitted whenever a `Http2Stream` instance is +abnormally aborted in mid-communication. + +*Note*: The `'aborted'` event will only be emitted if the `Http2Stream` +writable side has not been ended. + +#### Event: 'error' + + +The `'error'` event is emitted when an error occurs during the processing of +an `Http2Stream`. + +#### Event: 'fetchTrailers' + + +The `'fetchTrailers'` event is emitted by the `Http2Stream` immediately after +queuing the last chunk of payload data to be sent. The listener callback is +passed a single object (with a `null` prototype) that the listener may used +to specify the trailing header fields to send to the peer. + +```js +stream.on('fetchTrailers', (trailers) => { + trailers['ABC'] = 'some value to send'; +}); +``` + +*Note*: The HTTP/1 specification forbids trailers from containing HTTP/2 +"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event +will be emitted if the `'fetchTrailers'` event handler attempts to set such +header fields. + +#### Event: 'frameError' + + +The `'frameError'` event is emitted when an error occurs while attempting to +send a frame. When invoked, the handler function will receive an integer +argument identifying the frame type, and an integer argument identifying the +error code. The `Http2Stream` instance will be destroyed immediately after the +`'frameError'` event is emitted. + +#### Event: 'streamClosed' + + +The `'streamClosed'` event is emitted when the `Http2Stream` is destroyed. Once +this event is emitted, the `Http2Stream` instance is no longer usable. + +The listener callback is passed a single argument specifying the HTTP/2 error +code specified when closing the stream. If the code is any value other than +`NGHTTP2_NO_ERROR` (`0`), an `'error'` event will also be emitted. + +#### Event: 'timeout' + + +The `'timeout'` event is emitted after no activity is received for this +`'Http2Stream'` within the number of millseconds set using +`http2stream.setTimeout()`. + +#### Event: 'trailers' + + +The `'trailers'` event is emitted when a block of headers associated with +trailing header fields is received. The listener callback is passed the +[Headers Object][] and flags associated with the headers. + +```js +stream.on('trailers', (headers, flags) => { + console.log(headers); +}); +``` + +#### http2stream.aborted + + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance was aborted abnormally. When set, +the `'aborted'` event will have been emitted. + +#### http2stream.destroyed + + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance has been destroyed and is no longer +usable. + +#### http2stream.priority(options) + + +* `options` {Object} + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + this stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of this stream. Defaults + to `false`. + * `parent` {number} Specifies the numeric identifier of a stream this stream + is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). + * `silent` {boolean} When `true`, changes the priority locally without + sending a `PRIORITY` frame to the connected peer. +* Returns: {undefined} + +Updates the priority for this `Http2Stream` instance. + +#### http2stream.rstCode + + +* Value: {number} + +Set to the `RST_STREAM` [error code][] reported when the `Http2Stream` is +destroyed after either receiving an `RST_STREAM` frame from the connected peer, +calling `http2stream.rstStream()`, or `http2stream.destroy()`. Will be +`undefined` if the `Http2Stream` has not been closed. + +#### http2stream.rstStream(code) + + +* code {number} Unsigned 32-bit integer identifying the error code. Defaults to + `http2.constant.NGHTTP2_NO_ERROR` (`0x00`) +* Returns: {undefined} + +Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing this +`Http2Stream` to be closed on both sides using [error code][] `code`. + +#### http2stream.rstWithNoError() + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error). + +#### http2stream.rstWithProtocolError() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error). + +#### http2stream.rstWithCancel() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel). + +#### http2stream.rstWithRefuse() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream). + +#### http2stream.rstWithInternalError() { + + +* Returns: {undefined} + +Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error). + +#### http2stream.session + + +* Value: {Http2Sesssion} + +A reference to the `Http2Session` instance that owns this `Http2Stream`. The +value will be `undefined` after the `Http2Stream` instance is destroyed. + +#### http2stream.setTimeout(msecs, callback) + + +* `msecs` {number} +* `callback` {Function} +* Returns: {undefined} + +```js +const http2 = require('http2'); +const client = http2.connect('http://example.org:8000'); + +const req = client.request({ ':path': '/' }); + +// Cancel the stream if there's no activity after 5 seconds +req.setTimeout(5000, () => req.rstStreamWithCancel()); +``` + +#### http2stream.state + + +* Value: {Object} + * `localWindowSize` {number} + * `state` {number} + * `streamLocalClose` {number} + * `streamRemoteClose` {number} + * `sumDependencyWeight` {number} + * `weight` {number} + +A current state of this `Http2Stream`. + +### Class: ClientHttp2Stream + + +* Extends {Http2Stream} + +The `ClientHttp2Stream` class is an extension of `Http2Stream` that is +used exclusively on HTTP/2 Clients. `Http2Stream` instances on the client +provide events such as `'response'` and `'push'` that are only relevant on +the client. + +#### Event: 'headers' + + +The `'headers'` event is emitted when an additional block of headers is received +for a stream, such as when a block of `1xx` informational headers are received. +The listener callback is passed the [Headers Object][] and flags associated with +the headers. + +```js +stream.on('headers', (headers, flags) => { + console.log(headers); +}); +``` + +#### Event: 'push' + + +The `'push'` event is emitted when response headers for a Server Push stream +are received. The listener callback is passed the [Headers Object][] and flags +associated with the headers. + +```js +stream.on('push', (headers, flags) => { + console.log(headers); +}); +``` + +#### Event: 'response' + + +The `'response'` event is emitted when a response `HEADERS` frame has been +received for this stream from the connected HTTP/2 server. The listener is +invoked with two arguments: an Object containing the received +[Headers Object][], and flags associated with the headers. + +For example: + +```js +const http2 = require('http'); +const client = http2.connect('https://localhost'); +const req = client.request({ ':path': '/' }); +req.on('response', (headers, flags) => { + console.log(headers[':status']); +}); +``` + +### Class: ServerHttp2Stream + + +* Extends: {Http2Stream} + +The `ServerHttp2Stream` class is an extension of [`Http2Stream`][] that is +used exclusively on HTTP/2 Servers. `Http2Stream` instances on the server +provide additional methods such as `http2stream.pushStream()` and +`http2stream.respond()` that are only relevant on the server. + +#### http2stream.additionalHeaders(headers) + + +* `headers` {[Headers Object][]} +* Returns: {undefined} + +Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer. + +#### http2stream.headersSent + + +* Value: {boolean} + +Boolean (read-only). True if headers were sent, false otherwise. + +#### http2stream.pushAllowed + + +* Value: {boolean} + +Read-only property mapped to the `SETTINGS_ENABLE_PUSH` flag of the remote +client's most recent `SETTINGS` frame. Will be `true` if the remote peer +accepts push streams, `false` otherwise. Settings are the same for every +`Http2Stream` in the same `Http2Session`. + +#### http2stream.pushStream(headers[, options], callback) + + +* `headers` {[Headers Object][]} +* `options` {Object} + * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, + the created stream is made the sole direct dependency of the parent, with + all other existing dependents made a dependent of the newly created stream. + Defaults to `false`. + * `parent` {number} Specifies the numeric identifier of a stream the newly + created stream is dependent on. + * `weight` {number} Specifies the relative dependency of a stream in relation + to other streams with the same `parent`. The value is a number between `1` + and `256` (inclusive). +* `callback` {Function} Callback that is called once the push stream has been + initiated. +* Returns: {undefined} + +Initiates a push stream. The callback is invoked with the new `Htt2Stream` +instance created for the push stream. + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond({ ':status': 200 }); + stream.pushStream({ ':path': '/' }, (pushStream) => { + pushStream.respond({ ':status': 200 }); + pushStream.end('some pushed data'); + }); + stream.end('some data'); +}); +``` + +#### http2stream.respond([headers[, options]]) + + +* `headers` {[Headers Object][]} +* `options` {Object} + * `endStream` {boolean} Set to `true` to indicate that the response will not + include payload data. +* Returns: {undefined} + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond({ ':status': 200 }); + stream.end('some data'); +}); +``` + +#### http2stream.respondWithFD(fd[, headers]) + + +* `fd` {number} A readable file descriptor +* `headers` {[Headers Object][]} + +Initiates a response whose data is read from the given file descriptor. No +validation is performed on the given file descriptor. If an error occurs while +attempting to read data using the file descriptor, the `Http2Stream` will be +closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR` code. + +When used, the `Http2Stream` object's Duplex interface will be closed +automatically. HTTP trailer fields cannot be sent. The `'fetchTrailers'` event +will *not* be emitted. + +```js +const http2 = require('http2'); +const fs = require('fs'); + +const fd = fs.openSync('/some/file', 'r'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + const stat = fs.fstatSync(fd); + const headers = { + 'content-length': stat.size, + 'last-modified': stat.mtime.toUTCString(), + 'content-type': 'text/plain' + }; + stream.respondWithFD(fd, headers); +}); +server.on('close', () => fs.closeSync(fd)); +``` + +#### http2stream.respondWithFile(path[, headers[, options]]) + + +* `path` {string|Buffer|URL} +* `headers` {[Headers Object][]} +* `options` {Object} + * `statCheck` {Function} + +Sends a regular file as the response. The `path` must specify a regular file +or an `'error'` event will be emitted on the `Http2Stream` object. + +When used, the `Http2Stream` object's Duplex interface will be closed +automatically. HTTP trailer fields cannot be sent. The `'fetchTrailers'` event +will *not* be emitted. + +The optional `options.statCheck` function may be specified to give user code +an opportunity to set additional content headers based on the `fs.Stat` details +of the given file: + +If an error occurs while attempting to read the file data, the `Http2Stream` +will be closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR` +code. + +Example using a file path: + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + function statCheck(stat, headers) { + headers['last-modified'] = stat.mtime.toUTCString(); + } + stream.respondWithFile('/some/file', + { 'content-type': 'text/plain' }, + { statCheck }); +}); +``` + +The `options.statCheck` function may also be used to cancel the send operation +by returning `false`. For instance, a conditional request may check the stat +results to determine if the file has been modified to return an appropriate +`304` response: + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream) => { + function statCheck(stat, headers) { + // Check the stat here... + stream.respond({ ':status': 304 }); + return false; // Cancel the send operation + } + stream.respondWithFile('/some/file', + { 'content-type': 'text/plain' }, + { statCheck }); +}); +``` + +The `content-length` header field will be automatically set. + +### Class: Http2Server + + +* Extends: {net.Server} + +#### Event: 'sessionError' + + +The `'sessionError'` event is emitted when an `'error'` event is emitted by +an `Http2Session` object. If no listener is registered for this event, an +`'error'` event is emitted. + +#### Event: 'socketError' + + +The `'socketError'` event is emitted when an `'error'` event is emitted by +a `Socket` associated with the server. If no listener is registered for this +event, an `'error'` event is emitted. + +#### Event: 'stream' + + +The `'stream'` event is emitted when a `'stream'` event has been emitted by +an `Http2Session` associated with the server. + +```js +const http2 = require('http2'); +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_TYPE +} = http2.constants; + +const server = http.createServer(); +server.on('stream', (stream, headers, flags) => { + const method = headers[HTTP2_HEADER_METHOD]; + const path = headers[HTTP2_HEADER_PATH]; + // ... + stream.respond({ + [HTTP2_HEADER_STATUS]: 200, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + stream.write('hello '); + stream.end('world'); +}); +``` + +#### Event: 'timeout' + + +The `'timeout'` event is emitted when there is no activity on the Server for +a given number of milliseconds set using `http2server.setTimeout()`. + +### Class: Http2SecureServer + + +* Extends: {tls.Server} + +#### Event: 'sessionError' + + +The `'sessionError'` event is emitted when an `'error'` event is emitted by +an `Http2Session` object. If no listener is registered for this event, an +`'error'` event is emitted on the `Http2Session` instance instead. + +#### Event: 'socketError' + + +The `'socketError'` event is emitted when an `'error'` event is emitted by +a `Socket` associated with the server. If no listener is registered for this +event, an `'error'` event is emitted on the `Socket` instance instead. + +#### Event: 'unknownProtocol' + + +The `'unknownProtocol'` event is emitted when a connecting client fails to +negotiate an allowed protocol (i.e. HTTP/2 or HTTP/1.1). The event handler +receives the socket for handling. If no listener is registered for this event, +the connection is terminated. See the + +#### Event: 'stream' + + +The `'stream'` event is emitted when a `'stream'` event has been emitted by +an `Http2Session` associated with the server. + +```js +const http2 = require('http2'); +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_TYPE +} = http2.constants; + +const options = getOptionsSomehow(); + +const server = http.createSecureServer(options); +server.on('stream', (stream, headers, flags) => { + const method = headers[HTTP2_HEADER_METHOD]; + const path = headers[HTTP2_HEADER_PATH]; + // ... + stream.respond({ + [HTTP2_HEADER_STATUS]: 200, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + stream.write('hello '); + stream.end('world'); +}); +``` + +#### Event: 'timeout' + + +### http2.createServer(options[, onRequestHandler]) + + +* `options` {Object} + * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size + for deflating header fields. Defaults to 4Kib. + * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a + serialized, compressed block of headers. Attempts to send headers that + exceed this limit will result in a `'frameError'` event being emitted + and the stream being closed and destroyed. + * `paddingStrategy` {number} Identifies the strategy used for determining the + amount of padding to use for HEADERS and DATA frames. Defaults to + `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of: + * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is + to be applied. + * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum + amount of padding, as determined by the internal implementation, is to + be applied. + * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user + provided `options.selectPadding` callback is to be used to determine the + amount of padding. + * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent + streams for the remote peer as if a SETTINGS frame had been received. Will + be overridden if the remote peer sets its own value for + `maxConcurrentStreams`. Defaults to 100. + * `selectPadding` {Function} When `options.paddingStrategy` is equal to + `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function + used to determine the padding. See [Using options.selectPadding][]. + * `settings` {[Settings Object][]} The initial settings to send to the + remote peer upon connection. +* `onRequestHandler` {Function} See [Compatibility API][] +* Returns: {Http2Server} + +Returns a `net.Server` instance that creates and manages `Http2Session` +instances. + +```js +const http2 = require('http2'); + +// Create a plain-text HTTP/2 server +const server = http2.createServer(); + +server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('

Hello World

'); +}); + +server.listen(80); +``` + +### http2.createSecureServer(options[, onRequestHandler]) + + +* `options` {Object} + * `allowHTTP1` {boolean} Incoming client connections that do not support + HTTP/2 will be downgraded to HTTP/1.x when set to `true`. The default value + is `false`. See the [`'unknownProtocol'`][] event. + * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size + for deflating header fields. Defaults to 4Kib. + * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a + serialized, compressed block of headers. Attempts to send headers that + exceed this limit will result in a `'frameError'` event being emitted + and the stream being closed and destroyed. + * `paddingStrategy` {number} Identifies the strategy used for determining the + amount of padding to use for HEADERS and DATA frames. Defaults to + `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of: + * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is + to be applied. + * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum + amount of padding, as determined by the internal implementation, is to + be applied. + * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user + provided `options.selectPadding` callback is to be used to determine the + amount of padding. + * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent + streams for the remote peer as if a SETTINGS frame had been received. Will + be overridden if the remote peer sets its own value for + `maxConcurrentStreams`. Defaults to 100. + * `selectPadding` {Function} When `options.paddingStrategy` is equal to + `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function + used to determine the padding. See [Using options.selectPadding][]. + * `settings` {[Settings Object][]} The initial settings to send to the + remote peer upon connection. + * ...: Any [`tls.createServer()`][] options can be provided. For + servers, the identity options (`pfx` or `key`/`cert`) are usually required. +* `onRequestHandler` {Function} See [Compatibility API][] +* Returns {Http2SecureServer} + +Returns a `tls.Server` instance that creates and manages `Http2Session` +instances. + +```js +const http2 = require('http2'); + +const options = { + key: fs.readFileSync('server-key.pem'), + cert: fs.readFileSync('server-cert.pem') +}; + +// Create a plain-text HTTP/2 server +const server = http2.createSecureServer(options); + +server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('

Hello World

'); +}); + +server.listen(80); +``` + +### http2.connect(authority[, options][, listener]) + + +* `authority` {string|URL} +* `options` {Object} + * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size + for deflating header fields. Defaults to 4Kib. + * `maxReservedRemoteStreams` {number} Sets the maximum number of reserved push + streams the client will accept at any given time. Once the current number of + currently reserved push streams exceeds reaches this limit, new push streams + sent by the server will be automatically rejected. + * `maxSendHeaderBlockLength` {number} Sets the maximum allowed size for a + serialized, compressed block of headers. Attempts to send headers that + exceed this limit will result in a `'frameError'` event being emitted + and the stream being closed and destroyed. + * `paddingStrategy` {number} Identifies the strategy used for determining the + amount of padding to use for HEADERS and DATA frames. Defaults to + `http2.constants.PADDING_STRATEGY_NONE`. Value may be one of: + * `http2.constants.PADDING_STRATEGY_NONE` - Specifies that no padding is + to be applied. + * `http2.constants.PADDING_STRATEGY_MAX` - Specifies that the maximum + amount of padding, as determined by the internal implementation, is to + be applied. + * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user + provided `options.selectPadding` callback is to be used to determine the + amount of padding. + * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent + streams for the remote peer as if a SETTINGS frame had been received. Will + be overridden if the remote peer sets its own value for + `maxConcurrentStreams`. Defaults to 100. + * `selectPadding` {Function} When `options.paddingStrategy` is equal to + `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function + used to determine the padding. See [Using options.selectPadding][]. + * `settings` {[Settings Object][]} The initial settings to send to the + remote peer upon connection. +* `listener` {Function} +* Returns {Http2Session} + +Returns a HTTP/2 client `Http2Session` instance. + +```js +const http2 = require('http2'); +const client = http2.connect('https://localhost:1234'); + +/** use the client **/ + +client.destroy(); +``` + +### http2.constants + + +#### Error Codes for RST_STREAM and GOAWAY + + +| Value | Name | Constant | +|-------|---------------------|-----------------------------------------------| +| 0x00 | No Error | `http2.constants.NGHTTP2_NO_ERROR` | +| 0x01 | Protocol Error | `http2.constants.NGHTTP2_PROTOCOL_ERROR` | +| 0x02 | Internal Error | `http2.constants.NGHTTP2_INTERNAL_ERROR` | +| 0x03 | Flow Control Error | `http2.constants.NGHTTP2_FLOW_CONTROL_ERROR` | +| 0x04 | Settings Timeout | `http2.constants.NGHTTP2_SETTINGS_TIMEOUT` | +| 0x05 | Stream Closed | `http2.constants.NGHTTP2_STREAM_CLOSED` | +| 0x06 | Frame Size Error | `http2.constants.NGHTTP2_FRAME_SIZE_ERROR` | +| 0x07 | Refused Stream | `http2.constants.NGHTTP2_REFUSED_STREAM` | +| 0x08 | Cancel | `http2.constants.NGHTTP2_CANCEL` | +| 0x09 | Compression Error | `http2.constants.NGHTTP2_COMPRESSION_ERROR` | +| 0x0a | Connect Error | `http2.constants.NGHTTP2_CONNECT_ERROR` | +| 0x0b | Enhance Your Calm | `http2.constants.NGHTTP2_ENHANCE_YOUR_CALM` | +| 0x0c | Inadequate Security | `http2.constants.NGHTTP2_INADEQUATE_SECURITY` | +| 0x0d | HTTP/1.1 Required | `http2.constants.NGHTTP2_HTTP_1_1_REQUIRED` | + +The `'timeout'` event is emitted when there is no activity on the Server for +a given number of milliseconds set using `http2server.setTimeout()`. + +### http2.getDefaultSettings() + + +* Returns: {[Settings Object][]} + +Returns an object containing the default settings for an `Http2Session` +instance. This method returns a new object instance every time it is called +so instances returned may be safely modified for use. + +### http2.getPackedSettings(settings) + + +* `settings` {[Settings Object][]} +* Returns: {Buffer} + +Returns a [Buffer][] instance containing serialized representation of the given +HTTP/2 settings as specified in the [HTTP/2][] specification. This is intended +for use with the `HTTP2-Settings` header field. + +```js +const http2 = require('http2'); + +const packed = http2.getPackedSettings({ enablePush: false }); + +console.log(packed.toString('base64')); +// Prints: AAIAAAAA +``` + +### http2.getUnpackedSettings(buf) + + +* `buf` {Buffer|Uint8Array} The packed settings +* Returns: {[Settings Object][]} + +Returns a [Settings Object][] containing the deserialized settings from the +given `Buffer` as generated by `http2.getPackedSettings()`. + +### Headers Object + +Headers are represented as own-properties on JavaScript objects. The property +keys will be serialized to lower-case. Property values should be strings (if +they are not they will be coerced to strings) or an Array of strings (in order +to send more than one value per header field). + +For example: + +```js +const headers = { + ':status': '200', + 'content-type': 'text-plain', + 'ABC': ['has', 'more', 'than', 'one', 'value'] +}; + +stream.respond(headers); +``` + +*Note*: Header objects passed to callback functions will have a `null` +prototype. This means that normal JavaScript object methods such as +`Object.prototype.toString()` and `Object.prototype.hasOwnProperty()` will +not work. + +```js +const http2 = require('http2'); +const server = http2.createServer(); +server.on('stream', (stream, headers) => { + console.log(headers[':path']); + console.log(headers.ABC); +}); +``` + +### Settings Object + +The `http2.getDefaultSettings()`, `http2.getPackedSettings()`, +`http2.createServer()`, `http2.createSecureServer()`, +`http2session.settings()`, `http2session.localSettings`, and +`http2session.remoteSettings` APIs either return or receive as input an +object that defines configuration settings for an `Http2Session` object. +These objects are ordinary JavaScript objects containing the following +properties. + +* `headerTableSize` {number} Specifies the maximum number of bytes used for + header compression. The default value is 4,096 octets. The minimum allowed + value is 0. The maximum allowed value is 232-1. +* `enablePush` {boolean} Specifies `true` if HTTP/2 Push Streams are to be + permitted on the `Http2Session` instances. +* `initialWindowSize` {number} Specifies the *senders* initial window size + for stream-level flow control. The default value is 65,535 bytes. The minimum + allowed value is 0. The maximum allowed value is 232-1. +* `maxFrameSize` {number} Specifies the size of the largest frame payload. + The default and the minimum allowed value is 16,384 bytes. The maximum + allowed value is 224-1. +* `maxConcurrentStreams` {number} Specifies the maximum number of concurrent + streams permitted on an `Http2Session`. There is no default value which + implies, at least theoretically, 231-1 streams may be open + concurrently at any given time in an `Http2Session`. The minimum value is + 0. The maximum allowed value is 231-1. +* `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets) + of header list that will be accepted. There is no default value. The minimum + allowed value is 0. The maximum allowed value is 232-1. + +All additional properties on the settings object are ignored. + +### Using `options.selectPadding` + +When `options.paddingStrategy` is equal to +`http2.constants.PADDING_STRATEGY_CALLBACK`, the the HTTP/2 implementation will +consult the `options.selectPadding` callback function, if provided, to determine +the specific amount of padding to use per HEADERS and DATA frame. + +The `options.selectPadding` function receives two numeric arguments, +`frameLen` and `maxFrameLen` and must return a number `N` such that +`frameLen <= N <= maxFrameLen`. + +```js +const http2 = require('http2'); +const server = http2.createServer({ + paddingStrategy: http2.constants.PADDING_STRATEGY_CALLBACK, + selectPadding(frameLen, maxFrameLen) { + return maxFrameLen; + } +}); +``` + +*Note*: The `options.selectPadding` function is invoked once for *every* +HEADERS and DATA frame. This has a definite noticeable impact on +performance. + +### Error Handling + +There are several types of error conditions that may arise when using the +`http2` module: + +Validation Errors occur when an incorrect argument, option or setting value is +passed in. These will always be reported by a synchronous `throw`. + +State Errors occur when an action is attempted at an incorrect time (for +instance, attempting to send data on a stream after it has closed). These will +be repoorted using either a synchronous `throw` or via an `'error'` event on +the `Http2Stream`, `Http2Session` or HTTP/2 Server objects, depending on where +and when the error occurs. + +Internal Errors occur when an HTTP/2 session fails unexpectedly. These will be +reported via an `'error'` event on the `Http2Session` or HTTP/2 Server objects. + +Protocol Errors occur when various HTTP/2 protocol constraints are violated. +These will be reported using either a synchronous `throw` or via an `'error'` +event on the `Http2Stream`, `Http2Session` or HTTP/2 Server objects, depending +on where and when the error occurs. + +### Push streams on the client + +To receive pushed streams on the client, set a listener for the `'stream'` +event on the `ClientHttp2Session`: + +```js +const http2 = require('http2'); + +const client = http2.connect('http://localhost'); + +client.on('stream', (pushedStream, requestHeaders) => { + pushedStream.on('push', (responseHeaders) => { + // process response headers + }); + pushedStream.on('data', (chunk) => { /* handle pushed data */ }); +}); + +const req = client.request({ ':path': '/' }); +``` + +### Supporting the CONNECT method + +The `CONNECT` method is used to allow an HTTP/2 server to be used as a proxy +for TCP/IP connections. + +A simple TCP Server: +```js +const net = require('net'); + +const server = net.createServer((socket) => { + let name = ''; + socket.setEncoding('utf8'); + socket.on('data', (chunk) => name += chunk); + socket.on('end', () => socket.end(`hello ${name}`)); +}); + +server.listen(8000); +``` + +An HTTP/2 CONNECT proxy: + +```js +const http2 = require('http2'); +const net = require('net'); +const { URL } = require('url'); + +const proxy = http2.createServer(); +proxy.on('stream', (stream, headers) => { + if (headers[':method'] !== 'CONNECT') { + // Only accept CONNECT requests + stream.rstWithRefused(); + return; + } + const auth = new URL(`tcp://${headers[':authority']}`); + // It's a very good idea to verify that hostname and port are + // things this proxy should be connecting to. + const socket = net.connect(auth.port, auth.hostname, () => { + stream.respond(); + socket.pipe(stream); + stream.pipe(socket); + }); + socket.on('error', (error) => { + stream.rstStream(http2.constants.NGHTTP2_CONNECT_ERROR); + }); +}); + +proxy.listen(8001); +``` + +An HTTP/2 CONNECT client: + +```js +const http2 = require('http2'); + +const client = http2.connect('http://localhost:8001'); + +// Must not specify the ':path' and ':scheme' headers +// for CONNECT requests or an error will be thrown. +const req = client.request({ + ':method': 'CONNECT', + ':authority': `localhost:${port}` +}); + +req.on('response', common.mustCall()); +let data = ''; +req.setEncoding('utf8'); +req.on('data', (chunk) => data += chunk); +req.on('end', () => { + console.log(`The server says: ${data}`); + client.destroy(); +}); +req.end('Jane'); +``` + +## Compatibility API + +TBD + + +[HTTP/2]: https://tools.ietf.org/html/rfc7540 +[HTTP/1]: http.html +[`net.Socket`]: net.html +[`tls.TLSSocket`]: tls.html +[`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener +[ClientHttp2Stream]: #http2_class_clienthttp2stream +[Compatibility API: #http2_compatibility_api +[`Duplex`]: stream.html#stream_class_stream_duplex +[Headers Object]: #http2_headers_object +[Http2Stream]: #http2_class_http2stream +[Http2Session and Sockets]: #http2_http2sesion_and_sockets +[ServerHttp2Stream]: #http2_class_serverhttp2stream +[Settings Object]: #http2_settings_object +[Using options.selectPadding]: #http2_using_options_selectpadding +[error code]: #error_codes +[`'unknownProtocol'`]: #http2_event_unknownprotocol diff --git a/doc/guides/writing-and-running-benchmarks.md b/doc/guides/writing-and-running-benchmarks.md index 3135f2115d78cb..7aeb9728aaedf0 100644 --- a/doc/guides/writing-and-running-benchmarks.md +++ b/doc/guides/writing-and-running-benchmarks.md @@ -41,6 +41,14 @@ benchmarker to be used should be specified by providing it as an argument: `node benchmark/http/simple.js benchmarker=autocannon` +#### HTTP/2 Benchmark Requirements + +To run the `http2` benchmarks, the `h2load` benchmarker must be used. The +`h2load` tool is a component of the `nghttp2` project and may be installed +from [nghttp.org][] or built from source. + +`node benchmark/http2/simple.js benchmarker=autocannon` + ### Benchmark Analysis Requirements To analyze the results, `R` should be installed. Use one of the available @@ -423,3 +431,4 @@ Supported options keys are: [wrk]: https://github.com/wg/wrk [t-test]: https://en.wikipedia.org/wiki/Student%27s_t-test#Equal_or_unequal_sample_sizes.2C_unequal_variances [git-for-windows]: http://git-scm.com/download/win +[nghttp2.org]: http://nghttp2.org diff --git a/doc/node.1 b/doc/node.1 index 753bf0f78d0b87..cf79ce33f929df 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -130,6 +130,10 @@ Emit pending deprecation warnings. .BR \-\-no\-warnings Silence all process warnings (including deprecations). +.TP +.BR \-\-expose\-http2 +Enable the experimental `'http2'` module. + .TP .BR \-\-napi\-modules Enable loading native modules compiled with the ABI-stable Node.js API (N-API) diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js old mode 100755 new mode 100644 diff --git a/lib/http2.js b/lib/http2.js new file mode 100644 index 00000000000000..e964abf589d0eb --- /dev/null +++ b/lib/http2.js @@ -0,0 +1,27 @@ +'use strict'; + +process.emitWarning( + 'The http2 module is an experimental API.', + 'ExperimentalWarning', undefined, + 'See https://github.com/nodejs/http2' +); + +const { + constants, + getDefaultSettings, + getPackedSettings, + getUnpackedSettings, + createServer, + createSecureServer, + connect +} = require('internal/http2/core'); + +module.exports = { + constants, + getDefaultSettings, + getPackedSettings, + getUnpackedSettings, + createServer, + createSecureServer, + connect +}; diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 9b56faa75b6158..01a16a9f0c0936 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -498,6 +498,11 @@ NativeModule._source = process.binding('natives'); NativeModule._cache = {}; + const config = process.binding('config'); + + if (!config.exposeHTTP2) + delete NativeModule._source.http2; + NativeModule.require = function(id) { if (id === 'native_module') { return NativeModule; @@ -536,8 +541,6 @@ return NativeModule._source.hasOwnProperty(id); }; - const config = process.binding('config'); - if (config.exposeInternals) { NativeModule.nonInternalExists = NativeModule.exists; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 3b54dcea934f83..b7dd509070731d 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -119,6 +119,69 @@ E('ERR_HTTP_HEADERS_SENT', E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s'); E('ERR_HTTP_TRAILER_INVALID', 'Trailers are invalid with this transfer encoding'); +E('ERR_HTTP_INVALID_CHAR', 'Invalid character in statusMessage.'); +E('ERR_HTTP_INVALID_STATUS_CODE', + (originalStatusCode) => `Invalid status code: ${originalStatusCode}`); +E('ERR_HTTP2_CONNECT_AUTHORITY', + ':authority header is required for CONNECT requests'); +E('ERR_HTTP2_CONNECT_PATH', + 'The :path header is forbidden for CONNECT requests'); +E('ERR_HTTP2_CONNECT_SCHEME', + 'The :scheme header is forbidden for CONNECT requests'); +E('ERR_HTTP2_FRAME_ERROR', + (type, code, id) => { + let msg = `Error sending frame type ${type}`; + if (id !== undefined) + msg += ` for stream ${id}`; + msg += ` with code ${code}`; + return msg; + }); +E('ERR_HTTP2_HEADER_REQUIRED', + (name) => `The ${name} header is required`); +E('ERR_HTTP2_HEADER_SINGLE_VALUE', + (name) => `Header field "${name}" must have only a single value`); +E('ERR_HTTP2_HEADERS_OBJECT', 'Headers must be an object'); +E('ERR_HTTP2_HEADERS_SENT', 'Response has already been initiated.'); +E('ERR_HTTP2_HEADERS_AFTER_RESPOND', + 'Cannot specify additional headers after response initiated'); +E('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND', + 'Cannot send informational headers after the HTTP message has been sent'); +E('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + 'Informational status codes cannot be used'); +E('ERR_HTTP2_INVALID_CONNECTION_HEADERS', + 'HTTP/1 Connection specific headers are forbidden'); +E('ERR_HTTP2_INVALID_HEADER_VALUE', 'Value must not be undefined or null'); +E('ERR_HTTP2_INVALID_INFO_STATUS', + (code) => `Invalid informational status code: ${code}`); +E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', + 'Packed settings length must be a multiple of six'); +E('ERR_HTTP2_INVALID_PSEUDOHEADER', + (name) => `"${name}" is an invalid pseudoheader or is used incorrectly`); +E('ERR_HTTP2_INVALID_SESSION', 'The session has been destroyed'); +E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed'); +E('ERR_HTTP2_INVALID_SETTING_VALUE', + (name, value) => `Invalid value for setting "${name}": ${value}`); +E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + (max) => `Maximum number of pending settings acknowledgements (${max})`); +E('ERR_HTTP2_PAYLOAD_FORBIDDEN', + (code) => `Responses with ${code} status must not have a payload`); +E('ERR_HTTP2_OUT_OF_STREAMS', + 'No stream ID is available because maximum stream ID has been reached'); +E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers'); +E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams'); +E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent'); +E('ERR_HTTP2_SOCKET_BOUND', + 'The socket is already bound to an Http2Session'); +E('ERR_HTTP2_STATUS_INVALID', + (code) => `Invalid status code: ${code}`); +E('ERR_HTTP2_STATUS_101', + 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2'); +E('ERR_HTTP2_STREAM_CLOSED', 'The stream is already closed'); +E('ERR_HTTP2_STREAM_ERROR', + (code) => `Stream closed with error code ${code}`); +E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', 'A stream cannot depend on itself'); +E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', + (protocol) => `protocol "${protocol}" is unsupported.`); E('ERR_INDEX_OUT_OF_RANGE', 'Index out of range'); E('ERR_INVALID_ARG_TYPE', invalidArgType); E('ERR_INVALID_CALLBACK', 'callback must be a function'); @@ -156,6 +219,7 @@ E('ERR_SOCKET_BAD_TYPE', E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data'); E('ERR_SOCKET_BAD_PORT', 'Port should be > 0 and < 65536'); E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running'); +E('ERR_OUTOFMEMORY', 'Out of memory'); E('ERR_STDERR_CLOSE', 'process.stderr cannot be closed'); E('ERR_STDOUT_CLOSE', 'process.stdout cannot be closed'); E('ERR_UNKNOWN_BUILTIN_MODULE', (id) => `No such built-in module: ${id}`); diff --git a/lib/internal/http.js b/lib/internal/http.js old mode 100755 new mode 100644 diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js new file mode 100644 index 00000000000000..cd9a1fa2b7f5ed --- /dev/null +++ b/lib/internal/http2/compat.js @@ -0,0 +1,570 @@ +'use strict'; + +const Stream = require('stream'); +const Readable = Stream.Readable; +const binding = process.binding('http2'); +const constants = binding.constants; +const errors = require('internal/errors'); + +const kFinish = Symbol('finish'); +const kBeginSend = Symbol('begin-send'); +const kState = Symbol('state'); +const kStream = Symbol('stream'); +const kRequest = Symbol('request'); +const kResponse = Symbol('response'); +const kHeaders = Symbol('headers'); +const kTrailers = Symbol('trailers'); + +let statusMessageWarned = false; + +// Defines and implements an API compatibility layer on top of the core +// HTTP/2 implementation, intended to provide an interface that is as +// close as possible to the current require('http') API + +function assertValidHeader(name, value) { + if (isPseudoHeader(name)) + throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED'); + if (value === undefined || value === null) + throw new errors.TypeError('ERR_HTTP2_INVALID_HEADER_VALUE'); +} + +function isPseudoHeader(name) { + switch (name) { + case ':status': + return true; + case ':method': + return true; + case ':path': + return true; + case ':authority': + return true; + case ':scheme': + return true; + default: + return false; + } +} + +function onStreamData(chunk) { + const request = this[kRequest]; + if (!request.push(chunk)) + this.pause(); +} + +function onStreamEnd() { + // Cause the request stream to end as well. + const request = this[kRequest]; + request.push(null); +} + +function onStreamError(error) { + const request = this[kRequest]; + request.emit('error', error); +} + +function onRequestPause() { + const stream = this[kStream]; + stream.pause(); +} + +function onRequestResume() { + const stream = this[kStream]; + stream.resume(); +} + +function onRequestDrain() { + if (this.isPaused()) + this.resume(); +} + +function onStreamResponseDrain() { + const response = this[kResponse]; + response.emit('drain'); +} + +function onStreamResponseError(error) { + const response = this[kResponse]; + response.emit('error', error); +} + +function onStreamClosedRequest() { + const req = this[kRequest]; + req.push(null); +} + +function onStreamClosedResponse() { + const res = this[kResponse]; + res.writable = false; + res.emit('finish'); +} + +function onAborted(hadError, code) { + if ((this.writable) || + (this._readableState && !this._readableState.ended)) { + this.emit('aborted', hadError, code); + } +} + +class Http2ServerRequest extends Readable { + constructor(stream, headers, options) { + super(options); + this[kState] = { + statusCode: null, + closed: false, + closedCode: constants.NGHTTP2_NO_ERROR + }; + this[kHeaders] = headers; + this[kStream] = stream; + stream[kRequest] = this; + + // Pause the stream.. + stream.pause(); + stream.on('data', onStreamData); + stream.on('end', onStreamEnd); + stream.on('error', onStreamError); + stream.on('close', onStreamClosedRequest); + stream.on('aborted', onAborted.bind(this)); + const onfinish = this[kFinish].bind(this); + stream.on('streamClosed', onfinish); + stream.on('finish', onfinish); + this.on('pause', onRequestPause); + this.on('resume', onRequestResume); + this.on('drain', onRequestDrain); + } + + get closed() { + const state = this[kState]; + return Boolean(state.closed); + } + + get code() { + const state = this[kState]; + return Number(state.closedCode); + } + + get stream() { + return this[kStream]; + } + + get statusCode() { + return this[kState].statusCode; + } + + get headers() { + return this[kHeaders]; + } + + get rawHeaders() { + const headers = this[kHeaders]; + if (headers === undefined) + return []; + const tuples = Object.entries(headers); + const flattened = Array.prototype.concat.apply([], tuples); + return flattened.map(String); + } + + get trailers() { + return this[kTrailers]; + } + + get httpVersionMajor() { + return 2; + } + + get httpVersionMinor() { + return 0; + } + + get httpVersion() { + return '2.0'; + } + + get socket() { + return this.stream.session.socket; + } + + get connection() { + return this.socket; + } + + _read(nread) { + const stream = this[kStream]; + if (stream) { + stream.resume(); + } else { + throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + } + } + + get method() { + const headers = this[kHeaders]; + if (headers === undefined) + return; + return headers[constants.HTTP2_HEADER_METHOD]; + } + + get authority() { + const headers = this[kHeaders]; + if (headers === undefined) + return; + return headers[constants.HTTP2_HEADER_AUTHORITY]; + } + + get scheme() { + const headers = this[kHeaders]; + if (headers === undefined) + return; + return headers[constants.HTTP2_HEADER_SCHEME]; + } + + get url() { + return this.path; + } + + set url(url) { + this.path = url; + } + + get path() { + const headers = this[kHeaders]; + if (headers === undefined) + return; + return headers[constants.HTTP2_HEADER_PATH]; + } + + set path(path) { + let headers = this[kHeaders]; + if (headers === undefined) + headers = this[kHeaders] = Object.create(null); + headers[constants.HTTP2_HEADER_PATH] = path; + } + + setTimeout(msecs, callback) { + const stream = this[kStream]; + if (stream === undefined) return; + stream.setTimeout(msecs, callback); + } + + [kFinish](code) { + const state = this[kState]; + if (state.closed) + return; + state.closedCode = code; + state.closed = true; + this.push(null); + this[kStream] = undefined; + } +} + +class Http2ServerResponse extends Stream { + constructor(stream, options) { + super(options); + this[kState] = { + sendDate: true, + statusCode: constants.HTTP_STATUS_OK, + headerCount: 0, + trailerCount: 0, + closed: false, + closedCode: constants.NGHTTP2_NO_ERROR + }; + this[kStream] = stream; + stream[kResponse] = this; + this.writable = true; + stream.on('drain', onStreamResponseDrain); + stream.on('error', onStreamResponseError); + stream.on('close', onStreamClosedResponse); + stream.on('aborted', onAborted.bind(this)); + const onfinish = this[kFinish].bind(this); + stream.on('streamClosed', onfinish); + stream.on('finish', onfinish); + } + + get finished() { + const stream = this[kStream]; + return stream === undefined || stream._writableState.ended; + } + + get closed() { + const state = this[kState]; + return Boolean(state.closed); + } + + get code() { + const state = this[kState]; + return Number(state.closedCode); + } + + get stream() { + return this[kStream]; + } + + get headersSent() { + const stream = this[kStream]; + return stream.headersSent; + } + + get sendDate() { + return Boolean(this[kState].sendDate); + } + + set sendDate(bool) { + this[kState].sendDate = Boolean(bool); + } + + get statusCode() { + return this[kState].statusCode; + } + + set statusCode(code) { + const state = this[kState]; + code |= 0; + if (code >= 100 && code < 200) + throw new errors.RangeError('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED'); + if (code < 200 || code > 599) + throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID', code); + state.statusCode = code; + } + + addTrailers(headers) { + let trailers = this[kTrailers]; + const keys = Object.keys(headers); + let key = ''; + if (keys.length > 0) + return; + if (trailers === undefined) + trailers = this[kTrailers] = Object.create(null); + for (var i = 0; i < keys.length; i++) { + key = String(keys[i]).trim().toLowerCase(); + const value = headers[key]; + assertValidHeader(key, value); + trailers[key] = String(value); + } + } + + getHeader(name) { + const headers = this[kHeaders]; + if (headers === undefined) + return; + name = String(name).trim().toLowerCase(); + return headers[name]; + } + + getHeaderNames() { + const headers = this[kHeaders]; + if (headers === undefined) + return []; + return Object.keys(headers); + } + + getHeaders() { + const headers = this[kHeaders]; + return Object.assign({}, headers); + } + + hasHeader(name) { + const headers = this[kHeaders]; + if (headers === undefined) + return false; + name = String(name).trim().toLowerCase(); + return Object.prototype.hasOwnProperty.call(headers, name); + } + + removeHeader(name) { + const headers = this[kHeaders]; + if (headers === undefined) + return; + name = String(name).trim().toLowerCase(); + delete headers[name]; + } + + setHeader(name, value) { + name = String(name).trim().toLowerCase(); + assertValidHeader(name, value); + let headers = this[kHeaders]; + if (headers === undefined) + headers = this[kHeaders] = Object.create(null); + headers[name] = String(value); + } + + flushHeaders() { + if (this[kStream].headersSent === false) + this[kBeginSend](); + } + + writeHead(statusCode, statusMessage, headers) { + if (typeof statusMessage === 'string' && statusMessageWarned === false) { + process.emitWarning( + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)', + 'UnsupportedWarning' + ); + statusMessageWarned = true; + } + if (headers === undefined && typeof statusMessage === 'object') { + headers = statusMessage; + } + if (headers) { + const keys = Object.keys(headers); + let key = ''; + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + this.setHeader(key, headers[key]); + } + } + this.statusCode = statusCode; + } + + write(chunk, encoding, cb) { + const stream = this[kStream]; + + if (typeof encoding === 'function') { + cb = encoding; + encoding = 'utf8'; + } + + if (stream === undefined) { + const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + if (cb) + process.nextTick(cb, err); + else + throw err; + return; + } + this[kBeginSend](); + return stream.write(chunk, encoding, cb); + } + + end(chunk, encoding, cb) { + const stream = this[kStream]; + + if (typeof chunk === 'function') { + cb = chunk; + chunk = null; + encoding = 'utf8'; + } else if (typeof encoding === 'function') { + cb = encoding; + encoding = 'utf8'; + } + if (chunk !== null && chunk !== undefined) { + this.write(chunk, encoding); + } + + if (typeof cb === 'function' && stream !== undefined) { + stream.once('finish', cb); + } + + this[kBeginSend]({endStream: true}); + + if (stream !== undefined) { + stream.end(); + } + } + + destroy(err) { + const stream = this[kStream]; + if (stream === undefined) { + // nothing to do, already closed + return; + } + stream.destroy(err); + } + + setTimeout(msecs, callback) { + const stream = this[kStream]; + if (stream === undefined) return; + stream.setTimeout(msecs, callback); + } + + sendContinue(headers) { + this.sendInfo(100, headers); + } + + sendInfo(code, headers) { + const stream = this[kStream]; + if (stream.headersSent === true) { + throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND'); + } + if (headers && typeof headers !== 'object') + throw new errors.TypeError('ERR_HTTP2_HEADERS_OBJECT'); + if (stream === undefined) return; + code |= 0; + if (code < 100 || code >= 200) + throw new errors.RangeError('ERR_HTTP2_INVALID_INFO_STATUS', code); + + headers[constants.HTTP2_HEADER_STATUS] = code; + stream.respond(headers); + } + + createPushResponse(headers, callback) { + const stream = this[kStream]; + if (stream === undefined) { + throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + } + stream.pushStream(headers, {}, function(stream, headers, options) { + const response = new Http2ServerResponse(stream); + callback(null, response); + }); + } + + [kBeginSend](options) { + const stream = this[kStream]; + if (stream !== undefined && stream.headersSent === false) { + const state = this[kState]; + const headers = this[kHeaders] || Object.create(null); + headers[constants.HTTP2_HEADER_STATUS] = state.statusCode; + if (stream.finished === true) + options.endStream = true; + if (stream.destroyed === false) { + stream.respond(headers, options); + } + } + } + + [kFinish](code) { + const state = this[kState]; + if (state.closed) + return; + state.closedCode = code; + state.closed = true; + this.end(); + this[kStream] = undefined; + this.emit('finish'); + } +} + +function onServerStream(stream, headers, flags) { + const server = this; + const request = new Http2ServerRequest(stream, headers); + const response = new Http2ServerResponse(stream); + + // Check for the CONNECT method + const method = headers[constants.HTTP2_HEADER_METHOD]; + if (method === 'CONNECT') { + if (!server.emit('connect', request, response)) { + response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED; + response.end(); + } + return; + } + + // Check for Expectations + if (headers.expect !== undefined) { + if (headers.expect === '100-continue') { + if (server.listenerCount('checkContinue')) { + server.emit('checkContinue', request, response); + } else { + response.sendContinue(); + server.emit('request', request, response); + } + } else if (server.listenerCount('checkExpectation')) { + server.emit('checkExpectation', request, response); + } else { + response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED; + response.end(); + } + return; + } + + server.emit('request', request, response); +} + +module.exports = { onServerStream }; diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js new file mode 100644 index 00000000000000..1bdd57926c4e62 --- /dev/null +++ b/lib/internal/http2/core.js @@ -0,0 +1,2392 @@ +'use strict'; + +/* eslint-disable no-use-before-define */ + +const binding = process.binding('http2'); +const debug = require('util').debuglog('http2'); +const assert = require('assert'); +const Buffer = require('buffer').Buffer; +const EventEmitter = require('events'); +const net = require('net'); +const tls = require('tls'); +const util = require('util'); +const fs = require('fs'); +const errors = require('internal/errors'); +const { Duplex } = require('stream'); +const { URL } = require('url'); +const { onServerStream } = require('internal/http2/compat'); +const { utcDate } = require('internal/http'); +const { _connectionListener: httpConnectionListener } = require('http'); +const { isUint8Array } = process.binding('util'); + +const { + assertIsObject, + assertValidPseudoHeaderResponse, + assertValidPseudoHeaderTrailer, + assertWithinRange, + getDefaultSettings, + getSessionState, + getSettings, + getStreamState, + isPayloadMeaningless, + mapToHeaders, + NghttpError, + toHeaderObject, + updateOptionsBuffer, + updateSettingsBuffer +} = require('internal/http2/util'); + +const { + _unrefActive, + enroll, + unenroll +} = require('timers'); + +const { WriteWrap } = process.binding('stream_wrap'); +const { constants } = binding; + +const NETServer = net.Server; +const TLSServer = tls.Server; + +const kInspect = require('internal/util').customInspectSymbol; + +const kAuthority = Symbol('authority'); +const kDestroySocket = Symbol('destroy-socket'); +const kHandle = Symbol('handle'); +const kID = Symbol('id'); +const kInit = Symbol('init'); +const kLocalSettings = Symbol('local-settings'); +const kOptions = Symbol('options'); +const kOwner = Symbol('owner'); +const kProceed = Symbol('proceed'); +const kProtocol = Symbol('protocol'); +const kRemoteSettings = Symbol('remote-settings'); +const kServer = Symbol('server'); +const kSession = Symbol('session'); +const kSocket = Symbol('socket'); +const kState = Symbol('state'); +const kType = Symbol('type'); + +const kDefaultSocketTimeout = 2 * 60 * 1000; +const kRenegTest = /TLS session renegotiation disabled for this socket/; + +const paddingBuffer = new Uint32Array(binding.paddingArrayBuffer); + +const { + NGHTTP2_CANCEL, + NGHTTP2_DEFAULT_WEIGHT, + NGHTTP2_FLAG_END_STREAM, + NGHTTP2_HCAT_HEADERS, + NGHTTP2_HCAT_PUSH_RESPONSE, + NGHTTP2_HCAT_RESPONSE, + NGHTTP2_INTERNAL_ERROR, + NGHTTP2_NO_ERROR, + NGHTTP2_PROTOCOL_ERROR, + NGHTTP2_REFUSED_STREAM, + NGHTTP2_SESSION_CLIENT, + NGHTTP2_SESSION_SERVER, + NGHTTP2_ERR_NOMEM, + NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE, + NGHTTP2_ERR_INVALID_ARGUMENT, + NGHTTP2_ERR_STREAM_CLOSED, + + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_DATE, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_CONTENT_LENGTH, + + NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, + NGHTTP2_SETTINGS_ENABLE_PUSH, + NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, + NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, + NGHTTP2_SETTINGS_MAX_FRAME_SIZE, + NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, + + HTTP2_METHOD_GET, + HTTP2_METHOD_HEAD, + HTTP2_METHOD_CONNECT, + + HTTP_STATUS_CONTENT_RESET, + HTTP_STATUS_OK, + HTTP_STATUS_NO_CONTENT, + HTTP_STATUS_NOT_MODIFIED, + HTTP_STATUS_SWITCHING_PROTOCOLS +} = constants; + +function sessionName(type) { + switch (type) { + case NGHTTP2_SESSION_CLIENT: + return 'client'; + case NGHTTP2_SESSION_SERVER: + return 'server'; + default: + return ''; + } +} + +// Top level to avoid creating a closure +function emit() { + this.emit.apply(this, arguments); +} + +// Called when a new block of headers has been received for a given +// stream. The stream may or may not be new. If the stream is new, +// create the associated Http2Stream instance and emit the 'stream' +// event. If the stream is not new, emit the 'headers' event to pass +// the block of headers on. +function onSessionHeaders(id, cat, flags, headers) { + _unrefActive(this); + const owner = this[kOwner]; + debug(`[${sessionName(owner[kType])}] headers were received on ` + + `stream ${id}: ${cat}`); + const streams = owner[kState].streams; + + const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM); + let stream = streams.get(id); + + // Convert the array of header name value pairs into an object + const obj = toHeaderObject(headers); + + if (stream === undefined) { + switch (owner[kType]) { + case NGHTTP2_SESSION_SERVER: + stream = new ServerHttp2Stream(owner, id, + { readable: !endOfStream }, + obj); + if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) { + // For head requests, there must not be a body... + // end the writable side immediately. + stream.end(); + const state = stream[kState]; + state.headRequest = true; + } + break; + case NGHTTP2_SESSION_CLIENT: + stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream }); + break; + default: + assert.fail(null, null, + 'Internal HTTP/2 Error. Invalid session type. Please ' + + 'report this as a bug in Node.js'); + } + streams.set(id, stream); + process.nextTick(emit.bind(owner, 'stream', stream, obj, flags)); + } else { + let event; + let status; + switch (cat) { + case NGHTTP2_HCAT_RESPONSE: + status = obj[HTTP2_HEADER_STATUS]; + if (!endOfStream && + status !== undefined && + status >= 100 && + status < 200) { + event = 'headers'; + } else { + event = 'response'; + } + break; + case NGHTTP2_HCAT_PUSH_RESPONSE: + event = 'push'; + break; + case NGHTTP2_HCAT_HEADERS: + status = obj[HTTP2_HEADER_STATUS]; + if (!endOfStream && status !== undefined && status >= 200) { + event = 'response'; + } else { + event = endOfStream ? 'trailers' : 'headers'; + } + break; + default: + assert.fail(null, null, + 'Internal HTTP/2 Error. Invalid headers category. Please ' + + 'report this as a bug in Node.js'); + } + debug(`[${sessionName(owner[kType])}] emitting stream '${event}' event`); + process.nextTick(emit.bind(stream, event, obj, flags)); + } +} + +// Called to determine if there are trailers to be sent at the end of a +// Stream. The 'fetchTrailers' event is emitted and passed a holder object. +// The trailers to return are set on that object by the handler. Once the +// event handler returns, those are sent off for processing. Note that this +// is a necessarily synchronous operation. We need to know immediately if +// there are trailing headers to send. +function onSessionTrailers(id) { + const owner = this[kOwner]; + debug(`[${sessionName(owner[kType])}] checking for trailers`); + const streams = owner[kState].streams; + const stream = streams.get(id); + // It should not be possible for the stream not to exist at this point. + // If it does not exist, there is something very very wrong. + assert(stream !== undefined, + 'Internal HTTP/2 Failure. Stream does not exist. Please ' + + 'report this as a bug in Node.js'); + + const trailers = Object.create(null); + stream.emit('fetchTrailers', trailers); + const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer); + if (!Array.isArray(headersList)) { + process.nextTick(() => stream.emit('error', headersList)); + return; + } + return headersList; +} + +// Called when the stream is closed. The streamClosed event is emitted on the +// Http2Stream instance. Note that this event is distinctly different than the +// require('stream') interface 'close' event which deals with the state of the +// Readable and Writable sides of the Duplex. +function onSessionStreamClose(id, code) { + const owner = this[kOwner]; + debug(`[${sessionName(owner[kType])}] session is closing the stream ` + + `${id}: ${code}`); + const stream = owner[kState].streams.get(id); + if (stream === undefined) + return; + _unrefActive(this); + // Set the rst state for the stream + abort(stream); + const state = stream[kState]; + state.rst = true; + state.rstCode = code; + + if (state.fd !== undefined) { + debug(`Closing fd ${state.fd} for stream ${id}`); + fs.close(state.fd, afterFDClose.bind(stream)); + } + + setImmediate(stream.destroy.bind(stream)); +} + +function afterFDClose(err) { + if (err) + process.nextTick(() => this.emit('error', err)); +} + +// Called when an error event needs to be triggered +function onSessionError(error) { + _unrefActive(this); + process.nextTick(() => this[kOwner].emit('error', error)); +} + +// Receives a chunk of data for a given stream and forwards it on +// to the Http2Stream Duplex for processing. +function onSessionRead(nread, buf, handle) { + const streams = this[kOwner][kState].streams; + const id = handle.id; + const stream = streams.get(id); + // It should not be possible for the stream to not exist at this point. + // If it does not, something is very very wrong + assert(stream !== undefined, + 'Internal HTTP/2 Failure. Stream does not exist. Please ' + + 'report this as a bug in Node.js'); + const state = stream[kState]; + _unrefActive(this); // Reset the session timeout timer + _unrefActive(stream); // Reset the stream timeout timer + + if (nread >= 0) { + if (!stream.push(buf)) { + assert(this.streamReadStop(id) === undefined, + `HTTP/2 Stream ${id} does not exist. Please report this as ' + + 'a bug in Node.js`); + state.reading = false; + } + } else { + // Last chunk was received. End the readable side. + stream.push(null); + } +} + +// Called when the remote peer settings have been updated. +// Resets the cached settings. +function onSettings(ack) { + const owner = this[kOwner]; + debug(`[${sessionName(owner[kType])}] new settings received`); + _unrefActive(this); + let event = 'remoteSettings'; + if (ack) { + if (owner[kState].pendingAck > 0) + owner[kState].pendingAck--; + owner[kLocalSettings] = undefined; + event = 'localSettings'; + } else { + owner[kRemoteSettings] = undefined; + } + // Only emit the event if there are listeners registered + if (owner.listenerCount(event) > 0) { + const settings = event === 'localSettings' ? + owner.localSettings : owner.remoteSettings; + process.nextTick(emit.bind(owner, event, settings)); + } +} + +// If the stream exists, an attempt will be made to emit an event +// on the stream object itself. Otherwise, forward it on to the +// session (which may, in turn, forward it on to the server) +function onPriority(id, parent, weight, exclusive) { + const owner = this[kOwner]; + debug(`[${sessionName(owner[kType])}] priority advisement for stream ` + + `${id}: \n parent: ${parent},\n weight: ${weight},\n` + + ` exclusive: ${exclusive}`); + _unrefActive(this); + const streams = owner[kState].streams; + const stream = streams.get(id); + const emitter = stream === undefined ? owner : stream; + process.nextTick( + emit.bind(emitter, 'priority', id, parent, weight, exclusive)); +} + +function emitFrameError(id, type, code) { + if (!this.emit('frameError', type, code, id)) { + const err = new errors.Error('ERR_HTTP2_FRAME_ERROR', type, code, id); + err.errno = code; + this.emit('error', err); + } +} + +// Called by the native layer when an error has occurred sending a +// frame. This should be exceedingly rare. +function onFrameError(id, type, code) { + const owner = this[kOwner]; + debug(`[${sessionName(owner[kType])}] error sending frame type ` + + `${type} on stream ${id}, code: ${code}`); + _unrefActive(this); + const streams = owner[kState].streams; + const stream = streams.get(id); + const emitter = stream !== undefined ? stream : owner; + process.nextTick(emitFrameError.bind(emitter, id, type, code)); +} + +function emitGoaway(state, code, lastStreamID, buf) { + this.emit('goaway', code, lastStreamID, buf); + // Tear down the session or destroy + if (!state.shuttingDown && !state.shutdown) { + this.shutdown({}, this.destroy.bind(this)); + } else { + this.destroy(); + } +} + +// Called by the native layer when a goaway frame has been received +function onGoawayData(code, lastStreamID, buf) { + const owner = this[kOwner]; + const state = owner[kState]; + debug(`[${sessionName(owner[kType])}] goaway data received`); + process.nextTick(emitGoaway.bind(owner, state, code, lastStreamID, buf)); +} + +// Returns the padding to use per frame. The selectPadding callback is set +// on the options. It is invoked with two arguments, the frameLen, and the +// maxPayloadLen. The method must return a numeric value within the range +// frameLen <= n <= maxPayloadLen. +function onSelectPadding(fn) { + assert(typeof fn === 'function', + 'options.selectPadding must be a function. Please report this as a ' + + 'bug in Node.js'); + return function getPadding() { + debug('fetching padding for frame'); + const frameLen = paddingBuffer[0]; + const maxFramePayloadLen = paddingBuffer[1]; + paddingBuffer[2] = Math.min(maxFramePayloadLen, + Math.max(frameLen, + fn(frameLen, + maxFramePayloadLen) | 0)); + }; +} + +// When a ClientHttp2Session is first created, the socket may not yet be +// connected. If request() is called during this time, the actual request +// will be deferred until the socket is ready to go. +function requestOnConnect(headers, options) { + const session = this[kSession]; + debug(`[${sessionName(session[kType])}] connected.. initializing request`); + const streams = session[kState].streams; + // ret will be either the reserved stream ID (if positive) + // or an error code (if negative) + validatePriorityOptions(options); + const handle = session[kHandle]; + + const headersList = mapToHeaders(headers); + if (!Array.isArray(headersList)) { + process.nextTick(() => this.emit('error', headersList)); + return; + } + + const ret = handle.submitRequest(headersList, + !!options.endStream, + options.parent | 0, + options.weight | 0, + !!options.exclusive); + + // In an error condition, one of three possible response codes will be + // possible: + // * NGHTTP2_ERR_NOMEM - Out of memory, this should be fatal to the process. + // * NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE - Maximum stream ID is reached, this + // is fatal for the session + // * NGHTTP2_ERR_INVALID_ARGUMENT - Stream was made dependent on itself, this + // impacts on this stream. + // For the first two, emit the error on the session, + // For the third, emit the error on the stream, it will bubble up to the + // session if not handled. + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => session.emit('error', err)); + break; + case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: + err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS'); + process.nextTick(() => this.emit('error', err)); + break; + case NGHTTP2_ERR_INVALID_ARGUMENT: + err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY'); + process.nextTick(() => this.emit('error', err)); + break; + default: + // Some other, unexpected error was returned. Emit on the session. + if (ret < 0) { + err = new NghttpError(ret); + process.nextTick(() => session.emit('error', err)); + break; + } + debug(`[${sessionName(session[kType])}] stream ${ret} initialized`); + this[kInit](ret); + streams.set(ret, this); + } +} + +function validatePriorityOptions(options) { + if (options.weight === undefined) + options.weight = NGHTTP2_DEFAULT_WEIGHT; + else if (typeof options.weight !== 'number') { + const err = new errors.RangeError('ERR_INVALID_OPT_VALUE', + 'weight', + options.weight); + Error.captureStackTrace(err, validatePriorityOptions); + throw err; + } + + if (options.parent === undefined) + options.parent = 0; + else if (typeof options.parent !== 'number' || options.parent < 0) { + const err = new errors.RangeError('ERR_INVALID_OPT_VALUE', + 'parent', + options.parent); + Error.captureStackTrace(err, validatePriorityOptions); + throw err; + } + + if (options.exclusive === undefined) + options.exclusive = false; + else if (typeof options.exclusive !== 'boolean') { + const err = new errors.RangeError('ERR_INVALID_OPT_VALUE', + 'exclusive', + options.exclusive); + Error.captureStackTrace(err, validatePriorityOptions); + throw err; + } + + if (options.silent === undefined) + options.silent = false; + else if (typeof options.silent !== 'boolean') { + const err = new errors.RangeError('ERR_INVALID_OPT_VALUE', + 'silent', + options.silent); + Error.captureStackTrace(err, validatePriorityOptions); + throw err; + } +} + +// Creates the internal binding.Http2Session handle for an Http2Session +// instance. This occurs only after the socket connection has been +// established. Note: the binding.Http2Session will take over ownership +// of the socket. No other code should read from or write to the socket. +function setupHandle(session, socket, type, options) { + return function() { + debug(`[${sessionName(type)}] setting up session handle`); + session[kState].connecting = false; + + updateOptionsBuffer(options); + const handle = new binding.Http2Session(type); + handle[kOwner] = session; + handle.onpriority = onPriority; + handle.onsettings = onSettings; + handle.onheaders = onSessionHeaders; + handle.ontrailers = onSessionTrailers; + handle.onstreamclose = onSessionStreamClose; + handle.onerror = onSessionError; + handle.onread = onSessionRead; + handle.onframeerror = onFrameError; + handle.ongoawaydata = onGoawayData; + + if (typeof options.selectPadding === 'function') + handle.ongetpadding = onSelectPadding(options.selectPadding); + + assert(socket._handle !== undefined, + 'Internal HTTP/2 Failure. The socket is not connected. Please ' + + 'report this as a bug in Node.js'); + handle.consume(socket._handle._externalStream); + + session[kHandle] = handle; + + const settings = typeof options.settings === 'object' ? + options.settings : Object.create(null); + + session.settings(settings); + process.nextTick(emit.bind(session, 'connect', session, socket)); + }; +} + +// Submits a SETTINGS frame to be sent to the remote peer. +function submitSettings(settings) { + debug(`[${sessionName(this[kType])}] submitting actual settings`); + _unrefActive(this); + this[kLocalSettings] = undefined; + + updateSettingsBuffer(settings); + const handle = this[kHandle]; + const ret = handle.submitSettings(); + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => this.emit('error', err)); + break; + default: + // Some other unexpected error was reported. + if (ret < 0) { + err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + } + } + debug(`[${sessionName(this[kType])}] settings complete`); +} + +// Submits a PRIORITY frame to be sent to the remote peer +// Note: If the silent option is true, the change will be made +// locally with no PRIORITY frame sent. +function submitPriority(stream, options) { + debug(`[${sessionName(this[kType])}] submitting actual priority`); + _unrefActive(this); + + const handle = this[kHandle]; + const ret = + handle.submitPriority( + stream[kID], + options.parent | 0, + options.weight | 0, + !!options.exclusive, + !!options.silent); + + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => this.emit('error', err)); + break; + default: + // Some other unexpected error was reported. + if (ret < 0) { + err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + } + } + debug(`[${sessionName(this[kType])}] priority complete`); +} + +// Submit an RST-STREAM frame to be sent to the remote peer. +// This will cause the Http2Stream to be closed. +function submitRstStream(stream, code) { + debug(`[${sessionName(this[kType])}] submit actual rststream`); + _unrefActive(this); + const id = stream[kID]; + const handle = this[kHandle]; + const ret = handle.submitRstStream(id, code); + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => this.emit('error', err)); + break; + default: + // Some other unexpected error was reported. + if (ret < 0) { + err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + break; + } + stream.destroy(); + } + debug(`[${sessionName(this[kType])}] rststream complete`); +} + +function doShutdown(options) { + const handle = this[kHandle]; + const state = this[kState]; + if (handle === undefined || state.shutdown) + return; // Nothing to do, possibly because the session shutdown already. + const ret = handle.submitGoaway(options.errorCode | 0, + options.lastStreamID | 0, + options.opaqueData); + state.shuttingDown = false; + state.shutdown = true; + if (ret < 0) { + debug(`[${sessionName(this[kType])}] shutdown failed! code: ${ret}`); + const err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + return; + } + process.nextTick(emit.bind(this, 'shutdown', options)); + debug(`[${sessionName(this[kType])}] shutdown is complete`); +} + +// Submit a graceful or immediate shutdown request for the Http2Session. +function submitShutdown(options) { + debug(`[${sessionName(this[kType])}] submitting actual shutdown request`); + const handle = this[kHandle]; + const type = this[kType]; + if (type === NGHTTP2_SESSION_SERVER && + options.graceful === true) { + // first send a shutdown notice + handle.submitShutdownNotice(); + // then, on flip of the event loop, do the actual shutdown + setImmediate(doShutdown.bind(this, options)); + } else { + doShutdown.call(this, options); + } +} + +function finishSessionDestroy(socket) { + if (!socket.destroyed) + socket.destroy(); + + // Destroy the handle + const handle = this[kHandle]; + if (handle !== undefined) { + handle.destroy(); + debug(`[${sessionName(this[kType])}] nghttp2session handle destroyed`); + } + + this.emit('close'); + debug(`[${sessionName(this[kType])}] nghttp2session destroyed`); +} + +// Upon creation, the Http2Session takes ownership of the socket. The session +// may not be ready to use immediately if the socket is not yet fully connected. +class Http2Session extends EventEmitter { + + // type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT + // options { Object } + // socket { net.Socket | tls.TLSSocket } + constructor(type, options, socket) { + super(); + + // No validation is performed on the input parameters because this + // constructor is not exported directly for users. + + // If the session property already exists on the socket, + // then it has already been bound to an Http2Session instance + // and cannot be attached again. + if (socket[kSession] !== undefined) + throw new errors.Error('ERR_HTTP2_SOCKET_BOUND'); + + socket[kSession] = this; + + this[kState] = { + streams: new Map(), + destroyed: false, + shutdown: false, + shuttingDown: false, + pendingAck: 0, + maxPendingAck: Math.max(1, (options.maxPendingAck | 0) || 10) + }; + + this[kType] = type; + this[kSocket] = socket; + + // Do not use nagle's algorithm + socket.setNoDelay(); + + // Disable TLS renegotiation on the socket + if (typeof socket.disableRenegotiation === 'function') + socket.disableRenegotiation(); + + socket[kDestroySocket] = socket.destroy; + socket.destroy = socketDestroy; + + const setupFn = setupHandle(this, socket, type, options); + if (socket.connecting) { + this[kState].connecting = true; + socket.once('connect', setupFn); + } else { + setupFn(); + } + + // Any individual session can have any number of active open + // streams, these may all need to be made aware of changes + // in state that occur -- such as when the associated socket + // is closed. To do so, we need to set the max listener count + // to something more reasonable because we may have any number + // of concurrent streams (2^31-1 is the upper limit on the number + // of streams) + this.setMaxListeners((2 ** 31) - 1); + debug(`[${sessionName(type)}] http2session created`); + } + + [kInspect](depth, opts) { + const state = this[kState]; + const obj = { + type: this[kType], + destroyed: state.destroyed, + destroying: state.destroying, + shutdown: state.shutdown, + shuttingDown: state.shuttingDown, + state: this.state, + localSettings: this.localSettings, + remoteSettings: this.remoteSettings + }; + return `Http2Session ${util.format(obj)}`; + } + + // The socket owned by this session + get socket() { + return this[kSocket]; + } + + // The session type + get type() { + return this[kType]; + } + + // true if the Http2Session is waiting for a settings acknowledgement + get pendingSettingsAck() { + return this[kState].pendingAck > 0; + } + + // true if the Http2Session has been destroyed + get destroyed() { + return this[kState].destroyed; + } + + // Retrieves state information for the Http2Session + get state() { + const handle = this[kHandle]; + return handle !== undefined ? + getSessionState(handle) : + Object.create(null); + } + + // The settings currently in effect for the local peer. These will + // be updated only when a settings acknowledgement has been received. + get localSettings() { + let settings = this[kLocalSettings]; + if (settings !== undefined) + return settings; + + const handle = this[kHandle]; + if (handle === undefined) + return Object.create(null); + + settings = getSettings(handle, false); // Local + this[kLocalSettings] = settings; + return settings; + } + + // The settings currently in effect for the remote peer. + get remoteSettings() { + let settings = this[kRemoteSettings]; + if (settings !== undefined) + return settings; + + const handle = this[kHandle]; + if (handle === undefined) + return Object.create(null); + + settings = getSettings(handle, true); // Remote + this[kRemoteSettings] = settings; + return settings; + } + + // Submits a SETTINGS frame to be sent to the remote peer. + settings(settings) { + if (this[kState].destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); + + // Validate the input first + assertIsObject(settings, 'settings'); + settings = Object.assign(Object.create(null), settings); + assertWithinRange('headerTableSize', + settings.headerTableSize, + 0, 2 ** 32 - 1); + assertWithinRange('initialWindowSize', + settings.initialWindowSize, + 0, 2 ** 32 - 1); + assertWithinRange('maxFrameSize', + settings.maxFrameSize, + 16384, 2 ** 24 - 1); + assertWithinRange('maxConcurrentStreams', + settings.maxConcurrentStreams, + 0, 2 ** 31 - 1); + assertWithinRange('maxHeaderListSize', + settings.maxHeaderListSize, + 0, 2 ** 32 - 1); + if (settings.enablePush !== undefined && + typeof settings.enablePush !== 'boolean') { + const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE', + 'enablePush', settings.enablePush); + err.actual = settings.enablePush; + throw err; + } + if (this[kState].pendingAck === this[kState].maxPendingAck) { + throw new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + this[kState].pendingAck); + } + debug(`[${sessionName(this[kType])}] sending settings`); + + this[kState].pendingAck++; + if (this[kState].connecting) { + debug(`[${sessionName(this[kType])}] session still connecting, ` + + 'queue settings'); + this.once('connect', submitSettings.bind(this, settings)); + return; + } + submitSettings.call(this, settings); + } + + // Submits a PRIORITY frame to be sent to the remote peer. + priority(stream, options) { + if (this[kState].destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); + + if (!(stream instanceof Http2Stream)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'stream', + 'Http2Stream'); + } + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + validatePriorityOptions(options); + + debug(`[${sessionName(this[kType])}] sending priority for stream ` + + `${stream[kID]}`); + + // A stream cannot be made to depend on itself + if (options.parent === stream[kID]) { + debug(`[${sessionName(this[kType])}] session still connecting. queue ` + + 'priority'); + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'parent', + options.parent); + } + + if (stream[kID] === undefined) { + stream.once('ready', submitPriority.bind(this, stream, options)); + return; + } + submitPriority.call(this, stream, options); + } + + // Submits an RST-STREAM frame to be sent to the remote peer. This will + // cause the stream to be closed. + rstStream(stream, code = NGHTTP2_NO_ERROR) { + if (this[kState].destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); + + if (!(stream instanceof Http2Stream)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'stream', + 'Http2Stream'); + } + + if (typeof code !== 'number') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'code', + 'number'); + } + + if (this[kState].rst) { + // rst has already been called, do not call again, + // skip straight to destroy + stream.destroy(); + return; + } + stream[kState].rst = true; + stream[kState].rstCode = code; + + debug(`[${sessionName(this[kType])}] initiating rststream for stream ` + + `${stream[kID]}: ${code}`); + + if (stream[kID] === undefined) { + debug(`[${sessionName(this[kType])}] session still connecting, queue ` + + 'rststream'); + stream.once('ready', submitRstStream.bind(this, stream, code)); + return; + } + submitRstStream.call(this, stream, code); + } + + // Destroy the Http2Session + destroy() { + const state = this[kState]; + if (state.destroyed || state.destroying) + return; + + debug(`[${sessionName(this[kType])}] destroying nghttp2session`); + state.destroying = true; + + // Unenroll the timer + unenroll(this); + + // Shut down any still open streams + const streams = state.streams; + streams.forEach((stream) => stream.destroy()); + + // Disassociate from the socket and server + const socket = this[kSocket]; + // socket.pause(); + delete this[kSocket]; + delete this[kServer]; + + state.destroyed = true; + state.destroying = false; + + setImmediate(finishSessionDestroy.bind(this, socket)); + } + + // Graceful or immediate shutdown of the Http2Session. Graceful shutdown + // is only supported on the server-side + shutdown(options, callback) { + if (this[kState].destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); + + if (this[kState].shutdown || this[kState].shuttingDown) + return; + + debug(`[${sessionName(this[kType])}] initiating shutdown`); + this[kState].shuttingDown = true; + + const type = this[kType]; + + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + + if (options.opaqueData !== undefined && + !Buffer.isBuffer(options.opaqueData)) { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'opaqueData', + options.opaqueData); + } + if (type === NGHTTP2_SESSION_SERVER && + options.graceful !== undefined && + typeof options.graceful !== 'boolean') { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'graceful', + options.graceful); + } + if (options.errorCode !== undefined && + typeof options.errorCode !== 'number') { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'errorCode', + options.errorCode); + } + if (options.lastStreamID !== undefined && + (typeof options.lastStreamID !== 'number' || + options.lastStreamID < 0)) { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'lastStreamID', + options.lastStreamID); + } + + if (options.opaqueData !== undefined && + !Buffer.isBuffer(options.opaqueData)) { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'opaqueData', + options.opaqueData); + } + + if (callback) { + this.on('shutdown', callback); + } + + if (this[kState].connecting) { + debug(`[${sessionName(this[kType])}] session still connecting, queue ` + + 'shutdown'); + this.once('connect', submitShutdown.bind(this, options)); + return; + } + + debug(`[${sessionName(this[kType])}] sending shutdown`); + submitShutdown.call(this, options); + } + + _onTimeout() { + this.emit('timeout'); + } +} + +class ServerHttp2Session extends Http2Session { + constructor(options, socket, server) { + super(NGHTTP2_SESSION_SERVER, options, socket); + this[kServer] = server; + } + + get server() { + return this[kServer]; + } +} + +class ClientHttp2Session extends Http2Session { + constructor(options, socket) { + super(NGHTTP2_SESSION_CLIENT, options, socket); + debug(`[${sessionName(this[kType])}] clienthttp2session created`); + } + + // Submits a new HTTP2 request to the connected peer. Returns the + // associated Http2Stream instance. + request(headers, options) { + if (this[kState].destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); + debug(`[${sessionName(this[kType])}] initiating request`); + _unrefActive(this); + assertIsObject(headers, 'headers'); + assertIsObject(options, 'options'); + + headers = Object.assign(Object.create(null), headers); + options = Object.assign(Object.create(null), options); + + if (headers[HTTP2_HEADER_METHOD] === undefined) + headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET; + + const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT; + + if (!connect) { + if (headers[HTTP2_HEADER_AUTHORITY] === undefined) + headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority]; + if (headers[HTTP2_HEADER_SCHEME] === undefined) + headers[HTTP2_HEADER_SCHEME] = this[kProtocol].slice(0, -1); + if (headers[HTTP2_HEADER_PATH] === undefined) + headers[HTTP2_HEADER_PATH] = '/'; + } else { + if (headers[HTTP2_HEADER_AUTHORITY] === undefined) + throw new errors.Error('ERR_HTTP2_CONNECT_AUTHORITY'); + if (headers[HTTP2_HEADER_SCHEME] !== undefined) + throw new errors.Error('ERR_HTTP2_CONNECT_SCHEME'); + if (headers[HTTP2_HEADER_PATH] !== undefined) + throw new errors.Error('ERR_HTTP2_CONNECT_PATH'); + } + + validatePriorityOptions(options); + + if (options.endStream === undefined) { + // For some methods, we know that a payload is meaningless, so end the + // stream by default if the user has not specifically indicated a + // preference. + options.endStream = isPayloadMeaningless(headers[HTTP2_HEADER_METHOD]); + } else if (typeof options.endStream !== 'boolean') { + throw new errors.RangeError('ERR_INVALID_OPT_VALUE', + 'endStream', + options.endStream); + } + + const stream = new ClientHttp2Stream(this, undefined, {}); + const onConnect = requestOnConnect.bind(stream, headers, options); + + // Close the writable side of the stream if options.endStream is set. + if (options.endStream) + stream.end(); + + if (this[kState].connecting) { + debug(`[${sessionName(this[kType])}] session still connecting, queue ` + + 'stream init'); + stream.on('connect', onConnect); + } else { + debug(`[${sessionName(this[kType])}] session connected, immediate ` + + 'stream init'); + onConnect(); + } + return stream; + } +} + +function createWriteReq(req, handle, data, encoding) { + switch (encoding) { + case 'latin1': + case 'binary': + return handle.writeLatin1String(req, data); + case 'buffer': + return handle.writeBuffer(req, data); + case 'utf8': + case 'utf-8': + return handle.writeUtf8String(req, data); + case 'ascii': + return handle.writeAsciiString(req, data); + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return handle.writeUcs2String(req, data); + default: + return handle.writeBuffer(req, Buffer.from(data, encoding)); + } +} + +function afterDoStreamWrite(status, handle, req) { + _unrefActive(handle[kOwner]); + if (typeof req.callback === 'function') + req.callback(); + this.handle = undefined; +} + +function onHandleFinish() { + const session = this[kSession]; + if (session === undefined) return; + if (this[kID] === undefined) { + this.once('ready', onHandleFinish.bind(this)); + } else { + const handle = session[kHandle]; + if (handle !== undefined) { + // Shutdown on the next tick of the event loop just in case there is + // still data pending in the outbound queue. + assert(handle.shutdownStream(this[kID]) === undefined, + `The stream ${this[kID]} does not exist. Please report this as ` + + 'a bug in Node.js'); + } + } +} + +function onSessionClose(hadError, code) { + abort(this); + // Close the readable side + this.push(null); + // Close the writable side + this.end(); +} + +function onStreamClosed(code) { + abort(this); + // Close the readable side + this.push(null); + // Close the writable side + this.end(); +} + +function streamOnResume() { + if (this._paused) + return this.pause(); + if (this[kID] === undefined) { + this.once('ready', streamOnResume.bind(this)); + return; + } + const session = this[kSession]; + const state = this[kState]; + if (session && !state.reading) { + state.reading = true; + assert(session[kHandle].streamReadStart(this[kID]) === undefined, + 'HTTP/2 Stream #{this[kID]} does not exist. Please report this as ' + + 'a bug in Node.js'); + } +} + +function streamOnPause() { + const session = this[kSession]; + const state = this[kState]; + if (session && state.reading) { + state.reading = false; + assert(session[kHandle].streamReadStop(this[kID]) === undefined, + `HTTP/2 Stream ${this[kID]} does not exist. Please report this as ' + + 'a bug in Node.js`); + } +} + +function streamOnDrain() { + const needPause = 0 > this._writableState.highWaterMark; + if (this._paused && !needPause) { + this._paused = false; + this.resume(); + } +} + +function streamOnSessionConnect() { + const session = this[kSession]; + debug(`[${sessionName(session[kType])}] session connected. emiting stream ` + + 'connect'); + this[kState].connecting = false; + process.nextTick(emit.bind(this, 'connect')); +} + +function streamOnceReady() { + const session = this[kSession]; + debug(`[${sessionName(session[kType])}] stream ${this[kID]} is ready`); + this.uncork(); +} + +function abort(stream) { + if (!stream[kState].aborted && + stream._writableState && + !(stream._writableState.ended || stream._writableState.ending)) { + stream.emit('aborted'); + stream[kState].aborted = true; + } +} + +// An Http2Stream is a Duplex stream. On the server-side, the Readable side +// provides access to the received request data. On the client-side, the +// Readable side provides access to the received response data. On the +// server side, the writable side is used to transmit response data, while +// on the client side it is used to transmit request data. +class Http2Stream extends Duplex { + constructor(session, options) { + options.allowHalfOpen = true; + super(options); + this.cork(); + this[kSession] = session; + + const state = this[kState] = { + rst: false, + rstCode: NGHTTP2_NO_ERROR, + headersSent: false, + aborted: false, + closeHandler: onSessionClose.bind(this) + }; + + this.once('ready', streamOnceReady); + this.once('streamClosed', onStreamClosed); + this.once('finish', onHandleFinish); + this.on('resume', streamOnResume); + this.on('pause', streamOnPause); + this.on('drain', streamOnDrain); + session.once('close', state.closeHandler); + + if (session[kState].connecting) { + debug(`[${sessionName(session[kType])}] session is still connecting, ` + + 'queuing stream init'); + state.connecting = true; + session.once('connect', streamOnSessionConnect.bind(this)); + } + debug(`[${sessionName(session[kType])}] http2stream created`); + } + + [kInit](id) { + this[kID] = id; + this.emit('ready'); + } + + [kInspect](depth, opts) { + const obj = { + id: this[kID], + state: this.state, + readableState: this._readableState, + writeableSate: this._writableState + }; + return `Http2Stream ${util.format(obj)}`; + } + + // The id of the Http2Stream, will be undefined if the socket is not + // yet connected. + get id() { + return this[kID]; + } + + // The Http2Session that owns this Http2Stream. + get session() { + return this[kSession]; + } + + _onTimeout() { + this.emit('timeout'); + } + + // true if the Http2Stream was aborted abornomally. + get aborted() { + return this[kState].aborted; + } + + // The error code reported when this Http2Stream was closed. + get rstCode() { + return this[kState].rst ? this[kState].rstCode : undefined; + } + + // State information for the Http2Stream + get state() { + const id = this[kID]; + if (this.destroyed || id === undefined) + return Object.create(null); + return getStreamState(this[kSession][kHandle], id); + } + + [kProceed]() { + assert.fail(null, null, + 'Implementors MUST implement this. Please report this as a ' + + 'bug in Node.js'); + } + + _write(data, encoding, cb) { + if (this[kID] === undefined) { + this.once('ready', this._write.bind(this, data, encoding, cb)); + return; + } + _unrefActive(this); + if (!this[kState].headersSent) + this[kProceed](); + const session = this[kSession]; + const handle = session[kHandle]; + const req = new WriteWrap(); + req.stream = this[kID]; + req.handle = handle; + req.callback = cb; + req.oncomplete = afterDoStreamWrite; + req.async = false; + const err = createWriteReq(req, handle, data, encoding); + if (err) + throw util._errnoException(err, 'write', req.error); + this._bytesDispatched += req.bytes; + + } + + _writev(data, cb) { + if (this[kID] === undefined) { + this.once('ready', this._writev.bind(this, data, cb)); + return; + } + _unrefActive(this); + if (!this[kState].headersSent) + this[kProceed](); + const session = this[kSession]; + const handle = session[kHandle]; + const req = new WriteWrap(); + req.stream = this[kID]; + req.handle = handle; + req.callback = cb; + req.oncomplete = afterDoStreamWrite; + req.async = false; + const chunks = new Array(data.length << 1); + for (var i = 0; i < data.length; i++) { + const entry = data[i]; + chunks[i * 2] = entry.chunk; + chunks[i * 2 + 1] = entry.encoding; + } + const err = handle.writev(req, chunks); + if (err) + throw util._errnoException(err, 'write', req.error); + } + + _read(nread) { + if (this[kID] === undefined) { + this.once('ready', this._read.bind(this, nread)); + return; + } + if (this.destroyed) { + this.push(null); + return; + } + _unrefActive(this); + const state = this[kState]; + if (state.reading) + return; + state.reading = true; + assert(this[kSession][kHandle].streamReadStart(this[kID]) === undefined, + 'HTTP/2 Stream #{this[kID]} does not exist. Please report this as ' + + 'a bug in Node.js'); + } + + // Submits an RST-STREAM frame to shutdown this stream. + // If the stream ID has not yet been allocated, the action will + // defer until the ready event is emitted. + // After sending the rstStream, this.destroy() will be called making + // the stream object no longer usable. + rstStream(code = NGHTTP2_NO_ERROR) { + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + const session = this[kSession]; + if (this[kID] === undefined) { + debug( + `[${sessionName(session[kType])}] queuing rstStream for new stream`); + this.once('ready', this.rstStream.bind(this, code)); + return; + } + debug(`[${sessionName(session[kType])}] sending rstStream for stream ` + + `${this[kID]}: ${code}`); + _unrefActive(this); + this[kSession].rstStream(this, code); + } + + rstWithNoError() { + this.rstStream(NGHTTP2_NO_ERROR); + } + + rstWithProtocolError() { + this.rstStream(NGHTTP2_PROTOCOL_ERROR); + } + + rstWithCancel() { + this.rstStream(NGHTTP2_CANCEL); + } + + rstWithRefuse() { + this.rstStream(NGHTTP2_REFUSED_STREAM); + } + + rstWithInternalError() { + this.rstStream(NGHTTP2_INTERNAL_ERROR); + } + + // Note that this (and other methods like additionalHeaders and rstStream) + // cause nghttp to queue frames up in its internal buffer that are not + // actually sent on the wire until the next tick of the event loop. The + // semantics of this method then are: queue a priority frame to be sent and + // not immediately send the priority frame. There is current no callback + // triggered when the data is actually sent. + priority(options) { + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + const session = this[kSession]; + if (this[kID] === undefined) { + debug(`[${sessionName(session[kType])}] queuing priority for new stream`); + this.once('ready', this.priority.bind(this, options)); + return; + } + debug(`[${sessionName(session[kType])}] sending priority for stream ` + + `${this[kID]}`); + _unrefActive(this); + this[kSession].priority(this, options); + } + + // Called by this.destroy(). + // * If called before the stream is allocated, will defer until the + // ready event is emitted. + // * Will submit an RST stream to shutdown the stream if necessary. + // This will cause the internal resources to be released. + // * Then cleans up the resources on the js side + _destroy(err, callback) { + const session = this[kSession]; + const handle = session[kHandle]; + if (this[kID] === undefined) { + debug(`[${sessionName(session[kType])}] queuing destroy for new stream`); + this.once('ready', this._destroy.bind(this, err, callback)); + return; + } + debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`); + + // Submit RST-STREAM frame if one hasn't been sent already and the + // stream hasn't closed normally... + if (!this[kState].rst) { + const code = + err instanceof Error ? + NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR; + this[kSession].rstStream(this, code); + } + + + // Remove the close handler on the session + session.removeListener('close', this[kState].closeHandler); + + // Unenroll the timer + unenroll(this); + + setImmediate(finishStreamDestroy.bind(this, handle)); + session[kState].streams.delete(this[kID]); + delete this[kSession]; + + // All done + const rst = this[kState].rst; + const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR; + if (code !== NGHTTP2_NO_ERROR) { + const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code); + process.nextTick(() => this.emit('error', err)); + } + process.nextTick(emit.bind(this, 'streamClosed', code)); + debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`); + callback(err); + } +} + +function finishStreamDestroy(handle) { + if (handle !== undefined) + handle.destroyStream(this[kID]); +} + +function processHeaders(headers) { + assertIsObject(headers, 'headers'); + headers = Object.assign(Object.create(null), headers); + if (headers[HTTP2_HEADER_STATUS] == null) + headers[HTTP2_HEADER_STATUS] = HTTP_STATUS_OK; + headers[HTTP2_HEADER_DATE] = utcDate(); + + const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; + // This is intentionally stricter than the HTTP/1 implementation, which + // allows values between 100 and 999 (inclusive) in order to allow for + // backwards compatibility with non-spec compliant code. With HTTP/2, + // we have the opportunity to start fresh with stricter spec copmliance. + // This will have an impact on the compatibility layer for anyone using + // non-standard, non-compliant status codes. + if (statusCode < 200 || statusCode > 599) + throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID', + headers[HTTP2_HEADER_STATUS]); + + return headers; +} + +function processRespondWithFD(fd, headers) { + const session = this[kSession]; + const state = this[kState]; + state.headersSent = true; + + // Close the writable side of the stream + this.end(); + + const handle = session[kHandle]; + const ret = + handle.submitFile(this[kID], fd, headers); + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => session.emit('error', err)); + break; + default: + if (ret < 0) { + err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + } + } +} + +function doSendFD(session, options, fd, headers, err, stat) { + if (this.destroyed || session.destroyed) { + abort(this); + return; + } + if (err) { + process.nextTick(() => this.emit('error', err)); + return; + } + if (!stat.isFile()) { + err = new errors.Error('ERR_HTTP2_SEND_FILE'); + process.nextTick(() => this.emit('error', err)); + return; + } + + // Set the content-length by default + headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size; + if (typeof options.statCheck === 'function' && + options.statCheck.call(this, stat, headers) === false) { + return; + } + + const headersList = mapToHeaders(headers, + assertValidPseudoHeaderResponse); + if (!Array.isArray(headersList)) { + throw headersList; + } + + processRespondWithFD.call(this, fd, headersList); +} + +function afterOpen(session, options, headers, err, fd) { + const state = this[kState]; + if (this.destroyed || session.destroyed) { + abort(this); + return; + } + if (err) { + process.nextTick(() => this.emit('error', err)); + return; + } + state.fd = fd; + + fs.fstat(fd, doSendFD.bind(this, session, options, fd, headers)); +} + + +class ServerHttp2Stream extends Http2Stream { + constructor(session, id, options, headers) { + super(session, options); + this[kInit](id); + this[kProtocol] = headers[HTTP2_HEADER_SCHEME]; + this[kAuthority] = headers[HTTP2_HEADER_AUTHORITY]; + debug(`[${sessionName(session[kType])}] created serverhttp2stream`); + } + + // true if the HEADERS frame has been sent + get headersSent() { + return this[kState].headersSent; + } + + // true if the remote peer accepts push streams + get pushAllowed() { + return this[kSession].remoteSettings.enablePush; + } + + // create a push stream, call the given callback with the created + // Http2Stream for the push stream. + pushStream(headers, options, callback) { + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + + const session = this[kSession]; + debug(`[${sessionName(session[kType])}] initiating push stream for stream` + + ` ${this[kID]}`); + + _unrefActive(this); + const state = session[kState]; + const streams = state.streams; + const handle = session[kHandle]; + + if (!this[kSession].remoteSettings.enablePush) + throw new errors.Error('ERR_HTTP2_PUSH_DISABLED'); + + if (typeof options === 'function') { + callback = options; + options = undefined; + } + + if (typeof callback !== 'function') + throw new errors.TypeError('ERR_INVALID_CALLBACK'); + + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + options.endStream = !!options.endStream; + + assertIsObject(headers, 'headers'); + headers = Object.assign(Object.create(null), headers); + + if (headers[HTTP2_HEADER_METHOD] === undefined) + headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET; + if (headers[HTTP2_HEADER_AUTHORITY] === undefined) + headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority]; + if (headers[HTTP2_HEADER_SCHEME] === undefined) + headers[HTTP2_HEADER_SCHEME] = this[kProtocol]; + if (headers[HTTP2_HEADER_PATH] === undefined) + headers[HTTP2_HEADER_PATH] = '/'; + + let headRequest = false; + if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) { + headRequest = true; + options.endStream = true; + } + + const headersList = mapToHeaders(headers); + if (!Array.isArray(headersList)) { + // An error occurred! + throw headersList; + } + + const ret = handle.submitPushPromise(this[kID], + headersList, + options.endStream); + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => session.emit('error', err)); + break; + case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: + err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS'); + process.nextTick(() => this.emit('error', err)); + break; + case NGHTTP2_ERR_STREAM_CLOSED: + err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + process.nextTick(() => this.emit('error', err)); + break; + default: + if (ret <= 0) { + err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + break; + } + debug(`[${sessionName(session[kType])}] push stream ${ret} created`); + options.readable = !options.endStream; + + const stream = new ServerHttp2Stream(session, ret, options, headers); + + // If the push stream is a head request, close the writable side of + // the stream immediately as there won't be any data sent. + if (headRequest) { + stream.end(); + const state = stream[kState]; + state.headRequest = true; + } + + streams.set(ret, stream); + process.nextTick(callback, stream, headers, 0); + } + } + + // Initiate a response on this Http2Stream + respond(headers, options) { + const session = this[kSession]; + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + debug(`[${sessionName(session[kType])}] initiating response for stream ` + + `${this[kID]}`); + _unrefActive(this); + const state = this[kState]; + + if (state.headersSent) + throw new errors.Error('ERR_HTTP2_HEADERS_SENT'); + + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + options.endStream = !!options.endStream; + + headers = processHeaders(headers); + const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; + + // Payload/DATA frames are not permitted in these cases so set + // the options.endStream option to true so that the underlying + // bits do not attempt to send any. + if (statusCode === HTTP_STATUS_NO_CONTENT || + statusCode === HTTP_STATUS_CONTENT_RESET || + statusCode === HTTP_STATUS_NOT_MODIFIED || + state.headRequest === true) { + options.endStream = true; + } + + const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse); + if (!Array.isArray(headersList)) { + // An error occurred! + throw headersList; + } + state.headersSent = true; + + // Close the writable side if the endStream option is set + if (options.endStream) + this.end(); + + const handle = session[kHandle]; + const ret = + handle.submitResponse(this[kID], headersList, options.endStream); + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => session.emit('error', err)); + break; + default: + if (ret < 0) { + err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + } + } + } + + // Initiate a response using an open FD. Note that there are fewer + // protections with this approach. For one, the fd is not validated. + // In respondWithFile, the file is checked to make sure it is a + // regular file, here the fd is passed directly. If the underlying + // mechanism is not able to read from the fd, then the stream will be + // reset with an error code. + respondWithFD(fd, headers) { + const session = this[kSession]; + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + debug(`[${sessionName(session[kType])}] initiating response for stream ` + + `${this[kID]}`); + _unrefActive(this); + const state = this[kState]; + + if (state.headersSent) + throw new errors.Error('ERR_HTTP2_HEADERS_SENT'); + + if (typeof fd !== 'number') + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'fd', 'number'); + + headers = processHeaders(headers); + const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; + // Payload/DATA frames are not permitted in these cases + if (statusCode === HTTP_STATUS_NO_CONTENT || + statusCode === HTTP_STATUS_CONTENT_RESET || + statusCode === HTTP_STATUS_NOT_MODIFIED) { + throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode); + } + + const headersList = mapToHeaders(headers, + assertValidPseudoHeaderResponse); + if (!Array.isArray(headersList)) { + throw headersList; + } + + processRespondWithFD.call(this, fd, headersList); + } + + // Initiate a file response on this Http2Stream. The path is passed to + // fs.open() to acquire the fd with mode 'r', then the fd is passed to + // fs.fstat(). Assuming fstat is successful, a check is made to ensure + // that the file is a regular file, then options.statCheck is called, + // giving the user an opportunity to verify the details and set additional + // headers. If statCheck returns false, the operation is aborted and no + // file details are sent. + respondWithFile(path, headers, options) { + const session = this[kSession]; + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + debug(`[${sessionName(session[kType])}] initiating response for stream ` + + `${this[kID]}`); + _unrefActive(this); + const state = this[kState]; + + if (state.headersSent) + throw new errors.Error('ERR_HTTP2_HEADERS_SENT'); + + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + + if (options.statCheck !== undefined && + typeof options.statCheck !== 'function') { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'statCheck', + options.statCheck); + } + + headers = processHeaders(headers); + const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; + // Payload/DATA frames are not permitted in these cases + if (statusCode === HTTP_STATUS_NO_CONTENT || + statusCode === HTTP_STATUS_CONTENT_RESET || + statusCode === HTTP_STATUS_NOT_MODIFIED) { + throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode); + } + + fs.open(path, 'r', afterOpen.bind(this, session, options, headers)); + } + + // Sends a block of informational headers. In theory, the HTTP/2 spec + // allows sending a HEADER block at any time during a streams lifecycle, + // but the HTTP request/response semantics defined in HTTP/2 places limits + // such that HEADERS may only be sent *before* or *after* DATA frames. + // If the block of headers being sent includes a status code, it MUST be + // a 1xx informational code and it MUST be sent before the request/response + // headers are sent, or an error will be thrown. + additionalHeaders(headers) { + if (this.destroyed) + throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); + + if (this[kState].headersSent) + throw new errors.Error('ERR_HTTP2_HEADERS_AFTER_RESPOND'); + + const session = this[kSession]; + debug(`[${sessionName(session[kType])}] sending additional headers`); + + assertIsObject(headers, 'headers'); + headers = Object.assign(Object.create(null), headers); + if (headers[HTTP2_HEADER_STATUS] != null) { + const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; + if (statusCode === HTTP_STATUS_SWITCHING_PROTOCOLS) + throw new errors.Error('ERR_HTTP2_STATUS_101'); + if (statusCode < 100 || statusCode >= 200) { + throw new errors.RangeError('ERR_HTTP2_INVALID_INFO_STATUS', + headers[HTTP2_HEADER_STATUS]); + } + } + + _unrefActive(this); + const handle = this[kSession][kHandle]; + + const headersList = mapToHeaders(headers, + assertValidPseudoHeaderResponse); + if (!Array.isArray(headersList)) { + throw headersList; + } + + const ret = + handle.sendHeaders(this[kID], headersList); + let err; + switch (ret) { + case NGHTTP2_ERR_NOMEM: + err = new errors.Error('ERR_OUTOFMEMORY'); + process.nextTick(() => this[kSession].emit('error', err)); + break; + default: + if (ret < 0) { + err = new NghttpError(ret); + process.nextTick(() => this.emit('error', err)); + } + } + } +} + +ServerHttp2Stream.prototype[kProceed] = ServerHttp2Stream.prototype.respond; + +class ClientHttp2Stream extends Http2Stream { + constructor(session, id, options) { + super(session, options); + this[kState].headersSent = true; + if (id !== undefined) + this[kInit](id); + debug(`[${sessionName(session[kType])}] clienthttp2stream created`); + } +} + +const setTimeout = { + configurable: true, + enumerable: true, + writable: true, + value: function(msecs, callback) { + if (typeof msecs !== 'number') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'msecs', + 'number'); + } + if (msecs === 0) { + unenroll(this); + if (callback !== undefined) { + if (typeof callback !== 'function') + throw new errors.TypeError('ERR_INVALID_CALLBACK'); + this.removeListener('timeout', callback); + } + } else { + enroll(this, msecs); + _unrefActive(this); + if (callback !== undefined) { + if (typeof callback !== 'function') + throw new errors.TypeError('ERR_INVALID_CALLBACK'); + this.once('timeout', callback); + } + } + return this; + } +}; + +const onTimeout = { + configurable: false, + enumerable: false, + value: function() { + this.emit('timeout'); + } +}; + +Object.defineProperties(Http2Stream.prototype, { + setTimeout, + onTimeout +}); +Object.defineProperties(Http2Session.prototype, { + setTimeout, + onTimeout +}); + +// -------------------------------------------------------------------- + +// Set as a replacement for socket.prototype.destroy upon the +// establishment of a new connection. +function socketDestroy(error) { + const type = this[kSession][kType]; + debug(`[${sessionName(type)}] socket destroy called`); + delete this[kServer]; + // destroy the session first so that it will stop trying to + // send data while we close the socket. + this[kSession].destroy(); + this.destroy = this[kDestroySocket]; + debug(`[${sessionName(type)}] destroying the socket`); + this.destroy(error); +} + +function socketOnResume() { + if (this._paused) + return this.pause(); + if (this._handle && !this._handle.reading) { + this._handle.reading = true; + this._handle.readStart(); + } +} + +function socketOnPause() { + if (this._handle && this._handle.reading) { + this._handle.reading = false; + this._handle.readStop(); + } +} + +function socketOnDrain() { + const needPause = 0 > this._writableState.highWaterMark; + if (this._paused && !needPause) { + this._paused = false; + this.resume(); + } +} + +// When an Http2Session emits an error, first try to forward it to the +// server as a sessionError; failing that, forward it to the socket as +// a sessionError; failing that, destroy, remove the error listener, and +// re-emit the error event +function sessionOnError(error) { + debug(`[${sessionName(this[kType])}] server session error: ${error.message}`); + if (this[kServer] !== undefined && this[kServer].emit('sessionError', error)) + return; + if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error)) + return; + this.destroy(); + this.removeListener('error', sessionOnError); + this.emit('error', error); +} + +// When a Socket emits an error, first try to forward it to the server +// as a socketError; failing that, forward it to the session as a +// socketError; failing that, remove the listener and call destroy +function socketOnError(error) { + const type = this[kSession] && this[kSession][kType]; + debug(`[${sessionName(type)}] server socket error: ${error.message}`); + if (kRenegTest.test(error.message)) + return this.destroy(); + if (this[kServer] !== undefined && this[kServer].emit('socketError', error)) + return; + if (this[kSession] !== undefined && this[kSession].emit('socketError', error)) + return; + this.removeListener('error', socketOnError); + this.destroy(error); +} + +// When the socket times out, attempt a graceful shutdown +// of the session +function socketOnTimeout() { + debug('socket timeout'); + const server = this[kServer]; + // server can be null if the socket is a client + if (server === undefined || !server.emit('timeout', this)) { + this[kSession].shutdown( + { + graceful: true, + errorCode: NGHTTP2_NO_ERROR + }, + this.destroy.bind(this)); + } +} + +// Handles the on('stream') event for a session and forwards +// it on to the server object. +function sessionOnStream(stream, headers, flags) { + debug(`[${sessionName(this[kType])}] emit server stream event`); + this[kServer].emit('stream', stream, headers, flags); +} + +function sessionOnPriority(stream, parent, weight, exclusive) { + debug(`[${sessionName(this[kType])}] priority change received`); + this[kServer].emit('priority', stream, parent, weight, exclusive); +} + +function connectionListener(socket) { + debug('[server] received a connection'); + const options = this[kOptions] || {}; + + if (this.timeout) { + socket.setTimeout(this.timeout); + socket.on('timeout', socketOnTimeout); + } + + if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') { + if (options.allowHTTP1 === true) { + // Fallback to HTTP/1.1 + return httpConnectionListener.call(this, socket); + } else if (this.emit('unknownProtocol', socket)) { + // Let event handler deal with the socket + return; + } else { + // Reject non-HTTP/2 client + return socket.destroy(); + } + } + + socket.on('error', socketOnError); + socket.on('resume', socketOnResume); + socket.on('pause', socketOnPause); + socket.on('drain', socketOnDrain); + + // Set up the Session + const session = new ServerHttp2Session(options, socket, this); + + session.on('error', sessionOnError); + session.on('stream', sessionOnStream); + session.on('priority', sessionOnPriority); + + socket[kServer] = this; + + process.nextTick(emit.bind(this, 'session', session)); +} + +function initializeOptions(options) { + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + options.allowHalfOpen = true; + assertIsObject(options.settings, 'options.settings'); + options.settings = Object.assign(Object.create(null), options.settings); + return options; +} + +function initializeTLSOptions(options, servername) { + options = initializeOptions(options); + options.ALPNProtocols = ['h2']; + if (options.allowHTTP1 === true) + options.ALPNProtocols.push('http/1.1'); + if (servername !== undefined && options.servername === undefined) + options.servername = servername; + return options; +} + +function onErrorSecureServerSession(err, conn) { + if (!this.emit('clientError', err, conn)) + conn.destroy(err); +} + +class Http2SecureServer extends TLSServer { + constructor(options, requestListener) { + options = initializeTLSOptions(options); + super(options, connectionListener); + this[kOptions] = options; + this.timeout = kDefaultSocketTimeout; + this.on('newListener', setupCompat); + if (typeof requestListener === 'function') + this.on('request', requestListener); + this.on('tlsClientError', onErrorSecureServerSession); + debug('http2secureserver created'); + } + + setTimeout(msecs, callback) { + this.timeout = msecs; + if (callback !== undefined) { + if (typeof callback !== 'function') + throw new errors.TypeError('ERR_INVALID_CALLBACK'); + this.on('timeout', callback); + } + return this; + } +} + +class Http2Server extends NETServer { + constructor(options, requestListener) { + super(connectionListener); + this[kOptions] = initializeOptions(options); + this.timeout = kDefaultSocketTimeout; + this.on('newListener', setupCompat); + if (typeof requestListener === 'function') + this.on('request', requestListener); + debug('http2server created'); + } + + setTimeout(msecs, callback) { + this.timeout = msecs; + if (callback !== undefined) { + if (typeof callback !== 'function') + throw new errors.TypeError('ERR_INVALID_CALLBACK'); + this.on('timeout', callback); + } + return this; + } +} + +function setupCompat(ev) { + if (ev === 'request') { + debug('setting up compatibility handler'); + this.removeListener('newListener', setupCompat); + this.on('stream', onServerStream); + } +} + +// If the socket emits an error, forward it to the session as a socketError; +// failing that, remove the listener and destroy the socket +function clientSocketOnError(error) { + const type = this[kSession] && this[kSession][kType]; + debug(`[${sessionName(type)}] client socket error: ${error.message}`); + if (kRenegTest.test(error.message)) + return this.destroy(); + if (this[kSession] !== undefined && this[kSession].emit('socketError', error)) + return; + this.removeListener('error', clientSocketOnError); + this.destroy(error); +} + +// If the session emits an error, forward it to the socket as a sessionError; +// failing that, destroy the session, remove the listener and re-emit the error +function clientSessionOnError(error) { + debug(`client session error: ${error.message}`); + if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error)) + return; + this.destroy(); + this.removeListener('error', clientSocketOnError); + this.removeListener('error', clientSessionOnError); +} + +function connect(authority, options, listener) { + if (typeof options === 'function') { + listener = options; + options = undefined; + } + + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + + if (typeof authority === 'string') + authority = new URL(authority); + + assertIsObject(authority, 'authority', ['string', 'object', 'URL']); + + debug(`connecting to ${authority}`); + + const protocol = authority.protocol || options.protocol || 'https:'; + const port = '' + (authority.port !== '' ? authority.port : 443); + const host = authority.hostname || authority.host || 'localhost'; + + let socket; + switch (protocol) { + case 'http:': + socket = net.connect(port, host); + break; + case 'https:': + socket = tls.connect(port, host, initializeTLSOptions(options, host)); + break; + default: + throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol); + } + + socket.on('error', clientSocketOnError); + socket.on('resume', socketOnResume); + socket.on('pause', socketOnPause); + socket.on('drain', socketOnDrain); + + const session = new ClientHttp2Session(options, socket); + + session.on('error', clientSessionOnError); + + session[kAuthority] = `${options.servername || host}:${port}`; + session[kProtocol] = protocol; + + if (typeof listener === 'function') + session.once('connect', listener); + return session; +} + +function createSecureServer(options, handler) { + if (typeof options === 'function') { + handler = options; + options = Object.create(null); + } + debug('creating http2secureserver'); + return new Http2SecureServer(options, handler); +} + +function createServer(options, handler) { + if (typeof options === 'function') { + handler = options; + options = Object.create(null); + } + debug('creating htt2pserver'); + return new Http2Server(options, handler); +} + +// Returns a Base64 encoded settings frame payload from the given +// object. The value is suitable for passing as the value of the +// HTTP2-Settings header frame. +function getPackedSettings(settings) { + assertIsObject(settings, 'settings'); + settings = settings || Object.create(null); + assertWithinRange('headerTableSize', + settings.headerTableSize, + 0, 2 ** 32 - 1); + assertWithinRange('initialWindowSize', + settings.initialWindowSize, + 0, 2 ** 32 - 1); + assertWithinRange('maxFrameSize', + settings.maxFrameSize, + 16384, 2 ** 24 - 1); + assertWithinRange('maxConcurrentStreams', + settings.maxConcurrentStreams, + 0, 2 ** 31 - 1); + assertWithinRange('maxHeaderListSize', + settings.maxHeaderListSize, + 0, 2 ** 32 - 1); + if (settings.enablePush !== undefined && + typeof settings.enablePush !== 'boolean') { + const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE', + 'enablePush', settings.enablePush); + err.actual = settings.enablePush; + throw err; + } + updateSettingsBuffer(settings); + return binding.packSettings(); +} + +function getUnpackedSettings(buf, options = {}) { + if (!isUint8Array(buf)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buf', + ['Buffer', 'Uint8Array']); + } + if (buf.length % 6 !== 0) + throw new errors.RangeError('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH'); + const settings = Object.create(null); + let offset = 0; + while (offset < buf.length) { + const id = buf.readUInt16BE(offset); + offset += 2; + const value = buf.readUInt32BE(offset); + switch (id) { + case NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: + settings.headerTableSize = value; + break; + case NGHTTP2_SETTINGS_ENABLE_PUSH: + settings.enablePush = Boolean(value); + break; + case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: + settings.maxConcurrentStreams = value; + break; + case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: + settings.initialWindowSize = value; + break; + case NGHTTP2_SETTINGS_MAX_FRAME_SIZE: + settings.maxFrameSize = value; + break; + case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: + settings.maxHeaderListSize = value; + break; + } + offset += 4; + } + + if (options != null && options.validate) { + assertWithinRange('headerTableSize', + settings.headerTableSize, + 0, 2 ** 32 - 1); + assertWithinRange('initialWindowSize', + settings.initialWindowSize, + 0, 2 ** 32 - 1); + assertWithinRange('maxFrameSize', + settings.maxFrameSize, + 16384, 2 ** 24 - 1); + assertWithinRange('maxConcurrentStreams', + settings.maxConcurrentStreams, + 0, 2 ** 31 - 1); + assertWithinRange('maxHeaderListSize', + settings.maxHeaderListSize, + 0, 2 ** 32 - 1); + if (settings.enablePush !== undefined && + typeof settings.enablePush !== 'boolean') { + const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE', + 'enablePush', settings.enablePush); + err.actual = settings.enablePush; + throw err; + } + } + + return settings; +} + +// Exports +module.exports = { + constants, + getDefaultSettings, + getPackedSettings, + getUnpackedSettings, + createServer, + createSecureServer, + connect +}; + +/* eslint-enable no-use-before-define */ diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js new file mode 100644 index 00000000000000..ea36444fadfa36 --- /dev/null +++ b/lib/internal/http2/util.js @@ -0,0 +1,513 @@ +'use strict'; + +const binding = process.binding('http2'); +const errors = require('internal/errors'); + +const { + HTTP2_HEADER_STATUS, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_PATH, + HTTP2_HEADER_AGE, + HTTP2_HEADER_AUTHORIZATION, + HTTP2_HEADER_CONTENT_ENCODING, + HTTP2_HEADER_CONTENT_LANGUAGE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_CONTENT_LOCATION, + HTTP2_HEADER_CONTENT_MD5, + HTTP2_HEADER_CONTENT_RANGE, + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_COOKIE, + HTTP2_HEADER_DATE, + HTTP2_HEADER_ETAG, + HTTP2_HEADER_EXPIRES, + HTTP2_HEADER_FROM, + HTTP2_HEADER_IF_MATCH, + HTTP2_HEADER_IF_NONE_MATCH, + HTTP2_HEADER_IF_MODIFIED_SINCE, + HTTP2_HEADER_IF_RANGE, + HTTP2_HEADER_IF_UNMODIFIED_SINCE, + HTTP2_HEADER_LAST_MODIFIED, + HTTP2_HEADER_LOCATION, + HTTP2_HEADER_MAX_FORWARDS, + HTTP2_HEADER_PROXY_AUTHORIZATION, + HTTP2_HEADER_RANGE, + HTTP2_HEADER_REFERER, + HTTP2_HEADER_RETRY_AFTER, + HTTP2_HEADER_USER_AGENT, + + HTTP2_HEADER_CONNECTION, + HTTP2_HEADER_UPGRADE, + HTTP2_HEADER_HTTP2_SETTINGS, + HTTP2_HEADER_TE, + HTTP2_HEADER_TRANSFER_ENCODING, + HTTP2_HEADER_HOST, + HTTP2_HEADER_KEEP_ALIVE, + HTTP2_HEADER_PROXY_CONNECTION, + + HTTP2_METHOD_DELETE, + HTTP2_METHOD_GET, + HTTP2_METHOD_HEAD +} = binding.constants; + +// This set is defined strictly by the HTTP/2 specification. Only +// :-prefixed headers defined by that specification may be added to +// this set. +const kValidPseudoHeaders = new Set([ + HTTP2_HEADER_STATUS, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_PATH +]); + +// This set contains headers that are permitted to have only a single +// value. Multiple instances must not be specified. +const kSingleValueHeaders = new Set([ + HTTP2_HEADER_STATUS, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_PATH, + HTTP2_HEADER_AGE, + HTTP2_HEADER_AUTHORIZATION, + HTTP2_HEADER_CONTENT_ENCODING, + HTTP2_HEADER_CONTENT_LANGUAGE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_CONTENT_LOCATION, + HTTP2_HEADER_CONTENT_MD5, + HTTP2_HEADER_CONTENT_RANGE, + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_DATE, + HTTP2_HEADER_ETAG, + HTTP2_HEADER_EXPIRES, + HTTP2_HEADER_FROM, + HTTP2_HEADER_IF_MATCH, + HTTP2_HEADER_IF_MODIFIED_SINCE, + HTTP2_HEADER_IF_NONE_MATCH, + HTTP2_HEADER_IF_RANGE, + HTTP2_HEADER_IF_UNMODIFIED_SINCE, + HTTP2_HEADER_LAST_MODIFIED, + HTTP2_HEADER_LOCATION, + HTTP2_HEADER_MAX_FORWARDS, + HTTP2_HEADER_PROXY_AUTHORIZATION, + HTTP2_HEADER_RANGE, + HTTP2_HEADER_REFERER, + HTTP2_HEADER_RETRY_AFTER, + HTTP2_HEADER_USER_AGENT +]); + +// The HTTP methods in this set are specifically defined as assigning no +// meaning to the request payload. By default, unless the user explicitly +// overrides the endStream option on the request method, the endStream +// option will be defaulted to true when these methods are used. +const kNoPayloadMethods = new Set([ + HTTP2_METHOD_DELETE, + HTTP2_METHOD_GET, + HTTP2_METHOD_HEAD +]); + +// The following ArrayBuffer instances are used to share memory more efficiently +// with the native binding side for a number of methods. These are not intended +// to be used directly by users in any way. The ArrayBuffers are created on +// the native side with values that are filled in on demand, the js code then +// reads those values out. The set of IDX constants that follow identify the +// relevant data positions within these buffers. +const settingsBuffer = new Uint32Array(binding.settingsArrayBuffer); +const optionsBuffer = new Uint32Array(binding.optionsArrayBuffer); + +// Note that Float64Array is used here because there is no Int64Array available +// and these deal with numbers that can be beyond the range of Uint32 and Int32. +// The values set on the native side will always be integers. This is not a +// unique example of this, this pattern can be found in use in other parts of +// Node.js core as a performance optimization. +const sessionState = new Float64Array(binding.sessionStateArrayBuffer); +const streamState = new Float64Array(binding.streamStateArrayBuffer); + +const IDX_SETTINGS_HEADER_TABLE_SIZE = 0; +const IDX_SETTINGS_ENABLE_PUSH = 1; +const IDX_SETTINGS_INITIAL_WINDOW_SIZE = 2; +const IDX_SETTINGS_MAX_FRAME_SIZE = 3; +const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4; +const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5; +const IDX_SETTINGS_FLAGS = 6; + +const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0; +const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1; +const IDX_SESSION_STATE_NEXT_STREAM_ID = 2; +const IDX_SESSION_STATE_LOCAL_WINDOW_SIZE = 3; +const IDX_SESSION_STATE_LAST_PROC_STREAM_ID = 4; +const IDX_SESSION_STATE_REMOTE_WINDOW_SIZE = 5; +const IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE = 6; +const IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE = 7; +const IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE = 8; +const IDX_STREAM_STATE = 0; +const IDX_STREAM_STATE_WEIGHT = 1; +const IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT = 2; +const IDX_STREAM_STATE_LOCAL_CLOSE = 3; +const IDX_STREAM_STATE_REMOTE_CLOSE = 4; +const IDX_STREAM_STATE_LOCAL_WINDOW_SIZE = 5; + +const IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE = 0; +const IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS = 1; +const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2; +const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3; +const IDX_OPTIONS_PADDING_STRATEGY = 4; +const IDX_OPTIONS_FLAGS = 5; + +function updateOptionsBuffer(options) { + var flags = 0; + if (typeof options.maxDeflateDynamicTableSize === 'number') { + flags |= (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE); + optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE] = + options.maxDeflateDynamicTableSize; + } + if (typeof options.maxReservedRemoteStreams === 'number') { + flags |= (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS); + optionsBuffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS] = + options.maxReservedRemoteStreams; + } + if (typeof options.maxSendHeaderBlockLength === 'number') { + flags |= (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH); + optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] = + options.maxSendHeaderBlockLength; + } + if (typeof options.peerMaxConcurrentStreams === 'number') { + flags |= (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS); + optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS] = + options.peerMaxConcurrentStreams; + } + if (typeof options.paddingStrategy === 'number') { + flags |= (1 << IDX_OPTIONS_PADDING_STRATEGY); + optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY] = + options.paddingStrategy; + } + optionsBuffer[IDX_OPTIONS_FLAGS] = flags; +} + +function getDefaultSettings() { + settingsBuffer[IDX_SETTINGS_FLAGS] = 0; + binding.refreshDefaultSettings(); + const holder = Object.create(null); + + const flags = settingsBuffer[IDX_SETTINGS_FLAGS]; + + if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) === + (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { + holder.headerTableSize = + settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE]; + } + + if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) === + (1 << IDX_SETTINGS_ENABLE_PUSH)) { + holder.enablePush = + settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] === 1; + } + + if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) === + (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { + holder.initialWindowSize = + settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]; + } + + if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) === + (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { + holder.maxFrameSize = + settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE]; + } + + if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) === + (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { + console.log('setting it'); + holder.maxConcurrentStreams = + settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]; + } + + if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) === + (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { + holder.maxHeaderListSize = + settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; + } + + return holder; +} + +// remote is a boolean. true to fetch remote settings, false to fetch local. +// this is only called internally +function getSettings(session, remote) { + const holder = Object.create(null); + if (remote) + binding.refreshRemoteSettings(session); + else + binding.refreshLocalSettings(session); + + holder.headerTableSize = + settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE]; + holder.enablePush = + !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH]; + holder.initialWindowSize = + settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]; + holder.maxFrameSize = + settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE]; + holder.maxConcurrentStreams = + settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]; + holder.maxHeaderListSize = + settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; + return holder; +} + +function updateSettingsBuffer(settings) { + var flags = 0; + if (typeof settings.headerTableSize === 'number') { + flags |= (1 << IDX_SETTINGS_HEADER_TABLE_SIZE); + settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = + settings.headerTableSize; + } + if (typeof settings.maxConcurrentStreams === 'number') { + flags |= (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS); + settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = + settings.maxConcurrentStreams; + } + if (typeof settings.initialWindowSize === 'number') { + flags |= (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE); + settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = + settings.initialWindowSize; + } + if (typeof settings.maxFrameSize === 'number') { + flags |= (1 << IDX_SETTINGS_MAX_FRAME_SIZE); + settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] = + settings.maxFrameSize; + } + if (typeof settings.maxHeaderListSize === 'number') { + flags |= (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE); + settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = + settings.maxHeaderListSize; + } + if (typeof settings.enablePush === 'boolean') { + flags |= (1 << IDX_SETTINGS_ENABLE_PUSH); + settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush); + } + + settingsBuffer[IDX_SETTINGS_FLAGS] = flags; +} + +function getSessionState(session) { + const holder = Object.create(null); + binding.refreshSessionState(session); + holder.effectiveLocalWindowSize = + sessionState[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE]; + holder.effectiveRecvDataLength = + sessionState[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH]; + holder.nextStreamID = + sessionState[IDX_SESSION_STATE_NEXT_STREAM_ID]; + holder.localWindowSize = + sessionState[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE]; + holder.lastProcStreamID = + sessionState[IDX_SESSION_STATE_LAST_PROC_STREAM_ID]; + holder.remoteWindowSize = + sessionState[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE]; + holder.outboundQueueSize = + sessionState[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE]; + holder.deflateDynamicTableSize = + sessionState[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE]; + holder.inflateDynamicTableSize = + sessionState[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE]; + return holder; +} + +function getStreamState(session, stream) { + const holder = Object.create(null); + binding.refreshStreamState(session, stream); + holder.state = + streamState[IDX_STREAM_STATE]; + holder.weight = + streamState[IDX_STREAM_STATE_WEIGHT]; + holder.sumDependencyWeight = + streamState[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT]; + holder.localClose = + streamState[IDX_STREAM_STATE_LOCAL_CLOSE]; + holder.remoteClose = + streamState[IDX_STREAM_STATE_REMOTE_CLOSE]; + holder.localWindowSize = + streamState[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE]; + return holder; +} + +function isIllegalConnectionSpecificHeader(name, value) { + switch (name) { + case HTTP2_HEADER_CONNECTION: + case HTTP2_HEADER_UPGRADE: + case HTTP2_HEADER_HOST: + case HTTP2_HEADER_HTTP2_SETTINGS: + case HTTP2_HEADER_KEEP_ALIVE: + case HTTP2_HEADER_PROXY_CONNECTION: + case HTTP2_HEADER_TRANSFER_ENCODING: + return true; + case HTTP2_HEADER_TE: + const val = Array.isArray(value) ? value.join(', ') : value; + return val !== 'trailers'; + default: + return false; + } +} + +function assertValidPseudoHeader(key) { + if (!kValidPseudoHeaders.has(key)) { + const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key); + Error.captureStackTrace(err, assertValidPseudoHeader); + return err; + } +} + +function assertValidPseudoHeaderResponse(key) { + if (key !== ':status') { + const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key); + Error.captureStackTrace(err, assertValidPseudoHeaderResponse); + return err; + } +} + +function assertValidPseudoHeaderTrailer(key) { + const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key); + Error.captureStackTrace(err, assertValidPseudoHeaderTrailer); + return err; +} + +function mapToHeaders(map, + assertValuePseudoHeader = assertValidPseudoHeader) { + const ret = []; + const keys = Object.keys(map); + const singles = new Set(); + for (var i = 0; i < keys.length; i++) { + let key = keys[i]; + let value = map[key]; + let val; + if (typeof key === 'symbol' || value === undefined || !key) + continue; + key = String(key).toLowerCase(); + const isArray = Array.isArray(value); + if (isArray) { + switch (value.length) { + case 0: + continue; + case 1: + value = String(value[0]); + break; + default: + if (kSingleValueHeaders.has(key)) + return new errors.Error('ERR_HTTP2_HEADER_SINGLE_VALUE', key); + } + } + if (key[0] === ':') { + const err = assertValuePseudoHeader(key); + if (err !== undefined) + return err; + ret.unshift([key, String(value)]); + } else { + if (kSingleValueHeaders.has(key)) { + if (singles.has(key)) + return new errors.Error('ERR_HTTP2_HEADER_SINGLE_VALUE', key); + singles.add(key); + } + if (isIllegalConnectionSpecificHeader(key, value)) { + return new errors.Error('ERR_HTTP2_INVALID_CONNECTION_HEADERS'); + } + if (isArray) { + for (var k = 0; k < value.length; k++) { + val = String(value[k]); + ret.push([key, val]); + } + } else { + val = String(value); + ret.push([key, val]); + } + } + } + + return ret; +} + +class NghttpError extends Error { + constructor(ret) { + super(binding.nghttp2ErrorString(ret)); + this.code = 'ERR_HTTP2_ERROR'; + this.name = 'Error [ERR_HTTP2_ERROR]'; + this.errno = ret; + } +} + +function assertIsObject(value, name, types) { + if (value !== undefined && + (value === null || + typeof value !== 'object' || + Array.isArray(value))) { + const err = new errors.TypeError('ERR_INVALID_ARG_TYPE', + name, types || 'object'); + Error.captureStackTrace(err, assertIsObject); + throw err; + } +} + +function assertWithinRange(name, value, min = 0, max = Infinity) { + if (value !== undefined && + (typeof value !== 'number' || value < min || value > max)) { + const err = new errors.RangeError('ERR_HTTP2_INVALID_SETTING_VALUE', + name, value); + err.min = min; + err.max = max; + err.actual = value; + Error.captureStackTrace(err, assertWithinRange); + throw err; + } +} + +function toHeaderObject(headers) { + const obj = Object.create(null); + for (var n = 0; n < headers.length; n = n + 2) { + var name = headers[n]; + var value = headers[n + 1]; + if (name === HTTP2_HEADER_STATUS) + value |= 0; + var existing = obj[name]; + if (existing === undefined) { + obj[name] = value; + } else if (!kSingleValueHeaders.has(name)) { + if (name === HTTP2_HEADER_COOKIE) { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 + // "...If there are multiple Cookie header fields after decompression, + // these MUST be concatenated into a single octet string using the + // two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ") before + // being passed into a non-HTTP/2 context." + obj[name] = `${existing}; ${value}`; + } else { + if (Array.isArray(existing)) + existing.push(value); + else + obj[name] = [existing, value]; + } + } + } + return obj; +} + +function isPayloadMeaningless(method) { + return kNoPayloadMethods.has(method); +} + +module.exports = { + assertIsObject, + assertValidPseudoHeaderResponse, + assertValidPseudoHeaderTrailer, + assertWithinRange, + getDefaultSettings, + getSessionState, + getSettings, + getStreamState, + isPayloadMeaningless, + mapToHeaders, + NghttpError, + toHeaderObject, + updateOptionsBuffer, + updateSettingsBuffer +}; diff --git a/lib/internal/module.js b/lib/internal/module.js index 08d8f770c8d873..cf994b51c0675f 100644 --- a/lib/internal/module.js +++ b/lib/internal/module.js @@ -78,11 +78,15 @@ function stripShebang(content) { const builtinLibs = [ 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'crypto', - 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net', 'os', - 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream', + 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net', + 'os', 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib' ]; +const { exposeHTTP2 } = process.binding('config'); +if (exposeHTTP2) + builtinLibs.push('http2'); + function addBuiltinLibsToObject(object) { // Make built-in modules available directly (loaded lazily). builtinLibs.forEach((name) => { diff --git a/node.gyp b/node.gyp index 1650f1598bf02a..81f549f8b63f73 100644 --- a/node.gyp +++ b/node.gyp @@ -37,6 +37,7 @@ 'lib/events.js', 'lib/fs.js', 'lib/http.js', + 'lib/http2.js', 'lib/_http_agent.js', 'lib/_http_client.js', 'lib/_http_common.js', @@ -103,6 +104,9 @@ 'lib/internal/test/unicode.js', 'lib/internal/url.js', 'lib/internal/util.js', + 'lib/internal/http2/core.js', + 'lib/internal/http2/compat.js', + 'lib/internal/http2/util.js', 'lib/internal/v8_prof_polyfill.js', 'lib/internal/v8_prof_processor.js', 'lib/internal/streams/lazy_transform.js', @@ -146,6 +150,7 @@ 'dependencies': [ 'node_js2c#host', + 'deps/nghttp2/nghttp2.gyp:nghttp2' ], 'includes': [ @@ -156,7 +161,8 @@ 'src', 'tools/msvs/genfiles', 'deps/uv/src/ares', - '<(SHARED_INTERMEDIATE_DIR)', + '<(SHARED_INTERMEDIATE_DIR)', # for node_natives.h + 'deps/nghttp2/lib/includes' ], 'sources': [ @@ -178,6 +184,8 @@ 'src/node_contextify.cc', 'src/node_debug_options.cc', 'src/node_file.cc', + 'src/node_http2_core.cc', + 'src/node_http2.cc', 'src/node_http_parser.cc', 'src/node_main.cc', 'src/node_os.cc', @@ -220,9 +228,12 @@ 'src/handle_wrap.h', 'src/js_stream.h', 'src/node.h', + 'src/node_http2_core.h', + 'src/node_http2_core-inl.h', 'src/node_buffer.h', 'src/node_constants.h', 'src/node_debug_options.h', + 'src/node_http2.h', 'src/node_internals.h', 'src/node_javascript.h', 'src/node_mutex.h', @@ -265,6 +276,8 @@ 'NODE_WANT_INTERNALS=1', # Warn when using deprecated V8 APIs. 'V8_DEPRECATION_WARNINGS=1', + # We're using the nghttp2 static lib + 'NGHTTP2_STATICLIB' ], }, { diff --git a/node.gypi b/node.gypi index a926d9a8e7ff26..e9905ab4436dd8 100644 --- a/node.gypi +++ b/node.gypi @@ -52,6 +52,10 @@ 'NODE_RELEASE_URLBASE="<(node_release_urlbase)"', ] }], + [ + 'debug_http2==1', { + 'defines': [ 'NODE_DEBUG_HTTP2=1' ] + }], [ 'v8_enable_i18n_support==1', { 'defines': [ 'NODE_HAVE_I18N_SUPPORT=1' ], 'dependencies': [ diff --git a/src/async-wrap.h b/src/async-wrap.h index a123ae06e8e936..ffdf8358747f12 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -41,6 +41,8 @@ namespace node { V(FSREQWRAP) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ + V(HTTP2SESSION) \ + V(HTTP2SESSIONSHUTDOWNWRAP) \ V(HTTPPARSER) \ V(JSSTREAM) \ V(PIPECONNECTWRAP) \ diff --git a/src/env-inl.h b/src/env-inl.h index f7d9ff626f598a..cbbfceea3f85bc 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -303,6 +303,7 @@ inline Environment::Environment(IsolateData* isolate_data, #endif handle_cleanup_waiting_(0), http_parser_buffer_(nullptr), + http2_socket_buffer_(nullptr), fs_stats_field_array_(nullptr), context_(context->GetIsolate(), context) { // We'll be creating new objects so make sure we've entered the context. @@ -329,6 +330,12 @@ inline Environment::~Environment() { delete[] heap_statistics_buffer_; delete[] heap_space_statistics_buffer_; delete[] http_parser_buffer_; + delete[] http2_socket_buffer_; + delete[] http2_settings_buffer_; + delete[] http2_options_buffer_; + delete[] http2_session_state_buffer_; + delete[] http2_stream_state_buffer_; + delete[] http2_padding_buffer_; } inline v8::Isolate* Environment::isolate() const { @@ -469,6 +476,55 @@ inline void Environment::set_heap_space_statistics_buffer(double* pointer) { heap_space_statistics_buffer_ = pointer; } +inline uint32_t* Environment::http2_settings_buffer() const { + CHECK_NE(http2_settings_buffer_, nullptr); + return http2_settings_buffer_; +} + +inline void Environment::set_http2_settings_buffer(uint32_t* pointer) { + CHECK_EQ(http2_settings_buffer_, nullptr); // Should be set only once + http2_settings_buffer_ = pointer; +} + +inline uint32_t* Environment::http2_options_buffer() const { + CHECK_NE(http2_options_buffer_, nullptr); + return http2_options_buffer_; +} + +inline void Environment::set_http2_options_buffer(uint32_t* pointer) { + CHECK_EQ(http2_options_buffer_, nullptr); // Should be set only once + http2_options_buffer_ = pointer; +} + +inline double* Environment::http2_session_state_buffer() const { + CHECK_NE(http2_session_state_buffer_, nullptr); + return http2_session_state_buffer_; +} + +inline void Environment::set_http2_session_state_buffer(double* pointer) { + CHECK_EQ(http2_session_state_buffer_, nullptr); + http2_session_state_buffer_ = pointer; +} + +inline double* Environment::http2_stream_state_buffer() const { + CHECK_NE(http2_stream_state_buffer_, nullptr); + return http2_stream_state_buffer_; +} + +inline void Environment::set_http2_stream_state_buffer(double* pointer) { + CHECK_EQ(http2_stream_state_buffer_, nullptr); + http2_stream_state_buffer_ = pointer; +} + +inline uint32_t* Environment::http2_padding_buffer() const { + CHECK_NE(http2_padding_buffer_, nullptr); + return http2_padding_buffer_; +} + +inline void Environment::set_http2_padding_buffer(uint32_t* pointer) { + CHECK_EQ(http2_padding_buffer_, nullptr); + http2_padding_buffer_ = pointer; +} inline char* Environment::http_parser_buffer() const { return http_parser_buffer_; @@ -488,6 +544,15 @@ inline void Environment::set_fs_stats_field_array(double* fields) { fs_stats_field_array_ = fields; } +inline char* Environment::http2_socket_buffer() const { + return http2_socket_buffer_; +} + +inline void Environment::set_http2_socket_buffer(char* buffer) { + CHECK_EQ(http2_socket_buffer_, nullptr); // Should be set only once. + http2_socket_buffer_ = buffer; +} + inline IsolateData* Environment::isolate_data() const { return isolate_data_; } diff --git a/src/env.h b/src/env.h index ae8deb5e04f960..3e601b0118d338 100644 --- a/src/env.h +++ b/src/env.h @@ -104,6 +104,7 @@ namespace node { V(configurable_string, "configurable") \ V(cwd_string, "cwd") \ V(dest_string, "dest") \ + V(destroy_string, "destroy") \ V(detached_string, "detached") \ V(disposed_string, "_disposed") \ V(dns_a_string, "A") \ @@ -117,11 +118,13 @@ namespace node { V(dns_srv_string, "SRV") \ V(dns_txt_string, "TXT") \ V(domain_string, "domain") \ + V(emit_string, "emit") \ V(emitting_top_level_domain_error_string, "_emittingTopLevelDomainError") \ V(exchange_string, "exchange") \ V(enumerable_string, "enumerable") \ V(idle_string, "idle") \ V(irq_string, "irq") \ + V(enablepush_string, "enablePush") \ V(encoding_string, "encoding") \ V(enter_string, "enter") \ V(entries_string, "entries") \ @@ -148,8 +151,11 @@ namespace node { V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \ V(gid_string, "gid") \ V(handle_string, "handle") \ + V(heap_total_string, "heapTotal") \ + V(heap_used_string, "heapUsed") \ V(homedir_string, "homedir") \ V(hostmaster_string, "hostmaster") \ + V(id_string, "id") \ V(ignore_string, "ignore") \ V(immediate_callback_string, "_immediateCallback") \ V(infoaccess_string, "infoAccess") \ @@ -174,6 +180,7 @@ namespace node { V(netmask_string, "netmask") \ V(nice_string, "nice") \ V(nsname_string, "nsname") \ + V(nexttick_string, "nextTick") \ V(ocsp_request_string, "OCSPRequest") \ V(onchange_string, "onchange") \ V(onclienthello_string, "onclienthello") \ @@ -182,19 +189,27 @@ namespace node { V(ondone_string, "ondone") \ V(onerror_string, "onerror") \ V(onexit_string, "onexit") \ + V(onframeerror_string, "onframeerror") \ + V(ongetpadding_string, "ongetpadding") \ V(onhandshakedone_string, "onhandshakedone") \ V(onhandshakestart_string, "onhandshakestart") \ + V(onheaders_string, "onheaders") \ V(onmessage_string, "onmessage") \ V(onnewsession_string, "onnewsession") \ V(onnewsessiondone_string, "onnewsessiondone") \ V(onocspresponse_string, "onocspresponse") \ + V(ongoawaydata_string, "ongoawaydata") \ + V(onpriority_string, "onpriority") \ V(onread_string, "onread") \ V(onreadstart_string, "onreadstart") \ V(onreadstop_string, "onreadstop") \ V(onselect_string, "onselect") \ + V(onsettings_string, "onsettings") \ V(onshutdown_string, "onshutdown") \ V(onsignal_string, "onsignal") \ V(onstop_string, "onstop") \ + V(onstreamclose_string, "onstreamclose") \ + V(ontrailers_string, "ontrailers") \ V(onwrite_string, "onwrite") \ V(output_string, "output") \ V(order_string, "order") \ @@ -234,6 +249,7 @@ namespace node { V(stack_string, "stack") \ V(status_string, "status") \ V(stdio_string, "stdio") \ + V(stream_string, "stream") \ V(subject_string, "subject") \ V(subjectaltname_string, "subjectaltname") \ V(sys_string, "sys") \ @@ -262,7 +278,7 @@ namespace node { V(write_host_object_string, "_writeHostObject") \ V(write_queue_size_string, "writeQueueSize") \ V(x_forwarded_string, "x-forwarded-for") \ - V(zero_return_string, "ZERO_RETURN") \ + V(zero_return_string, "ZERO_RETURN") #define ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) \ V(as_external, v8::External) \ @@ -580,8 +596,25 @@ class Environment { inline double* heap_space_statistics_buffer() const; inline void set_heap_space_statistics_buffer(double* pointer); + inline uint32_t* http2_settings_buffer() const; + inline void set_http2_settings_buffer(uint32_t* pointer); + + inline uint32_t* http2_options_buffer() const; + inline void set_http2_options_buffer(uint32_t* pointer); + + inline double* http2_session_state_buffer() const; + inline void set_http2_session_state_buffer(double* pointer); + + inline double* http2_stream_state_buffer() const; + inline void set_http2_stream_state_buffer(double* pointer); + + inline uint32_t* http2_padding_buffer() const; + inline void set_http2_padding_buffer(uint32_t* pointer); + inline char* http_parser_buffer() const; inline void set_http_parser_buffer(char* buffer); + inline char* http2_socket_buffer() const; + inline void set_http2_socket_buffer(char* buffer); inline double* fs_stats_field_array() const; inline void set_fs_stats_field_array(double* fields); @@ -687,8 +720,14 @@ class Environment { double* heap_statistics_buffer_ = nullptr; double* heap_space_statistics_buffer_ = nullptr; + uint32_t* http2_settings_buffer_ = nullptr; + uint32_t* http2_options_buffer_ = nullptr; + double* http2_session_state_buffer_ = nullptr; + double* http2_stream_state_buffer_ = nullptr; + uint32_t* http2_padding_buffer_ = nullptr; char* http_parser_buffer_; + char* http2_socket_buffer_; double* fs_stats_field_array_; diff --git a/src/freelist.h b/src/freelist.h new file mode 100644 index 00000000000000..7dff56a35d348a --- /dev/null +++ b/src/freelist.h @@ -0,0 +1,92 @@ +#ifndef SRC_FREELIST_H_ +#define SRC_FREELIST_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "util.h" + +namespace node { + +struct DefaultFreelistTraits; + +template +class Freelist { + public: + typedef struct list_item { + T* item = nullptr; + list_item* next = nullptr; + } list_item; + + Freelist() {} + ~Freelist() { + while (head_ != nullptr) { + list_item* item = head_; + head_ = item->next; + FreelistTraits::Free(item->item); + free(item); + } + } + + void push(T* item) { + if (size_ > kMaximumLength) { + FreelistTraits::Free(item); + } else { + size_++; + FreelistTraits::Reset(item); + list_item* li = Calloc(1); + li->item = item; + if (head_ == nullptr) { + head_ = li; + tail_ = li; + } else { + tail_->next = li; + tail_ = li; + } + } + } + + T* pop() { + if (head_ != nullptr) { + size_--; + list_item* cur = head_; + T* item = cur->item; + head_ = cur->next; + free(cur); + return item; + } else { + return FreelistTraits::template Alloc(); + } + } + + private: + size_t size_ = 0; + list_item* head_ = nullptr; + list_item* tail_ = nullptr; +}; + +struct DefaultFreelistTraits { + template + static T* Alloc() { + return ::new (Malloc(1)) T(); + } + + template + static void Free(T* item) { + item->~T(); + free(item); + } + + template + static void Reset(T* item) { + item->~T(); + ::new (item) T(); + } +}; + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_FREELIST_H_ diff --git a/src/node.cc b/src/node.cc index 551f9fbf396949..775accc0412606 100644 --- a/src/node.cc +++ b/src/node.cc @@ -59,6 +59,7 @@ #include "env-inl.h" #include "handle_wrap.h" #include "http_parser.h" +#include "nghttp2/nghttp2ver.h" #include "req-wrap.h" #include "req-wrap-inl.h" #include "string_bytes.h" @@ -232,6 +233,9 @@ std::string config_warning_file; // NOLINT(runtime/string) // that is used by lib/internal/bootstrap_node.js bool config_expose_internals = false; +// Set in node.cc by ParseArgs when --expose-http2 is used. +bool config_expose_http2 = false; + bool v8_initialized = false; bool linux_at_secure = false; @@ -3210,6 +3214,10 @@ void SetupProcessObject(Environment* env, "modules", FIXED_ONE_BYTE_STRING(env->isolate(), node_modules_version)); + READONLY_PROPERTY(versions, + "nghttp2", + FIXED_ONE_BYTE_STRING(env->isolate(), NGHTTP2_VERSION)); + // process._promiseRejectEvent Local promiseRejectEvent = Object::New(env->isolate()); READONLY_DONT_ENUM_PROPERTY(process, @@ -3649,6 +3657,7 @@ static void PrintHelp() { " --abort-on-uncaught-exception\n" " aborting instead of exiting causes a\n" " core file to be generated for analysis\n" + " --expose-http2 enable experimental HTTP2 support\n" " --trace-warnings show stack traces on process warnings\n" " --redirect-warnings=file\n" " write warnings to file instead of\n" @@ -3770,6 +3779,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env, "--throw-deprecation", "--no-warnings", "--napi-modules", + "--expose-http2", "--trace-warnings", "--redirect-warnings", "--trace-sync-io", @@ -3967,6 +3977,9 @@ static void ParseArgs(int* argc, } else if (strcmp(arg, "--expose-internals") == 0 || strcmp(arg, "--expose_internals") == 0) { config_expose_internals = true; + } else if (strcmp(arg, "--expose-http2") == 0 || + strcmp(arg, "--expose_http2") == 0) { + config_expose_http2 = true; } else if (strcmp(arg, "-") == 0) { break; } else if (strcmp(arg, "--") == 0) { diff --git a/src/node.h b/src/node.h index 596769a6b97734..a3c29c22423b02 100644 --- a/src/node.h +++ b/src/node.h @@ -253,6 +253,25 @@ NODE_EXTERN void RunAtExit(Environment* env); } \ while (0) +#define NODE_DEFINE_HIDDEN_CONSTANT(target, constant) \ + do { \ + v8::Isolate* isolate = target->GetIsolate(); \ + v8::Local context = isolate->GetCurrentContext(); \ + v8::Local constant_name = \ + v8::String::NewFromUtf8(isolate, #constant); \ + v8::Local constant_value = \ + v8::Number::New(isolate, static_cast(constant)); \ + v8::PropertyAttribute constant_attributes = \ + static_cast(v8::ReadOnly | \ + v8::DontDelete | \ + v8::DontEnum); \ + (target)->DefineOwnProperty(context, \ + constant_name, \ + constant_value, \ + constant_attributes).FromJust(); \ + } \ + while (0) + // Used to be a macro, hence the uppercase name. inline void NODE_SET_METHOD(v8::Local recv, const char* name, diff --git a/src/node_config.cc b/src/node_config.cc index b309171282182a..041e18f6b76ff9 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -88,6 +88,9 @@ static void InitConfig(Local target, if (config_expose_internals) READONLY_BOOLEAN_PROPERTY("exposeInternals"); + + if (config_expose_http2) + READONLY_BOOLEAN_PROPERTY("exposeHTTP2"); } // InitConfig } // namespace node diff --git a/src/node_crypto_bio.cc b/src/node_crypto_bio.cc index 00fd0b420c38c5..4c84973f75facc 100644 --- a/src/node_crypto_bio.cc +++ b/src/node_crypto_bio.cc @@ -357,7 +357,6 @@ size_t NodeBIO::IndexOf(char delim, size_t limit) { return max; } - void NodeBIO::Write(const char* data, size_t size) { size_t offset = 0; size_t left = size; diff --git a/src/node_http2.cc b/src/node_http2.cc new file mode 100644 index 00000000000000..5ad1352cc108dd --- /dev/null +++ b/src/node_http2.cc @@ -0,0 +1,1326 @@ +#include "node.h" +#include "node_buffer.h" +#include "node_http2.h" + +namespace node { + +using v8::ArrayBuffer; +using v8::Boolean; +using v8::Context; +using v8::Function; +using v8::Integer; +using v8::Undefined; + +namespace http2 { + +enum Http2SettingsIndex { + IDX_SETTINGS_HEADER_TABLE_SIZE, + IDX_SETTINGS_ENABLE_PUSH, + IDX_SETTINGS_INITIAL_WINDOW_SIZE, + IDX_SETTINGS_MAX_FRAME_SIZE, + IDX_SETTINGS_MAX_CONCURRENT_STREAMS, + IDX_SETTINGS_MAX_HEADER_LIST_SIZE, + IDX_SETTINGS_COUNT +}; + +enum Http2SessionStateIndex { + IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE, + IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH, + IDX_SESSION_STATE_NEXT_STREAM_ID, + IDX_SESSION_STATE_LOCAL_WINDOW_SIZE, + IDX_SESSION_STATE_LAST_PROC_STREAM_ID, + IDX_SESSION_STATE_REMOTE_WINDOW_SIZE, + IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE, + IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE, + IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE, + IDX_SESSION_STATE_COUNT +}; + +enum Http2StreamStateIndex { + IDX_STREAM_STATE, + IDX_STREAM_STATE_WEIGHT, + IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT, + IDX_STREAM_STATE_LOCAL_CLOSE, + IDX_STREAM_STATE_REMOTE_CLOSE, + IDX_STREAM_STATE_LOCAL_WINDOW_SIZE, + IDX_STREAM_STATE_COUNT +}; + +enum Http2OptionsIndex { + IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE, + IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS, + IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH, + IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS, + IDX_OPTIONS_PADDING_STRATEGY, + IDX_OPTIONS_FLAGS +}; + +Http2Options::Http2Options(Environment* env) { + nghttp2_option_new(&options_); + + uint32_t* buffer = env->http2_options_buffer(); + uint32_t flags = buffer[IDX_OPTIONS_FLAGS]; + + if ((flags & (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE)) == + (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE)) { + SetMaxDeflateDynamicTableSize( + buffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE]); + } + + if ((flags & (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS)) == + (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS)) { + SetMaxReservedRemoteStreams( + buffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS]); + } + + if ((flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)) == + (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)) { + SetMaxSendHeaderBlockLength( + buffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH]); + } + + SetPeerMaxConcurrentStreams(100); // Recommended default + if ((flags & (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)) == + (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)) { + SetPeerMaxConcurrentStreams( + buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]); + } + + if ((flags & (1 << IDX_OPTIONS_PADDING_STRATEGY)) == + (1 << IDX_OPTIONS_PADDING_STRATEGY)) { + SetPaddingStrategy(buffer[IDX_OPTIONS_PADDING_STRATEGY]); + } +} + +inline void CopyHeaders(Isolate* isolate, + Local context, + MaybeStackBuffer* list, + Local headers) { + Local item; + Local header; + + for (size_t n = 0; n < headers->Length(); n++) { + item = headers->Get(context, n).ToLocalChecked(); + header = item.As(); + Local key = header->Get(context, 0).ToLocalChecked(); + Local value = header->Get(context, 1).ToLocalChecked(); + CHECK(key->IsString()); + CHECK(value->IsString()); + size_t keylen = StringBytes::StorageSize(isolate, key, ASCII); + size_t valuelen = StringBytes::StorageSize(isolate, value, ASCII); + nghttp2_nv& nv = (*list)[n]; + nv.flags = NGHTTP2_NV_FLAG_NONE; + Local flag = header->Get(context, 2).ToLocalChecked(); + if (flag->BooleanValue(context).ToChecked()) + nv.flags |= NGHTTP2_NV_FLAG_NO_INDEX; + nv.name = Malloc(keylen); + nv.value = Malloc(valuelen); + nv.namelen = + StringBytes::Write(isolate, + reinterpret_cast(nv.name), + keylen, key, ASCII); + nv.valuelen = + StringBytes::Write(isolate, + reinterpret_cast(nv.value), + valuelen, value, ASCII); + } +} + +inline void FreeHeaders(MaybeStackBuffer* list) { + for (size_t n = 0; n < list->length(); n++) { + free((*list)[n].name); + free((*list)[n].value); + } +} + +void Http2Session::OnFreeSession() { + ::delete this; +} + +ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2("Http2Session: using max frame size padding\n"); + return maxPayloadLen; +} + +ssize_t Http2Session::OnCallbackPadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2("Http2Session: using callback padding\n"); + Isolate* isolate = env()->isolate(); + Local context = env()->context(); + + HandleScope handle_scope(isolate); + Context::Scope context_scope(context); + + if (object()->Has(context, env()->ongetpadding_string()).FromJust()) { + uint32_t* buffer = env()->http2_padding_buffer(); + buffer[0] = frameLen; + buffer[1] = maxPayloadLen; + MakeCallback(env()->ongetpadding_string(), 0, nullptr); + uint32_t retval = buffer[2]; + retval = retval <= maxPayloadLen ? retval : maxPayloadLen; + retval = retval >= frameLen ? retval : frameLen; + CHECK_GE(retval, frameLen); + CHECK_LE(retval, maxPayloadLen); + return retval; + } + return frameLen; +} + +void Http2Session::SetNextStreamID(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + nghttp2_session* s = session->session(); + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: setting next stream id to %d\n", id); + nghttp2_session_set_next_stream_id(s, id); +} + +void HttpErrorString(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + uint32_t val = args[0]->Uint32Value(env->context()).ToChecked(); + args.GetReturnValue().Set( + OneByteString(env->isolate(), nghttp2_strerror(val))); +} + +// Serializes the settings object into a Buffer instance that +// would be suitable, for instance, for creating the Base64 +// output for an HTTP2-Settings header field. +void PackSettings(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + HandleScope scope(env->isolate()); + + std::vector entries; + entries.reserve(6); + + uint32_t* const buffer = env->http2_settings_buffer(); + uint32_t flags = buffer[IDX_SETTINGS_COUNT]; + + if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) == + (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { + DEBUG_HTTP2("Setting header table size: %d\n", + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) == + (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { + DEBUG_HTTP2("Setting max concurrent streams: %d\n", + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]); + entries.push_back({NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) == + (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { + DEBUG_HTTP2("Setting max frame size: %d\n", + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_FRAME_SIZE, + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) == + (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { + DEBUG_HTTP2("Setting initial window size: %d\n", + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) == + (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { + DEBUG_HTTP2("Setting max header list size: %d\n", + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) == + (1 << IDX_SETTINGS_ENABLE_PUSH)) { + DEBUG_HTTP2("Setting enable push: %d\n", + buffer[IDX_SETTINGS_ENABLE_PUSH]); + entries.push_back({NGHTTP2_SETTINGS_ENABLE_PUSH, + buffer[IDX_SETTINGS_ENABLE_PUSH]}); + } + + const size_t len = entries.size() * 6; + MaybeStackBuffer buf(len); + ssize_t ret = + nghttp2_pack_settings_payload( + reinterpret_cast(*buf), len, &entries[0], entries.size()); + if (ret >= 0) { + args.GetReturnValue().Set( + Buffer::Copy(env, *buf, len).ToLocalChecked()); + } +} + +// Used to fill in the spec defined initial values for each setting. +void RefreshDefaultSettings(const FunctionCallbackInfo& args) { + DEBUG_HTTP2("Http2Session: refreshing default settings\n"); + Environment* env = Environment::GetCurrent(args); + uint32_t* const buffer = env->http2_settings_buffer(); + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = + DEFAULT_SETTINGS_HEADER_TABLE_SIZE; + buffer[IDX_SETTINGS_ENABLE_PUSH] = + DEFAULT_SETTINGS_ENABLE_PUSH; + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = + DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE; + buffer[IDX_SETTINGS_MAX_FRAME_SIZE] = + DEFAULT_SETTINGS_MAX_FRAME_SIZE; + buffer[IDX_SETTINGS_COUNT] = + (1 << IDX_SETTINGS_HEADER_TABLE_SIZE) | + (1 << IDX_SETTINGS_ENABLE_PUSH) | + (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE) | + (1 << IDX_SETTINGS_MAX_FRAME_SIZE); +} + +template +void RefreshSettings(const FunctionCallbackInfo& args) { + DEBUG_HTTP2("Http2Session: refreshing settings for session\n"); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsObject()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); + Environment* env = session->env(); + nghttp2_session* s = session->session(); + + uint32_t* const buffer = env->http2_settings_buffer(); + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = + fn(s, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE); + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = + fn(s, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = + fn(s, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE); + buffer[IDX_SETTINGS_MAX_FRAME_SIZE] = + fn(s, NGHTTP2_SETTINGS_MAX_FRAME_SIZE); + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = + fn(s, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE); + buffer[IDX_SETTINGS_ENABLE_PUSH] = + fn(s, NGHTTP2_SETTINGS_ENABLE_PUSH); +} + +// Used to fill in the spec defined initial values for each setting. +void RefreshSessionState(const FunctionCallbackInfo& args) { + DEBUG_HTTP2("Http2Session: refreshing session state\n"); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsObject()); + Environment* env = Environment::GetCurrent(args); + double* const buffer = env->http2_session_state_buffer(); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); + nghttp2_session* s = session->session(); + + buffer[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_effective_local_window_size(s); + buffer[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH] = + nghttp2_session_get_effective_recv_data_length(s); + buffer[IDX_SESSION_STATE_NEXT_STREAM_ID] = + nghttp2_session_get_next_stream_id(s); + buffer[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_local_window_size(s); + buffer[IDX_SESSION_STATE_LAST_PROC_STREAM_ID] = + nghttp2_session_get_last_proc_stream_id(s); + buffer[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE] = + nghttp2_session_get_remote_window_size(s); + buffer[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE] = + nghttp2_session_get_outbound_queue_size(s); + buffer[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE] = + nghttp2_session_get_hd_deflate_dynamic_table_size(s); + buffer[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE] = + nghttp2_session_get_hd_inflate_dynamic_table_size(s); +} + +void RefreshStreamState(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 2); + CHECK(args[0]->IsObject()); + CHECK(args[1]->IsNumber()); + int32_t id = args[1]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: refreshing stream %d state\n", id); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); + nghttp2_session* s = session->session(); + Nghttp2Stream* stream; + + double* const buffer = env->http2_stream_state_buffer(); + + if ((stream = session->FindStream(id)) == nullptr) { + buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; + buffer[IDX_STREAM_STATE_WEIGHT] = + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; + return; + } + nghttp2_stream* str = + nghttp2_session_find_stream(s, stream->id()); + + if (str == nullptr) { + buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; + buffer[IDX_STREAM_STATE_WEIGHT] = + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; + } else { + buffer[IDX_STREAM_STATE] = + nghttp2_stream_get_state(str); + buffer[IDX_STREAM_STATE_WEIGHT] = + nghttp2_stream_get_weight(str); + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + nghttp2_stream_get_sum_dependency_weight(str); + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + nghttp2_session_get_stream_local_close(s, id); + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + nghttp2_session_get_stream_remote_close(s, id); + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_stream_local_window_size(s, id); + } +} + +void Http2Session::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + + int val = args[0]->IntegerValue(env->context()).ToChecked(); + nghttp2_session_type type = static_cast(val); + DEBUG_HTTP2("Http2Session: creating a session of type: %d\n", type); + new Http2Session(env, args.This(), type); +} + + +// Capture the stream that this session will use to send and receive data +void Http2Session::Consume(const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsExternal()); + session->Consume(args[0].As()); +} + +void Http2Session::Destroy(const FunctionCallbackInfo& args) { + DEBUG_HTTP2("Http2Session: destroying session\n"); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->Unconsume(); + session->Free(); +} + +void Http2Session::SubmitPriority(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local context = env->context(); + + nghttp2_priority_spec spec; + int32_t id = args[0]->Int32Value(context).ToChecked(); + int32_t parent = args[1]->Int32Value(context).ToChecked(); + int32_t weight = args[2]->Int32Value(context).ToChecked(); + bool exclusive = args[3]->BooleanValue(context).ToChecked(); + bool silent = args[4]->BooleanValue(context).ToChecked(); + DEBUG_HTTP2("Http2Session: submitting priority for stream %d: " + "parent: %d, weight: %d, exclusive: %d, silent: %d\n", + id, parent, weight, exclusive, silent); + CHECK_GT(id, 0); + CHECK_GE(parent, 0); + CHECK_GE(weight, 0); + + Nghttp2Stream* stream; + if (!(stream = session->FindStream(id))) { + // invalid stream + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + nghttp2_priority_spec_init(&spec, parent, weight, exclusive ? 1 : 0); + + args.GetReturnValue().Set(stream->SubmitPriority(&spec, silent)); +} + +void Http2Session::SubmitSettings(const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + Environment* env = session->env(); + + uint32_t* const buffer = env->http2_settings_buffer(); + uint32_t flags = buffer[IDX_SETTINGS_COUNT]; + + std::vector entries; + entries.reserve(6); + + if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) == + (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { + DEBUG_HTTP2("Setting header table size: %d\n", + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) == + (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { + DEBUG_HTTP2("Setting max concurrent streams: %d\n", + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]); + entries.push_back({NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) == + (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { + DEBUG_HTTP2("Setting max frame size: %d\n", + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_FRAME_SIZE, + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) == + (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { + DEBUG_HTTP2("Setting initial window size: %d\n", + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) == + (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { + DEBUG_HTTP2("Setting max header list size: %d\n", + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) == + (1 << IDX_SETTINGS_ENABLE_PUSH)) { + DEBUG_HTTP2("Setting enable push: %d\n", + buffer[IDX_SETTINGS_ENABLE_PUSH]); + entries.push_back({NGHTTP2_SETTINGS_ENABLE_PUSH, + buffer[IDX_SETTINGS_ENABLE_PUSH]}); + } + + if (entries.size() > 0) { + args.GetReturnValue().Set( + session->Nghttp2Session::SubmitSettings(&entries[0], entries.size())); + } else { + args.GetReturnValue().Set( + session->Nghttp2Session::SubmitSettings(nullptr, 0)); + } +} + +void Http2Session::SubmitRstStream(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsNumber()); + + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + int32_t id = args[0]->Int32Value(context).ToChecked(); + uint32_t code = args[1]->Uint32Value(context).ToChecked(); + + Nghttp2Stream* stream; + if (!(stream = session->FindStream(id))) { + // invalid stream + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + DEBUG_HTTP2("Http2Session: sending rst_stream for stream %d, code: %d\n", + id, code); + args.GetReturnValue().Set(stream->SubmitRstStream(code)); +} + +void Http2Session::SubmitRequest(const FunctionCallbackInfo& args) { + // args[0] Array of headers + // args[1] endStream boolean + // args[2] parentStream ID (for priority spec) + // args[3] weight (for priority spec) + // args[4] exclusive boolean (for priority spec) + CHECK(args[0]->IsArray()); + + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local context = env->context(); + Isolate* isolate = env->isolate(); + + Local headers = args[0].As(); + bool endStream = args[1]->BooleanValue(context).ToChecked(); + int32_t parent = args[2]->Int32Value(context).ToChecked(); + int32_t weight = args[3]->Int32Value(context).ToChecked(); + bool exclusive = args[4]->BooleanValue(context).ToChecked(); + + DEBUG_HTTP2("Http2Session: submitting request: headers: %d, end-stream: %d, " + "parent: %d, weight: %d, exclusive: %d\n", headers->Length(), + endStream, parent, weight, exclusive); + + nghttp2_priority_spec prispec; + nghttp2_priority_spec_init(&prispec, parent, weight, exclusive ? 1 : 0); + + Headers list(isolate, context, headers); + + int32_t ret = session->Nghttp2Session::SubmitRequest(&prispec, + *list, list.length(), + nullptr, endStream); + DEBUG_HTTP2("Http2Session: request submitted, response: %d\n", ret); + args.GetReturnValue().Set(ret); +} + +void Http2Session::SubmitResponse(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsArray()); + + Http2Session* session; + Nghttp2Stream* stream; + + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local context = env->context(); + Isolate* isolate = env->isolate(); + + int32_t id = args[0]->Int32Value(context).ToChecked(); + Local headers = args[1].As(); + bool endStream = args[2]->BooleanValue(context).ToChecked(); + + DEBUG_HTTP2("Http2Session: submitting response for stream %d: headers: %d, " + "end-stream: %d\n", id, headers->Length(), endStream); + + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + args.GetReturnValue().Set( + stream->SubmitResponse(*list, list.length(), endStream)); +} + +void Http2Session::SubmitFile(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsNumber()); // Stream ID + CHECK(args[1]->IsNumber()); // File Descriptor + CHECK(args[2]->IsArray()); // Headers + + Http2Session* session; + Nghttp2Stream* stream; + + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local context = env->context(); + Isolate* isolate = env->isolate(); + + int32_t id = args[0]->Int32Value(context).ToChecked(); + int fd = args[1]->Int32Value(context).ToChecked(); + Local headers = args[2].As(); + + DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, " + "end-stream: %d\n", fd, id, headers->Length()); + + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length())); +} + +void Http2Session::SendHeaders(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsArray()); + + Http2Session* session; + Nghttp2Stream* stream; + + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local context = env->context(); + Isolate* isolate = env->isolate(); + + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + Local headers = args[1].As(); + + DEBUG_HTTP2("Http2Session: sending informational headers for stream %d, " + "count: %d\n", id, headers->Length()); + + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); +} + +void Http2Session::ShutdownStream(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsNumber()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Nghttp2Stream* stream; + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: shutting down stream %d\n", id); + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->Shutdown(); +} + + +void Http2Session::StreamReadStart(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsNumber()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Nghttp2Stream* stream; + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->ReadStart(); +} + + +void Http2Session::StreamReadStop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsNumber()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Nghttp2Stream* stream; + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->ReadStop(); +} + +void Http2Session::SendShutdownNotice( + const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->SubmitShutdownNotice(); +} + +void Http2Session::SubmitGoaway(const FunctionCallbackInfo& args) { + Http2Session* session; + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + uint32_t errorCode = args[0]->Uint32Value(context).ToChecked(); + int32_t lastStreamID = args[1]->Int32Value(context).ToChecked(); + Local opaqueData = args[2]; + + uint8_t* data = NULL; + size_t length = 0; + + if (opaqueData->BooleanValue(context).ToChecked()) { + THROW_AND_RETURN_UNLESS_BUFFER(env, opaqueData); + SPREAD_BUFFER_ARG(opaqueData, buf); + data = reinterpret_cast(buf_data); + length = buf_length; + } + + DEBUG_HTTP2("Http2Session: initiating immediate shutdown. " + "last-stream-id: %d, code: %d, opaque-data: %d\n", + lastStreamID, errorCode, length); + int status = nghttp2_submit_goaway(session->session(), + NGHTTP2_FLAG_NONE, + lastStreamID, + errorCode, + data, length); + args.GetReturnValue().Set(status); +} + +void Http2Session::DestroyStream(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsNumber()); + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: destroy stream %d\n", id); + Nghttp2Stream* stream; + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->Destroy(); +} + +void Http2Session::SubmitPushPromise(const FunctionCallbackInfo& args) { + Http2Session* session; + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Isolate* isolate = env->isolate(); + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + CHECK(args[0]->IsNumber()); // parent stream ID + CHECK(args[1]->IsArray()); // headers array + + Nghttp2Stream* parent; + int32_t id = args[0]->Int32Value(context).ToChecked(); + Local headers = args[1].As(); + bool endStream = args[2]->BooleanValue(context).ToChecked(); + + DEBUG_HTTP2("Http2Session: submitting push promise for stream %d: " + "end-stream: %d, headers: %d\n", id, endStream, + headers->Length()); + + if (!(parent = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + int32_t ret = parent->SubmitPushPromise(*list, list.length(), + nullptr, endStream); + DEBUG_HTTP2("Http2Session: push promise submitted, ret: %d\n", ret); + args.GetReturnValue().Set(ret); +} + +int Http2Session::DoWrite(WriteWrap* req_wrap, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) { + Environment* env = req_wrap->env(); + Local req_wrap_obj = req_wrap->object(); + Local context = env->context(); + + Nghttp2Stream* stream; + { + Local val = + req_wrap_obj->Get(context, env->stream_string()).ToLocalChecked(); + int32_t id = val->Int32Value(context).ToChecked(); + if (!val->IsNumber() || !(stream = FindStream(id))) { + // invalid stream + req_wrap->Dispatched(); + req_wrap->Done(0); + return NGHTTP2_ERR_INVALID_STREAM_ID; + } + } + + nghttp2_stream_write_t* req = new nghttp2_stream_write_t; + req->data = req_wrap; + + auto AfterWrite = [](nghttp2_stream_write_t* req, int status) { + WriteWrap* wrap = static_cast(req->data); + wrap->Done(status); + delete req; + }; + req_wrap->Dispatched(); + stream->Write(req, bufs, count, AfterWrite); + return 0; +} + +void Http2Session::AllocateSend(size_t recommended, uv_buf_t* buf) { + buf->base = stream_alloc(); + buf->len = kAllocBufferSize; +} + +void Http2Session::Send(uv_buf_t* buf, size_t length) { + if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) { + return; + } + HandleScope scope(env()->isolate()); + + auto AfterWrite = [](WriteWrap* req_wrap, int status) { + req_wrap->Dispose(); + }; + Local req_wrap_obj = + env()->write_wrap_constructor_function() + ->NewInstance(env()->context()).ToLocalChecked(); + WriteWrap* write_req = WriteWrap::New(env(), + req_wrap_obj, + this, + AfterWrite); + + uv_buf_t actual = uv_buf_init(buf->base, length); + if (stream_->DoWrite(write_req, &actual, 1, nullptr)) { + write_req->Dispose(); + } +} + +void Http2Session::OnTrailers(Nghttp2Stream* stream, + MaybeStackBuffer* trailers) { + DEBUG_HTTP2("Http2Session: prompting for trailers on stream %d\n", + stream->id()); + Local context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + + if (object()->Has(context, env()->ontrailers_string()).FromJust()) { + Local argv[1] = { + Integer::New(isolate, stream->id()) + }; + + Local ret = MakeCallback(env()->ontrailers_string(), + arraysize(argv), argv); + if (!ret.IsEmpty()) { + if (ret->IsArray()) { + Local headers = ret.As(); + if (headers->Length() > 0) { + trailers->AllocateSufficientStorage(headers->Length()); + CopyHeaders(isolate, context, trailers, headers); + } + } + } + } +} + +void Http2Session::OnHeaders(Nghttp2Stream* stream, + nghttp2_header_list* headers, + nghttp2_headers_category cat, + uint8_t flags) { + Local context = env()->context(); + Isolate* isolate = env()->isolate(); + Context::Scope context_scope(context); + HandleScope scope(isolate); + Local name_str; + Local value_str; + + Local holder = Array::New(isolate); + Local fn = env()->push_values_to_array_function(); + Local argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2]; + + // The headers are passed in above as a linked list of nghttp2_header_list + // structs. The following converts that into a JS array with the structure: + // [name1, value1, name2, value2, name3, value3, name3, value4] and so on. + // That array is passed up to the JS layer and converted into an Object form + // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it + // this way for performance reasons (it's faster to generate and pass an + // array than it is to generate and pass the object). + do { + size_t j = 0; + while (headers != nullptr && j < arraysize(argv) / 2) { + nghttp2_header_list* item = headers; + // The header name and value are passed as external one-byte strings + name_str = ExternalHeader::New(isolate, item->name); + value_str = ExternalHeader::New(isolate, item->value); + argv[j * 2] = name_str; + argv[j * 2 + 1] = value_str; + headers = item->next; + j++; + } + // For performance, we pass name and value pairs to array.protototype.push + // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no + // more items to push. + if (j > 0) { + fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked(); + } + } while (headers != nullptr); + + if (object()->Has(context, env()->onheaders_string()).FromJust()) { + Local argv[4] = { + Integer::New(isolate, stream->id()), + Integer::New(isolate, cat), + Integer::New(isolate, flags), + holder + }; + MakeCallback(env()->onheaders_string(), arraysize(argv), argv); + } +} + + +void Http2Session::OnStreamClose(int32_t id, uint32_t code) { + Isolate* isolate = env()->isolate(); + Local context = env()->context(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onstreamclose_string()).FromJust()) { + Local argv[2] = { + Integer::New(isolate, id), + Integer::NewFromUnsigned(isolate, code) + }; + MakeCallback(env()->onstreamclose_string(), arraysize(argv), argv); + } +} + +void FreeDataChunk(char* data, void* hint) { + nghttp2_data_chunk_t* item = reinterpret_cast(hint); + delete[] data; + data_chunk_free_list.push(item); +} + +void Http2Session::OnDataChunk( + Nghttp2Stream* stream, + nghttp2_data_chunk_t* chunk) { + Isolate* isolate = env()->isolate(); + Local context = env()->context(); + HandleScope scope(isolate); + Local obj = Object::New(isolate); + obj->Set(context, + env()->id_string(), + Integer::New(isolate, stream->id())).FromJust(); + ssize_t len = -1; + Local buf; + if (chunk != nullptr) { + len = chunk->buf.len; + buf = Buffer::New(isolate, + chunk->buf.base, len, + FreeDataChunk, + chunk).ToLocalChecked(); + } + EmitData(len, buf, obj); +} + +void Http2Session::OnSettings(bool ack) { + Local context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onsettings_string()).FromJust()) { + Local argv[1] = { Boolean::New(isolate, ack) }; + MakeCallback(env()->onsettings_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnFrameError(int32_t id, uint8_t type, int error_code) { + Local context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onframeerror_string()).FromJust()) { + Local argv[3] = { + Integer::New(isolate, id), + Integer::New(isolate, type), + Integer::New(isolate, error_code) + }; + MakeCallback(env()->onframeerror_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnPriority(int32_t stream, + int32_t parent, + int32_t weight, + int8_t exclusive) { + Local context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onpriority_string()).FromJust()) { + Local argv[4] = { + Integer::New(isolate, stream), + Integer::New(isolate, parent), + Integer::New(isolate, weight), + Boolean::New(isolate, exclusive) + }; + MakeCallback(env()->onpriority_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnGoAway(int32_t lastStreamID, + uint32_t errorCode, + uint8_t* data, + size_t length) { + Local context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->ongoawaydata_string()).FromJust()) { + Local argv[3] = { + Integer::NewFromUnsigned(isolate, errorCode), + Integer::New(isolate, lastStreamID), + Undefined(isolate) + }; + + if (length > 0) { + argv[2] = Buffer::Copy(isolate, + reinterpret_cast(data), + length).ToLocalChecked(); + } + + MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnStreamAllocImpl(size_t suggested_size, + uv_buf_t* buf, + void* ctx) { + Http2Session* session = static_cast(ctx); + buf->base = session->stream_alloc(); + buf->len = kAllocBufferSize; +} + + +void Http2Session::OnStreamReadImpl(ssize_t nread, + const uv_buf_t* bufs, + uv_handle_type pending, + void* ctx) { + Http2Session* session = static_cast(ctx); + if (nread < 0) { + uv_buf_t tmp_buf; + tmp_buf.base = nullptr; + tmp_buf.len = 0; + session->prev_read_cb_.fn(nread, + &tmp_buf, + pending, + session->prev_read_cb_.ctx); + return; + } + if (nread > 0) { + // Only pass data on if nread > 0 + uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) }; + ssize_t ret = session->Write(buf, 1); + if (ret < 0) { + DEBUG_HTTP2("Http2Session: fatal error receiving data: %d\n", ret); + nghttp2_session_terminate_session(session->session(), + NGHTTP2_PROTOCOL_ERROR); + } + } +} + + +void Http2Session::Consume(Local external) { + DEBUG_HTTP2("Http2Session: consuming socket\n"); + CHECK(prev_alloc_cb_.is_empty()); + StreamBase* stream = static_cast(external->Value()); + CHECK_NE(stream, nullptr); + stream->Consume(); + stream_ = stream; + prev_alloc_cb_ = stream->alloc_cb(); + prev_read_cb_ = stream->read_cb(); + stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this }); + stream->set_read_cb({ Http2Session::OnStreamReadImpl, this }); +} + + +void Http2Session::Unconsume() { + DEBUG_HTTP2("Http2Session: unconsuming socket\n"); + if (prev_alloc_cb_.is_empty()) + return; + stream_->set_alloc_cb(prev_alloc_cb_); + stream_->set_read_cb(prev_read_cb_); + prev_alloc_cb_.clear(); + prev_read_cb_.clear(); + stream_ = nullptr; +} + + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + + // Initialize the buffer used for padding callbacks + env->set_http2_padding_buffer(new uint32_t[3]); + const size_t http2_padding_buffer_byte_length = + sizeof(*env->http2_padding_buffer()) * 3; + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "paddingArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_padding_buffer(), + http2_padding_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the session state + env->set_http2_session_state_buffer( + new double[IDX_SESSION_STATE_COUNT]); + + const size_t http2_session_state_buffer_byte_length = + sizeof(*env->http2_session_state_buffer()) * + IDX_SESSION_STATE_COUNT; + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "sessionStateArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_session_state_buffer(), + http2_session_state_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the stream state + env->set_http2_stream_state_buffer( + new double[IDX_STREAM_STATE_COUNT]); + + const size_t http2_stream_state_buffer_byte_length = + sizeof(*env->http2_stream_state_buffer()) * + IDX_STREAM_STATE_COUNT; + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "streamStateArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_stream_state_buffer(), + http2_stream_state_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the current settings + env->set_http2_settings_buffer( + new uint32_t[IDX_SETTINGS_COUNT + 1]); + + const size_t http2_settings_buffer_byte_length = + sizeof(*env->http2_settings_buffer()) * + (IDX_SETTINGS_COUNT + 1); + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "settingsArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_settings_buffer(), + http2_settings_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the options + env->set_http2_options_buffer( + new uint32_t[IDX_OPTIONS_FLAGS + 1]); + + const size_t http2_options_buffer_byte_length = + sizeof(*env->http2_options_buffer()) * + (IDX_OPTIONS_FLAGS + 1); + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "optionsArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_options_buffer(), + http2_options_buffer_byte_length)) + .FromJust(); + + // Method to fetch the nghttp2 string description of an nghttp2 error code + env->SetMethod(target, "nghttp2ErrorString", HttpErrorString); + + Local http2SessionClassName = + String::NewFromUtf8(isolate, "Http2Session", + v8::NewStringType::kInternalized).ToLocalChecked(); + + Local session = + env->NewFunctionTemplate(Http2Session::New); + session->SetClassName(http2SessionClassName); + session->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(session, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(session, "consume", + Http2Session::Consume); + env->SetProtoMethod(session, "destroy", + Http2Session::Destroy); + env->SetProtoMethod(session, "sendHeaders", + Http2Session::SendHeaders); + env->SetProtoMethod(session, "submitShutdownNotice", + Http2Session::SendShutdownNotice); + env->SetProtoMethod(session, "submitGoaway", + Http2Session::SubmitGoaway); + env->SetProtoMethod(session, "submitSettings", + Http2Session::SubmitSettings); + env->SetProtoMethod(session, "submitPushPromise", + Http2Session::SubmitPushPromise); + env->SetProtoMethod(session, "submitRstStream", + Http2Session::SubmitRstStream); + env->SetProtoMethod(session, "submitResponse", + Http2Session::SubmitResponse); + env->SetProtoMethod(session, "submitFile", + Http2Session::SubmitFile); + env->SetProtoMethod(session, "submitRequest", + Http2Session::SubmitRequest); + env->SetProtoMethod(session, "submitPriority", + Http2Session::SubmitPriority); + env->SetProtoMethod(session, "shutdownStream", + Http2Session::ShutdownStream); + env->SetProtoMethod(session, "streamReadStart", + Http2Session::StreamReadStart); + env->SetProtoMethod(session, "streamReadStop", + Http2Session::StreamReadStop); + env->SetProtoMethod(session, "setNextStreamID", + Http2Session::SetNextStreamID); + env->SetProtoMethod(session, "destroyStream", + Http2Session::DestroyStream); + StreamBase::AddMethods(env, session, + StreamBase::kFlagHasWritev | + StreamBase::kFlagNoShutdown); + target->Set(context, + http2SessionClassName, + session->GetFunction()).FromJust(); + + Local constants = Object::New(isolate); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SESSION_SERVER); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SESSION_CLIENT); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_IDLE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_OPEN); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_RESERVED_LOCAL); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_RESERVED_REMOTE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_CLOSED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_NO_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_PROTOCOL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_INTERNAL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLOW_CONTROL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_TIMEOUT); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_CLOSED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FRAME_SIZE_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_REFUSED_STREAM); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_CANCEL); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_COMPRESSION_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_CONNECT_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_ENHANCE_YOUR_CALM); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_INADEQUATE_SECURITY); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_HTTP_1_1_REQUIRED); + + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_REQUEST); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_RESPONSE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_PUSH_RESPONSE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_HEADERS); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NONE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NO_INDEX); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_DEFERRED); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_NOMEM); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_INVALID_ARGUMENT); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_CLOSED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_ERR_FRAME_SIZE_ERROR); + + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_NONE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_END_STREAM); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_END_HEADERS); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_ACK); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_PADDED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_PRIORITY); + + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_HEADER_TABLE_SIZE); + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_ENABLE_PUSH); + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE); + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, MAX_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, MIN_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, MAX_INITIAL_WINDOW_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_DEFAULT_WEIGHT); + + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_ENABLE_PUSH); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE); + + NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE); + NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_MAX); + NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_CALLBACK); + +#define STRING_CONSTANT(NAME, VALUE) \ + NODE_DEFINE_STRING_CONSTANT(constants, "HTTP2_HEADER_" # NAME, VALUE); +HTTP_KNOWN_HEADERS(STRING_CONSTANT) +#undef STRING_CONSTANT + +#define STRING_CONSTANT(NAME, VALUE) \ + NODE_DEFINE_STRING_CONSTANT(constants, "HTTP2_METHOD_" # NAME, VALUE); +HTTP_KNOWN_METHODS(STRING_CONSTANT) +#undef STRING_CONSTANT + +#define V(name, _) NODE_DEFINE_CONSTANT(constants, HTTP_STATUS_##name); +HTTP_STATUS_CODES(V) +#undef V + + env->SetMethod(target, "refreshLocalSettings", + RefreshSettings); + env->SetMethod(target, "refreshRemoteSettings", + RefreshSettings); + env->SetMethod(target, "refreshDefaultSettings", RefreshDefaultSettings); + env->SetMethod(target, "refreshSessionState", RefreshSessionState); + env->SetMethod(target, "refreshStreamState", RefreshStreamState); + env->SetMethod(target, "packSettings", PackSettings); + + target->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "constants"), + constants).FromJust(); +} +} // namespace http2 +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(http2, node::http2::Initialize) diff --git a/src/node_http2.h b/src/node_http2.h new file mode 100644 index 00000000000000..f6ccad29846d4a --- /dev/null +++ b/src/node_http2.h @@ -0,0 +1,572 @@ +#ifndef SRC_NODE_HTTP2_H_ +#define SRC_NODE_HTTP2_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_http2_core-inl.h" +#include "stream_base-inl.h" +#include "string_bytes.h" + +namespace node { +namespace http2 { + +using v8::Array; +using v8::Context; +using v8::EscapableHandleScope; +using v8::Isolate; +using v8::MaybeLocal; + +#define HTTP_KNOWN_METHODS(V) \ + V(ACL, "ACL") \ + V(BASELINE_CONTROL, "BASELINE-CONTROL") \ + V(BIND, "BIND") \ + V(CHECKIN, "CHECKIN") \ + V(CHECKOUT, "CHECKOUT") \ + V(CONNECT, "CONNECT") \ + V(COPY, "COPY") \ + V(DELETE, "DELETE") \ + V(GET, "GET") \ + V(HEAD, "HEAD") \ + V(LABEL, "LABEL") \ + V(LINK, "LINK") \ + V(LOCK, "LOCK") \ + V(MERGE, "MERGE") \ + V(MKACTIVITY, "MKACTIVITY") \ + V(MKCALENDAR, "MKCALENDAR") \ + V(MKCOL, "MKCOL") \ + V(MKREDIRECTREF, "MKREDIRECTREF") \ + V(MKWORKSPACE, "MKWORKSPACE") \ + V(MOVE, "MOVE") \ + V(OPTIONS, "OPTIONS") \ + V(ORDERPATCH, "ORDERPATCH") \ + V(PATCH, "PATCH") \ + V(POST, "POST") \ + V(PRI, "PRI") \ + V(PROPFIND, "PROPFIND") \ + V(PROPPATCH, "PROPPATCH") \ + V(PUT, "PUT") \ + V(REBIND, "REBIND") \ + V(REPORT, "REPORT") \ + V(SEARCH, "SEARCH") \ + V(TRACE, "TRACE") \ + V(UNBIND, "UNBIND") \ + V(UNCHECKOUT, "UNCHECKOUT") \ + V(UNLINK, "UNLINK") \ + V(UNLOCK, "UNLOCK") \ + V(UPDATE, "UPDATE") \ + V(UPDATEREDIRECTREF, "UPDATEREDIRECTREF") \ + V(VERSION_CONTROL, "VERSION-CONTROL") + +#define HTTP_KNOWN_HEADERS(V) \ + V(STATUS, ":status") \ + V(METHOD, ":method") \ + V(AUTHORITY, ":authority") \ + V(SCHEME, ":scheme") \ + V(PATH, ":path") \ + V(ACCEPT_CHARSET, "accept-charset") \ + V(ACCEPT_ENCODING, "accept-encoding") \ + V(ACCEPT_LANGUAGE, "accept-language") \ + V(ACCEPT_RANGES, "accept-ranges") \ + V(ACCEPT, "accept") \ + V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \ + V(AGE, "age") \ + V(ALLOW, "allow") \ + V(AUTHORIZATION, "authorization") \ + V(CACHE_CONTROL, "cache-control") \ + V(CONNECTION, "connection") \ + V(CONTENT_DISPOSITION, "content-disposition") \ + V(CONTENT_ENCODING, "content-encoding") \ + V(CONTENT_LANGUAGE, "content-language") \ + V(CONTENT_LENGTH, "content-length") \ + V(CONTENT_LOCATION, "content-location") \ + V(CONTENT_MD5, "content-md5") \ + V(CONTENT_RANGE, "content-range") \ + V(CONTENT_TYPE, "content-type") \ + V(COOKIE, "cookie") \ + V(DATE, "date") \ + V(ETAG, "etag") \ + V(EXPECT, "expect") \ + V(EXPIRES, "expires") \ + V(FROM, "from") \ + V(HOST, "host") \ + V(IF_MATCH, "if-match") \ + V(IF_MODIFIED_SINCE, "if-modified-since") \ + V(IF_NONE_MATCH, "if-none-match") \ + V(IF_RANGE, "if-range") \ + V(IF_UNMODIFIED_SINCE, "if-unmodified-since") \ + V(LAST_MODIFIED, "last-modified") \ + V(LINK, "link") \ + V(LOCATION, "location") \ + V(MAX_FORWARDS, "max-forwards") \ + V(PREFER, "prefer") \ + V(PROXY_AUTHENTICATE, "proxy-authenticate") \ + V(PROXY_AUTHORIZATION, "proxy-authorization") \ + V(RANGE, "range") \ + V(REFERER, "referer") \ + V(REFRESH, "refresh") \ + V(RETRY_AFTER, "retry-after") \ + V(SERVER, "server") \ + V(SET_COOKIE, "set-cookie") \ + V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \ + V(TRANSFER_ENCODING, "transfer-encoding") \ + V(TE, "te") \ + V(UPGRADE, "upgrade") \ + V(USER_AGENT, "user-agent") \ + V(VARY, "vary") \ + V(VIA, "via") \ + V(WWW_AUTHENTICATE, "www-authenticate") \ + V(HTTP2_SETTINGS, "http2-settings") \ + V(KEEP_ALIVE, "keep-alive") \ + V(PROXY_CONNECTION, "proxy-connection") + +enum http_known_headers { +HTTP_KNOWN_HEADER_MIN, +#define V(name, value) HTTP_HEADER_##name, +HTTP_KNOWN_HEADERS(V) +#undef V +HTTP_KNOWN_HEADER_MAX +}; + +#define HTTP_STATUS_CODES(V) \ + V(CONTINUE, 100) \ + V(SWITCHING_PROTOCOLS, 101) \ + V(PROCESSING, 102) \ + V(OK, 200) \ + V(CREATED, 201) \ + V(ACCEPTED, 202) \ + V(NON_AUTHORITATIVE_INFORMATION, 203) \ + V(NO_CONTENT, 204) \ + V(RESET_CONTENT, 205) \ + V(PARTIAL_CONTENT, 206) \ + V(MULTI_STATUS, 207) \ + V(ALREADY_REPORTED, 208) \ + V(IM_USED, 226) \ + V(MULTIPLE_CHOICES, 300) \ + V(MOVED_PERMANENTLY, 301) \ + V(FOUND, 302) \ + V(SEE_OTHER, 303) \ + V(NOT_MODIFIED, 304) \ + V(USE_PROXY, 305) \ + V(TEMPORARY_REDIRECT, 307) \ + V(PERMANENT_REDIRECT, 308) \ + V(BAD_REQUEST, 400) \ + V(UNAUTHORIZED, 401) \ + V(PAYMENT_REQUIRED, 402) \ + V(FORBIDDEN, 403) \ + V(NOT_FOUND, 404) \ + V(METHOD_NOT_ALLOWED, 405) \ + V(NOT_ACCEPTABLE, 406) \ + V(PROXY_AUTHENTICATION_REQUIRED, 407) \ + V(REQUEST_TIMEOUT, 408) \ + V(CONFLICT, 409) \ + V(GONE, 410) \ + V(LENGTH_REQUIRED, 411) \ + V(PRECONDITION_FAILED, 412) \ + V(PAYLOAD_TOO_LARGE, 413) \ + V(URI_TOO_LONG, 414) \ + V(UNSUPPORTED_MEDIA_TYPE, 415) \ + V(RANGE_NOT_SATISFIABLE, 416) \ + V(EXPECTATION_FAILED, 417) \ + V(TEAPOT, 418) \ + V(MISDIRECTED_REQUEST, 421) \ + V(UNPROCESSABLE_ENTITY, 422) \ + V(LOCKED, 423) \ + V(FAILED_DEPENDENCY, 424) \ + V(UNORDERED_COLLECTION, 425) \ + V(UPGRADE_REQUIRED, 426) \ + V(PRECONDITION_REQUIRED, 428) \ + V(TOO_MANY_REQUESTS, 429) \ + V(REQUEST_HEADER_FIELDS_TOO_LARGE, 431) \ + V(UNAVAILABLE_FOR_LEGAL_REASONS, 451) \ + V(INTERNAL_SERVER_ERROR, 500) \ + V(NOT_IMPLEMENTED, 501) \ + V(BAD_GATEWAY, 502) \ + V(SERVICE_UNAVAILABLE, 503) \ + V(GATEWAY_TIMEOUT, 504) \ + V(HTTP_VERSION_NOT_SUPPORTED, 505) \ + V(VARIANT_ALSO_NEGOTIATES, 506) \ + V(INSUFFICIENT_STORAGE, 507) \ + V(LOOP_DETECTED, 508) \ + V(BANDWIDTH_LIMIT_EXCEEDED, 509) \ + V(NOT_EXTENDED, 510) \ + V(NETWORK_AUTHENTICATION_REQUIRED, 511) + +enum http_status_codes { +#define V(name, code) HTTP_STATUS_##name = code, +HTTP_STATUS_CODES(V) +#undef V +}; + +enum padding_strategy_type { + // No padding strategy + PADDING_STRATEGY_NONE, + // Padding will ensure all data frames are maxFrameSize + PADDING_STRATEGY_MAX, + // Padding will be determined via JS callback + PADDING_STRATEGY_CALLBACK +}; + +#define NGHTTP2_ERROR_CODES(V) \ + V(NGHTTP2_ERR_INVALID_ARGUMENT) \ + V(NGHTTP2_ERR_BUFFER_ERROR) \ + V(NGHTTP2_ERR_UNSUPPORTED_VERSION) \ + V(NGHTTP2_ERR_WOULDBLOCK) \ + V(NGHTTP2_ERR_PROTO) \ + V(NGHTTP2_ERR_INVALID_FRAME) \ + V(NGHTTP2_ERR_EOF) \ + V(NGHTTP2_ERR_DEFERRED) \ + V(NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE) \ + V(NGHTTP2_ERR_STREAM_CLOSED) \ + V(NGHTTP2_ERR_STREAM_CLOSING) \ + V(NGHTTP2_ERR_STREAM_SHUT_WR) \ + V(NGHTTP2_ERR_INVALID_STREAM_ID) \ + V(NGHTTP2_ERR_INVALID_STREAM_STATE) \ + V(NGHTTP2_ERR_DEFERRED_DATA_EXIST) \ + V(NGHTTP2_ERR_START_STREAM_NOT_ALLOWED) \ + V(NGHTTP2_ERR_GOAWAY_ALREADY_SENT) \ + V(NGHTTP2_ERR_INVALID_HEADER_BLOCK) \ + V(NGHTTP2_ERR_INVALID_STATE) \ + V(NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) \ + V(NGHTTP2_ERR_FRAME_SIZE_ERROR) \ + V(NGHTTP2_ERR_HEADER_COMP) \ + V(NGHTTP2_ERR_FLOW_CONTROL) \ + V(NGHTTP2_ERR_INSUFF_BUFSIZE) \ + V(NGHTTP2_ERR_PAUSE) \ + V(NGHTTP2_ERR_TOO_MANY_INFLIGHT_SETTINGS) \ + V(NGHTTP2_ERR_PUSH_DISABLED) \ + V(NGHTTP2_ERR_DATA_EXIST) \ + V(NGHTTP2_ERR_SESSION_CLOSING) \ + V(NGHTTP2_ERR_HTTP_HEADER) \ + V(NGHTTP2_ERR_HTTP_MESSAGING) \ + V(NGHTTP2_ERR_REFUSED_STREAM) \ + V(NGHTTP2_ERR_INTERNAL) \ + V(NGHTTP2_ERR_CANCEL) \ + V(NGHTTP2_ERR_FATAL) \ + V(NGHTTP2_ERR_NOMEM) \ + V(NGHTTP2_ERR_CALLBACK_FAILURE) \ + V(NGHTTP2_ERR_BAD_CLIENT_MAGIC) \ + V(NGHTTP2_ERR_FLOODED) + +const char* nghttp2_errname(int rv) { + switch (rv) { +#define V(code) case code: return #code; + NGHTTP2_ERROR_CODES(V) +#undef V + default: + return "NGHTTP2_UNKNOWN_ERROR"; + } +} + +#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096 +#define DEFAULT_SETTINGS_ENABLE_PUSH 1 +#define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535 +#define DEFAULT_SETTINGS_MAX_FRAME_SIZE 16384 +#define MAX_MAX_FRAME_SIZE 16777215 +#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE +#define MAX_INITIAL_WINDOW_SIZE 2147483647 + +class Http2Options { + public: + explicit Http2Options(Environment* env); + + ~Http2Options() { + nghttp2_option_del(options_); + } + + nghttp2_option* operator*() { + return options_; + } + + void SetPaddingStrategy(uint32_t val) { + CHECK_LE(val, PADDING_STRATEGY_CALLBACK); + padding_strategy_ = static_cast(val); + } + + void SetMaxDeflateDynamicTableSize(size_t val) { + nghttp2_option_set_max_deflate_dynamic_table_size(options_, val); + } + + void SetMaxReservedRemoteStreams(uint32_t val) { + nghttp2_option_set_max_reserved_remote_streams(options_, val); + } + + void SetMaxSendHeaderBlockLength(size_t val) { + nghttp2_option_set_max_send_header_block_length(options_, val); + } + + void SetPeerMaxConcurrentStreams(uint32_t val) { + nghttp2_option_set_peer_max_concurrent_streams(options_, val); + } + + padding_strategy_type GetPaddingStrategy() { + return padding_strategy_; + } + + private: + nghttp2_option* options_; + padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; +}; + +static const size_t kAllocBufferSize = 64 * 1024; + +//// +typedef uint32_t(*get_setting)(nghttp2_session* session, + nghttp2_settings_id id); + +class Http2Session : public AsyncWrap, + public StreamBase, + public Nghttp2Session { + public: + Http2Session(Environment* env, + Local wrap, + nghttp2_session_type type) : + AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTP2SESSION), + StreamBase(env) { + Wrap(object(), this); + + Http2Options opts(env); + + padding_strategy_ = opts.GetPaddingStrategy(); + + Init(env->event_loop(), type, *opts); + stream_buf_.AllocateSufficientStorage(kAllocBufferSize); + } + + ~Http2Session() override { + CHECK_EQ(false, persistent().IsEmpty()); + ClearWrap(object()); + persistent().Reset(); + CHECK_EQ(true, persistent().IsEmpty()); + } + + static void OnStreamAllocImpl(size_t suggested_size, + uv_buf_t* buf, + void* ctx); + static void OnStreamReadImpl(ssize_t nread, + const uv_buf_t* bufs, + uv_handle_type pending, + void* ctx); + protected: + void OnFreeSession() override; + + ssize_t OnMaxFrameSizePadding(size_t frameLength, + size_t maxPayloadLen); + + ssize_t OnCallbackPadding(size_t frame, + size_t maxPayloadLen); + + bool HasGetPaddingCallback() override { + return padding_strategy_ == PADDING_STRATEGY_MAX || + padding_strategy_ == PADDING_STRATEGY_CALLBACK; + } + + ssize_t GetPadding(size_t frameLength, size_t maxPayloadLen) override { + if (padding_strategy_ == PADDING_STRATEGY_MAX) { + return OnMaxFrameSizePadding(frameLength, maxPayloadLen); + } + + CHECK_EQ(padding_strategy_, PADDING_STRATEGY_CALLBACK); + + return OnCallbackPadding(frameLength, maxPayloadLen); + } + + void OnHeaders(Nghttp2Stream* stream, + nghttp2_header_list* headers, + nghttp2_headers_category cat, + uint8_t flags) override; + void OnStreamClose(int32_t id, uint32_t code) override; + void Send(uv_buf_t* bufs, size_t total) override; + void OnDataChunk(Nghttp2Stream* stream, nghttp2_data_chunk_t* chunk) override; + void OnSettings(bool ack) override; + void OnPriority(int32_t stream, + int32_t parent, + int32_t weight, + int8_t exclusive) override; + void OnGoAway(int32_t lastStreamID, + uint32_t errorCode, + uint8_t* data, + size_t length) override; + void OnFrameError(int32_t id, uint8_t type, int error_code) override; + void OnTrailers(Nghttp2Stream* stream, + MaybeStackBuffer* trailers) override; + void AllocateSend(size_t recommended, uv_buf_t* buf) override; + + int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count, + uv_stream_t* send_handle) override; + + AsyncWrap* GetAsyncWrap() override { + return static_cast(this); + } + + void* Cast() override { + return reinterpret_cast(this); + } + + // Required for StreamBase + bool IsAlive() override { + return true; + } + + // Required for StreamBase + bool IsClosing() override { + return false; + } + + // Required for StreamBase + int ReadStart() override { return 0; } + + // Required for StreamBase + int ReadStop() override { return 0; } + + // Required for StreamBase + int DoShutdown(ShutdownWrap* req_wrap) override { + return 0; + } + + public: + void Consume(Local external); + void Unconsume(); + + static void New(const FunctionCallbackInfo& args); + static void Consume(const FunctionCallbackInfo& args); + static void Unconsume(const FunctionCallbackInfo& args); + static void Destroy(const FunctionCallbackInfo& args); + static void SubmitSettings(const FunctionCallbackInfo& args); + static void SubmitRstStream(const FunctionCallbackInfo& args); + static void SubmitResponse(const FunctionCallbackInfo& args); + static void SubmitFile(const FunctionCallbackInfo& args); + static void SubmitRequest(const FunctionCallbackInfo& args); + static void SubmitPushPromise(const FunctionCallbackInfo& args); + static void SubmitPriority(const FunctionCallbackInfo& args); + static void SendHeaders(const FunctionCallbackInfo& args); + static void ShutdownStream(const FunctionCallbackInfo& args); + static void StreamWrite(const FunctionCallbackInfo& args); + static void StreamReadStart(const FunctionCallbackInfo& args); + static void StreamReadStop(const FunctionCallbackInfo& args); + static void SetNextStreamID(const FunctionCallbackInfo& args); + static void SendShutdownNotice(const FunctionCallbackInfo& args); + static void SubmitGoaway(const FunctionCallbackInfo& args); + static void DestroyStream(const FunctionCallbackInfo& args); + + template + static void GetSettings(const FunctionCallbackInfo& args); + + size_t self_size() const override { + return sizeof(*this); + } + + char* stream_alloc() { + return *stream_buf_; + } + + private: + StreamBase* stream_; + StreamResource::Callback prev_alloc_cb_; + StreamResource::Callback prev_read_cb_; + padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; + MaybeStackBuffer stream_buf_; +}; + +class ExternalHeader : + public String::ExternalOneByteStringResource { + public: + explicit ExternalHeader(nghttp2_rcbuf* buf) + : buf_(buf), vec_(nghttp2_rcbuf_get_buf(buf)) { + } + + ~ExternalHeader() override { + nghttp2_rcbuf_decref(buf_); + buf_ = nullptr; + } + + const char* data() const override { + return const_cast(reinterpret_cast(vec_.base)); + } + + size_t length() const override { + return vec_.len; + } + + static Local New(Isolate* isolate, nghttp2_rcbuf* buf) { + EscapableHandleScope scope(isolate); + nghttp2_vec vec = nghttp2_rcbuf_get_buf(buf); + if (vec.len == 0) { + nghttp2_rcbuf_decref(buf); + return scope.Escape(String::Empty(isolate)); + } + + ExternalHeader* h_str = new ExternalHeader(buf); + MaybeLocal str = String::NewExternalOneByte(isolate, h_str); + isolate->AdjustAmountOfExternalAllocatedMemory(vec.len); + + if (str.IsEmpty()) { + delete h_str; + return scope.Escape(String::Empty(isolate)); + } + + return scope.Escape(str.ToLocalChecked()); + } + + private: + nghttp2_rcbuf* buf_; + nghttp2_vec vec_; +}; + +class Headers { + public: + Headers(Isolate* isolate, Local context, Local headers) { + headers_.AllocateSufficientStorage(headers->Length()); + Local item; + Local header; + + for (size_t n = 0; n < headers->Length(); n++) { + item = headers->Get(context, n).ToLocalChecked(); + CHECK(item->IsArray()); + header = item.As(); + Local key = header->Get(context, 0).ToLocalChecked(); + Local value = header->Get(context, 1).ToLocalChecked(); + CHECK(key->IsString()); + CHECK(value->IsString()); + size_t keylen = StringBytes::StorageSize(isolate, key, ASCII); + size_t valuelen = StringBytes::StorageSize(isolate, value, ASCII); + headers_[n].flags = NGHTTP2_NV_FLAG_NONE; + Local flag = header->Get(context, 2).ToLocalChecked(); + if (flag->BooleanValue(context).ToChecked()) + headers_[n].flags |= NGHTTP2_NV_FLAG_NO_INDEX; + uint8_t* buf = Malloc(keylen + valuelen); + headers_[n].name = buf; + headers_[n].value = buf + keylen; + headers_[n].namelen = + StringBytes::Write(isolate, + reinterpret_cast(headers_[n].name), + keylen, key, ASCII); + headers_[n].valuelen = + StringBytes::Write(isolate, + reinterpret_cast(headers_[n].value), + valuelen, value, ASCII); + } + } + + ~Headers() { + for (size_t n = 0; n < headers_.length(); n++) + free(headers_[n].name); + } + + nghttp2_nv* operator*() { + return *headers_; + } + + size_t length() const { + return headers_.length(); + } + + private: + MaybeStackBuffer headers_; +}; + +} // namespace http2 +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_HTTP2_H_ diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h new file mode 100644 index 00000000000000..49ec63b59bd581 --- /dev/null +++ b/src/node_http2_core-inl.h @@ -0,0 +1,590 @@ +#ifndef SRC_NODE_HTTP2_CORE_INL_H_ +#define SRC_NODE_HTTP2_CORE_INL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_http2_core.h" +#include "node_internals.h" // arraysize +#include "freelist.h" + +namespace node { +namespace http2 { + +#define FREELIST_MAX 1024 + +#define LINKED_LIST_ADD(list, item) \ + do { \ + if (list ## _tail_ == nullptr) { \ + list ## _head_ = item; \ + list ## _tail_ = item; \ + } else { \ + list ## _tail_->next = item; \ + list ## _tail_ = item; \ + } \ + } while (0); + +extern Freelist + data_chunk_free_list; + +extern Freelist stream_free_list; + +extern Freelist header_free_list; + +extern Freelist + data_chunks_free_list; + +// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html +inline void Nghttp2Session::SubmitShutdownNotice() { + DEBUG_HTTP2("Nghttp2Session %d: submitting shutdown notice\n", session_type_); + nghttp2_submit_shutdown_notice(session_); +} + +// Sends a SETTINGS frame on the current session +// Note that this *should* send a SETTINGS frame even if niv == 0 and there +// are no settings entries to send. +inline int Nghttp2Session::SubmitSettings(const nghttp2_settings_entry iv[], + size_t niv) { + DEBUG_HTTP2("Nghttp2Session %d: submitting settings, count: %d\n", + session_type_, niv); + return nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv); +} + +// Returns the Nghttp2Stream associated with the given id, or nullptr if none +inline Nghttp2Stream* Nghttp2Session::FindStream(int32_t id) { + auto s = streams_.find(id); + if (s != streams_.end()) { + DEBUG_HTTP2("Nghttp2Session %d: stream %d found\n", session_type_, id); + return s->second; + } else { + DEBUG_HTTP2("Nghttp2Session %d: stream %d not found\n", session_type_, id); + return nullptr; + } +} + +// Flushes any received queued chunks of data out to the JS layer +inline void Nghttp2Stream::FlushDataChunks(bool done) { + while (data_chunks_head_ != nullptr) { + DEBUG_HTTP2("Nghttp2Stream %d: emitting data chunk\n", id_); + nghttp2_data_chunk_t* item = data_chunks_head_; + data_chunks_head_ = item->next; + // item will be passed to the Buffer instance and freed on gc + session_->OnDataChunk(this, item); + } + data_chunks_tail_ = nullptr; + if (done) + session_->OnDataChunk(this, nullptr); +} + +// Passes all of the the chunks for a data frame out to the JS layer +// The chunks are collected as the frame is being processed and sent out +// to the JS side only when the frame is fully processed. +inline void Nghttp2Session::HandleDataFrame(const nghttp2_frame* frame) { + int32_t id = frame->hd.stream_id; + DEBUG_HTTP2("Nghttp2Session %d: handling data frame for stream %d\n", + session_type_, id); + Nghttp2Stream* stream = this->FindStream(id); + // If the stream does not exist, something really bad happened + CHECK_NE(stream, nullptr); + bool done = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == + NGHTTP2_FLAG_END_STREAM; + stream->FlushDataChunks(done); +} + +// Passes all of the collected headers for a HEADERS frame out to the JS layer. +// The headers are collected as the frame is being processed and sent out +// to the JS side only when the frame is fully processed. +inline void Nghttp2Session::HandleHeadersFrame(const nghttp2_frame* frame) { + int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? + frame->push_promise.promised_stream_id : frame->hd.stream_id; + DEBUG_HTTP2("Nghttp2Session %d: handling headers frame for stream %d\n", + session_type_, id); + Nghttp2Stream* stream = FindStream(id); + // If the stream does not exist, something really bad happened + CHECK_NE(stream, nullptr); + OnHeaders(stream, + stream->headers(), + stream->headers_category(), + frame->hd.flags); + stream->FreeHeaders(); +} + +// Notifies the JS layer that a PRIORITY frame has been received +inline void Nghttp2Session::HandlePriorityFrame(const nghttp2_frame* frame) { + nghttp2_priority priority_frame = frame->priority; + int32_t id = frame->hd.stream_id; + DEBUG_HTTP2("Nghttp2Session %d: handling priority frame for stream %d\n", + session_type_, id); + // Ignore the priority frame if stream ID is <= 0 + // This actually should never happen because nghttp2 should treat this as + // an error condition that terminates the session. + if (id > 0) { + nghttp2_priority_spec spec = priority_frame.pri_spec; + OnPriority(id, spec.stream_id, spec.weight, spec.exclusive); + } +} + +// Notifies the JS layer that a GOAWAY frame has been received +inline void Nghttp2Session::HandleGoawayFrame(const nghttp2_frame* frame) { + nghttp2_goaway goaway_frame = frame->goaway; + DEBUG_HTTP2("Nghttp2Session %d: handling goaway frame\n", session_type_); + + OnGoAway(goaway_frame.last_stream_id, + goaway_frame.error_code, + goaway_frame.opaque_data, + goaway_frame.opaque_data_len); +} + +// Prompts nghttp2 to flush the queue of pending data frames +inline void Nghttp2Session::SendPendingData() { + const uint8_t* data; + ssize_t len = 0; + size_t ncopy = 0; + uv_buf_t buf; + AllocateSend(SEND_BUFFER_RECOMMENDED_SIZE, &buf); + while (nghttp2_session_want_write(session_)) { + len = nghttp2_session_mem_send(session_, &data); + CHECK_GE(len, 0); // If this is less than zero, we're out of memory + // While len is greater than 0, send a chunk + while (len > 0) { + ncopy = len; + if (ncopy > buf.len) + ncopy = buf.len; + memcpy(buf.base, data, ncopy); + Send(&buf, ncopy); + len -= ncopy; + CHECK_GE(len, 0); // This should never be less than zero + } + } +} + +// Initialize the Nghttp2Session handle by creating and +// assigning the Nghttp2Session instance and associated +// uv_loop_t. +inline int Nghttp2Session::Init(uv_loop_t* loop, + const nghttp2_session_type type, + nghttp2_option* options, + nghttp2_mem* mem) { + DEBUG_HTTP2("Nghttp2Session %d: initializing session\n", type); + loop_ = loop; + session_type_ = type; + int ret = 0; + + nghttp2_session_callbacks* callbacks + = callback_struct_saved[HasGetPaddingCallback() ? 1 : 0].callbacks; + + nghttp2_option* opts; + if (options != nullptr) { + opts = options; + } else { + nghttp2_option_new(&opts); + } + + switch (type) { + case NGHTTP2_SESSION_SERVER: + ret = nghttp2_session_server_new3(&session_, + callbacks, + this, + opts, + mem); + break; + case NGHTTP2_SESSION_CLIENT: + ret = nghttp2_session_client_new3(&session_, + callbacks, + this, + opts, + mem); + break; + } + if (opts != options) { + nghttp2_option_del(opts); + } + + // For every node::Http2Session instance, there is a uv_prep_t handle + // whose callback is triggered on every tick of the event loop. When + // run, nghttp2 is prompted to send any queued data it may have stored. + uv_prepare_init(loop_, &prep_); + uv_prepare_start(&prep_, [](uv_prepare_t* t) { + Nghttp2Session* session = ContainerOf(&Nghttp2Session::prep_, t); + session->SendPendingData(); + }); +// uv_unref(reinterpret_cast(&prep_)); + return ret; +} + + +inline int Nghttp2Session::Free() { + assert(session_ != nullptr); + DEBUG_HTTP2("Nghttp2Session %d: freeing session\n", session_type_); + // Stop the loop + uv_prepare_stop(&prep_); + auto PrepClose = [](uv_handle_t* handle) { + Nghttp2Session* session = + ContainerOf(&Nghttp2Session::prep_, + reinterpret_cast(handle)); + + session->OnFreeSession(); + DEBUG_HTTP2("Nghttp2Session %d: session is free\n", + session->session_type_); + }; + uv_close(reinterpret_cast(&prep_), PrepClose); + + nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); + nghttp2_session_del(session_); + session_ = nullptr; + loop_ = nullptr; + return 1; +} + +// Write data received from the socket to the underlying nghttp2_session. +inline ssize_t Nghttp2Session::Write(const uv_buf_t* bufs, unsigned int nbufs) { + size_t total = 0; + for (unsigned int n = 0; n < nbufs; n++) { + ssize_t ret = + nghttp2_session_mem_recv(session_, + reinterpret_cast(bufs[n].base), + bufs[n].len); + if (ret < 0) { + return ret; + } else { + total += ret; + } + } + SendPendingData(); + return total; +} + +inline void Nghttp2Session::AddStream(Nghttp2Stream* stream) { + streams_[stream->id()] = stream; +} + +// Removes a stream instance from this session +inline void Nghttp2Session::RemoveStream(int32_t id) { + streams_.erase(id); +} + +// Implementation for Nghttp2Stream functions + +inline Nghttp2Stream* Nghttp2Stream::Init( + int32_t id, + Nghttp2Session* session, + nghttp2_headers_category category) { + DEBUG_HTTP2("Nghttp2Stream %d: initializing stream\n", id); + Nghttp2Stream* stream = stream_free_list.pop(); + stream->ResetState(id, session, category); + session->AddStream(stream); + return stream; +} + + +// Resets the state of the stream instance to defaults +inline void Nghttp2Stream::ResetState( + int32_t id, + Nghttp2Session* session, + nghttp2_headers_category category) { + DEBUG_HTTP2("Nghttp2Stream %d: resetting stream state\n", id); + session_ = session; + queue_head_ = nullptr; + queue_tail_ = nullptr; + data_chunks_head_ = nullptr; + data_chunks_tail_ = nullptr; + current_headers_head_ = nullptr; + current_headers_tail_ = nullptr; + current_headers_category_ = category; + flags_ = NGHTTP2_STREAM_FLAG_NONE; + id_ = id; + code_ = NGHTTP2_NO_ERROR; + prev_local_window_size_ = 65535; + queue_head_index_ = 0; + queue_head_offset_ = 0; +} + + +inline void Nghttp2Stream::Destroy() { + DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_); + // Do nothing if this stream instance is already destroyed + if (IsDestroyed() || IsDestroying()) + return; + flags_ |= NGHTTP2_STREAM_DESTROYING; + Nghttp2Session* session = this->session_; + + if (session != nullptr) { + // Remove this stream from the associated session + session_->RemoveStream(this->id()); + session_ = nullptr; + } + + // Free any remaining incoming data chunks. + while (data_chunks_head_ != nullptr) { + nghttp2_data_chunk_t* chunk = data_chunks_head_; + data_chunks_head_ = chunk->next; + delete[] chunk->buf.base; + data_chunk_free_list.push(chunk); + } + data_chunks_tail_ = nullptr; + + // Free any remaining outgoing data chunks. + while (queue_head_ != nullptr) { + nghttp2_stream_write_queue* head = queue_head_; + queue_head_ = head->next; + head->cb(head->req, UV_ECANCELED); + delete head; + } + queue_tail_ = nullptr; + + // Free any remaining headers + FreeHeaders(); + + // Return this stream instance to the freelist + stream_free_list.push(this); +} + +inline void Nghttp2Stream::FreeHeaders() { + DEBUG_HTTP2("Nghttp2Stream %d: freeing headers\n", id_); + while (current_headers_head_ != nullptr) { + DEBUG_HTTP2("Nghttp2Stream %d: freeing header item\n", id_); + nghttp2_header_list* item = current_headers_head_; + current_headers_head_ = item->next; + header_free_list.push(item); + } + current_headers_tail_ = nullptr; +} + +// Submit informational headers for a stream. +inline int Nghttp2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { + DEBUG_HTTP2("Nghttp2Stream %d: sending informational headers, count: %d\n", + id_, len); + CHECK_GT(len, 0); + return nghttp2_submit_headers(session_->session(), + NGHTTP2_FLAG_NONE, + id_, nullptr, + nva, len, nullptr); +} + +inline int Nghttp2Stream::SubmitPriority(nghttp2_priority_spec* prispec, + bool silent) { + DEBUG_HTTP2("Nghttp2Stream %d: sending priority spec\n", id_); + return silent ? + nghttp2_session_change_stream_priority(session_->session(), + id_, prispec) : + nghttp2_submit_priority(session_->session(), + NGHTTP2_FLAG_NONE, + id_, prispec); +} + +// Submit an RST_STREAM frame +inline int Nghttp2Stream::SubmitRstStream(const uint32_t code) { + DEBUG_HTTP2("Nghttp2Stream %d: sending rst-stream, code: %d\n", id_, code); + session_->SendPendingData(); + return nghttp2_submit_rst_stream(session_->session(), + NGHTTP2_FLAG_NONE, + id_, + code); +} + +// Submit a push promise. +inline int32_t Nghttp2Stream::SubmitPushPromise( + nghttp2_nv* nva, + size_t len, + Nghttp2Stream** assigned, + bool emptyPayload) { + CHECK_GT(len, 0); + DEBUG_HTTP2("Nghttp2Stream %d: sending push promise\n", id_); + int32_t ret = nghttp2_submit_push_promise(session_->session(), + NGHTTP2_FLAG_NONE, + id_, nva, len, + nullptr); + if (ret > 0) { + auto stream = Nghttp2Stream::Init(ret, session_); + if (emptyPayload) stream->Shutdown(); + if (assigned != nullptr) *assigned = stream; + } + return ret; +} + +// Initiate a response. If the nghttp2_stream is still writable by +// the time this is called, then an nghttp2_data_provider will be +// initialized, causing at least one (possibly empty) data frame to +// be sent. +inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva, + size_t len, + bool emptyPayload) { + CHECK_GT(len, 0); + DEBUG_HTTP2("Nghttp2Stream %d: submitting response\n", id_); + nghttp2_data_provider* provider = nullptr; + nghttp2_data_provider prov; + prov.source.ptr = this; + prov.read_callback = Nghttp2Session::OnStreamRead; + if (!emptyPayload && IsWritable()) + provider = &prov; + + return nghttp2_submit_response(session_->session(), id_, + nva, len, provider); +} + +// Initiate a response that contains data read from a file descriptor. +inline int Nghttp2Stream::SubmitFile(int fd, nghttp2_nv* nva, size_t len) { + CHECK_GT(len, 0); + CHECK_GT(fd, 0); + DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_); + nghttp2_data_provider prov; + prov.source.ptr = this; + prov.source.fd = fd; + prov.read_callback = Nghttp2Session::OnStreamReadFD; + + return nghttp2_submit_response(session_->session(), id_, + nva, len, &prov); +} + +// Initiate a request. If writable is true (the default), then +// an nghttp2_data_provider will be initialized, causing at +// least one (possibly empty) data frame to to be sent. +inline int32_t Nghttp2Session::SubmitRequest( + nghttp2_priority_spec* prispec, + nghttp2_nv* nva, + size_t len, + Nghttp2Stream** assigned, + bool emptyPayload) { + CHECK_GT(len, 0); + DEBUG_HTTP2("Nghttp2Session: submitting request\n"); + nghttp2_data_provider* provider = nullptr; + nghttp2_data_provider prov; + prov.source.ptr = this; + prov.read_callback = OnStreamRead; + if (!emptyPayload) + provider = &prov; + int32_t ret = nghttp2_submit_request(session_, + prispec, nva, len, + provider, nullptr); + // Assign the Nghttp2Stream handle + if (ret > 0) { + Nghttp2Stream* stream = Nghttp2Stream::Init(ret, this); + if (emptyPayload) stream->Shutdown(); + if (assigned != nullptr) *assigned = stream; + } + return ret; +} + +// Queue the given set of uv_but_t handles for writing to an +// nghttp2_stream. The callback will be invoked once the chunks +// of data have been flushed to the underlying nghttp2_session. +// Note that this does *not* mean that the data has been flushed +// to the socket yet. +inline int Nghttp2Stream::Write(nghttp2_stream_write_t* req, + const uv_buf_t bufs[], + unsigned int nbufs, + nghttp2_stream_write_cb cb) { + if (!IsWritable()) { + if (cb != nullptr) + cb(req, UV_EOF); + return 0; + } + DEBUG_HTTP2("Nghttp2Stream %d: queuing buffers to send, count: %d\n", + id_, nbufs); + nghttp2_stream_write_queue* item = new nghttp2_stream_write_queue; + item->cb = cb; + item->req = req; + item->nbufs = nbufs; + item->bufs.AllocateSufficientStorage(nbufs); + req->handle = this; + req->item = item; + memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs)); + + if (queue_head_ == nullptr) { + queue_head_ = item; + queue_tail_ = item; + } else { + queue_tail_->next = item; + queue_tail_ = item; + } + nghttp2_session_resume_data(session_->session(), id_); + return 0; +} + +inline void Nghttp2Stream::ReadStart() { + // Has no effect if IsReading() is true. + if (IsReading()) + return; + DEBUG_HTTP2("Nghttp2Stream %d: start reading\n", id_); + if (IsPaused()) { + // If handle->reading is less than zero, read_start had never previously + // been called. If handle->reading is zero, reading had started and read + // stop had been previously called, meaning that the flow control window + // has been explicitly set to zero. Reset the flow control window now to + // restart the flow of data. + nghttp2_session_set_local_window_size(session_->session(), + NGHTTP2_FLAG_NONE, + id_, + prev_local_window_size_); + } + flags_ |= NGHTTP2_STREAM_READ_START; + flags_ &= ~NGHTTP2_STREAM_READ_PAUSED; + + // Flush any queued data chunks immediately out to the JS layer + FlushDataChunks(); +} + +inline void Nghttp2Stream::ReadStop() { + DEBUG_HTTP2("Nghttp2Stream %d: stop reading\n", id_); + // Has no effect if IsReading() is false, which will happen if we either + // have not started reading yet at all (NGHTTP2_STREAM_READ_START is not + // set) or if we're already paused (NGHTTP2_STREAM_READ_PAUSED is set. + if (!IsReading()) + return; + flags_ |= NGHTTP2_STREAM_READ_PAUSED; + + // When not reading, explicitly set the local window size to 0 so that + // the peer does not keep sending data that has to be buffered + int32_t ret = + nghttp2_session_get_stream_local_window_size(session_->session(), id_); + if (ret >= 0) + prev_local_window_size_ = ret; + nghttp2_session_set_local_window_size(session_->session(), + NGHTTP2_FLAG_NONE, + id_, 0); +} + +nghttp2_data_chunks_t::~nghttp2_data_chunks_t() { + for (unsigned int n = 0; n < nbufs; n++) { + free(buf[n].base); + } +} + +Nghttp2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) { + nghttp2_session_callbacks_new(&callbacks); + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, OnBeginHeadersCallback); + nghttp2_session_callbacks_set_on_header_callback2( + callbacks, OnHeaderCallback); + nghttp2_session_callbacks_set_on_frame_recv_callback( + callbacks, OnFrameReceive); + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, OnStreamClose); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, OnDataChunkReceived); + nghttp2_session_callbacks_set_on_frame_not_send_callback( + callbacks, OnFrameNotSent); + + // nghttp2_session_callbacks_set_on_invalid_frame_recv( + // callbacks, OnInvalidFrameReceived); + +#ifdef NODE_DEBUG_HTTP2 + nghttp2_session_callbacks_set_error_callback( + callbacks, OnNghttpError); +#endif + + if (kHasGetPaddingCallback) { + nghttp2_session_callbacks_set_select_padding_callback( + callbacks, OnSelectPadding); + } +} + +Nghttp2Session::Callbacks::~Callbacks() { + nghttp2_session_callbacks_del(callbacks); +} + +} // namespace http2 +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_HTTP2_CORE_INL_H_ diff --git a/src/node_http2_core.cc b/src/node_http2_core.cc new file mode 100644 index 00000000000000..4d9ab4a4dfa965 --- /dev/null +++ b/src/node_http2_core.cc @@ -0,0 +1,326 @@ +#include "node_http2_core-inl.h" + +namespace node { +namespace http2 { + +#ifdef NODE_DEBUG_HTTP2 +int Nghttp2Session::OnNghttpError(nghttp2_session* session, + const char* message, + size_t len, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + DEBUG_HTTP2("Nghttp2Session %d: Error '%.*s'\n", + handle->session_type_, len, message); + return 0; +} +#endif + +// nghttp2 calls this at the beginning a new HEADERS or PUSH_PROMISE frame. +// We use it to ensure that an Nghttp2Stream instance is allocated to store +// the state. +int Nghttp2Session::OnBeginHeadersCallback(nghttp2_session* session, + const nghttp2_frame* frame, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? + frame->push_promise.promised_stream_id : + frame->hd.stream_id; + DEBUG_HTTP2("Nghttp2Session %d: beginning headers for stream %d\n", + handle->session_type_, id); + + Nghttp2Stream* stream = handle->FindStream(id); + if (stream == nullptr) { + Nghttp2Stream::Init(id, handle, frame->headers.cat); + } else { + stream->StartHeaders(frame->headers.cat); + } + return 0; +} + +// nghttp2 calls this once for every header name-value pair in a HEADERS +// or PUSH_PROMISE block. CONTINUATION frames are handled automatically +// and transparently so we do not need to worry about those at all. +int Nghttp2Session::OnHeaderCallback(nghttp2_session* session, + const nghttp2_frame* frame, + nghttp2_rcbuf *name, + nghttp2_rcbuf *value, + uint8_t flags, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? + frame->push_promise.promised_stream_id : + frame->hd.stream_id; + Nghttp2Stream* stream = handle->FindStream(id); + nghttp2_header_list* header = header_free_list.pop(); + header->name = name; + header->value = value; + nghttp2_rcbuf_incref(name); + nghttp2_rcbuf_incref(value); + LINKED_LIST_ADD(stream->current_headers, header); + return 0; +} + +// When nghttp2 has completely processed a frame, it calls OnFrameReceive. +// It is our responsibility to delegate out from there. We can ignore most +// control frames since nghttp2 will handle those for us. +int Nghttp2Session::OnFrameReceive(nghttp2_session* session, + const nghttp2_frame* frame, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + DEBUG_HTTP2("Nghttp2Session %d: complete frame received: type: %d\n", + handle->session_type_, frame->hd.type); + bool ack; + switch (frame->hd.type) { + case NGHTTP2_DATA: + handle->HandleDataFrame(frame); + break; + case NGHTTP2_PUSH_PROMISE: + case NGHTTP2_HEADERS: + handle->HandleHeadersFrame(frame); + break; + case NGHTTP2_SETTINGS: + ack = (frame->hd.flags & NGHTTP2_FLAG_ACK) == NGHTTP2_FLAG_ACK; + handle->OnSettings(ack); + break; + case NGHTTP2_PRIORITY: + handle->HandlePriorityFrame(frame); + break; + case NGHTTP2_GOAWAY: + handle->HandleGoawayFrame(frame); + break; + default: + break; + } + return 0; +} + +int Nghttp2Session::OnFrameNotSent(nghttp2_session* session, + const nghttp2_frame* frame, + int error_code, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + DEBUG_HTTP2("Nghttp2Session %d: frame type %d was not sent, code: %d\n", + handle->session_type_, frame->hd.type, error_code); + // Do not report if the frame was not sent due to the session closing + if (error_code != NGHTTP2_ERR_SESSION_CLOSING && + error_code != NGHTTP2_ERR_STREAM_CLOSED && + error_code != NGHTTP2_ERR_STREAM_CLOSING) + handle->OnFrameError(frame->hd.stream_id, frame->hd.type, error_code); + return 0; +} + +// Called when nghttp2 closes a stream, either in response to an RST_STREAM +// frame or the stream closing naturally on it's own +int Nghttp2Session::OnStreamClose(nghttp2_session *session, + int32_t id, + uint32_t code, + void *user_data) { + Nghttp2Session* handle = static_cast(user_data); + DEBUG_HTTP2("Nghttp2Session %d: stream %d closed, code: %d\n", + handle->session_type_, id, code); + Nghttp2Stream* stream = handle->FindStream(id); + // Intentionally ignore the callback if the stream does not exist + if (stream != nullptr) + stream->Close(code); + return 0; +} + +// Called by nghttp2 multiple times while processing a DATA frame +int Nghttp2Session::OnDataChunkReceived(nghttp2_session *session, + uint8_t flags, + int32_t id, + const uint8_t *data, + size_t len, + void *user_data) { + Nghttp2Session* handle = static_cast(user_data); + DEBUG_HTTP2("Nghttp2Session %d: buffering data chunk for stream %d, size: " + "%d, flags: %d\n", handle->session_type_, id, len, flags); + Nghttp2Stream* stream = handle->FindStream(id); + nghttp2_data_chunk_t* chunk = data_chunk_free_list.pop(); + chunk->buf = uv_buf_init(new char[len], len); + memcpy(chunk->buf.base, data, len); + if (stream->data_chunks_tail_ == nullptr) { + stream->data_chunks_head_ = + stream->data_chunks_tail_ = chunk; + } else { + stream->data_chunks_tail_->next = chunk; + stream->data_chunks_tail_ = chunk; + } + return 0; +} + +// Called by nghttp2 when it needs to determine how much padding to apply +// to a DATA or HEADERS frame +ssize_t Nghttp2Session::OnSelectPadding(nghttp2_session* session, + const nghttp2_frame* frame, + size_t maxPayloadLen, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + assert(handle->HasGetPaddingCallback()); + ssize_t padding = handle->GetPadding(frame->hd.length, maxPayloadLen); + DEBUG_HTTP2("Nghttp2Session %d: using padding, size: %d\n", + handle->session_type_, padding); + return padding; +} + +// Called by nghttp2 to collect the data while a file response is sent. +// The buf is the DATA frame buffer that needs to be filled with at most +// length bytes. flags is used to control what nghttp2 does next. +ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + DEBUG_HTTP2("Nghttp2Session %d: reading outbound file data for stream %d\n", + handle->session_type_, id); + Nghttp2Stream* stream = handle->FindStream(id); + + int fd = source->fd; + int64_t offset = stream->fd_offset_; + ssize_t numchars; + + uv_buf_t data; + data.base = reinterpret_cast(buf); + data.len = length; + + uv_fs_t read_req; + numchars = uv_fs_read(handle->loop_, + &read_req, + fd, &data, 1, + offset, nullptr); + uv_fs_req_cleanup(&read_req); + + // Close the stream with an error if reading fails + if (numchars < 0) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + + // Update the read offset for the next read + stream->fd_offset_ += numchars; + + // if numchars < length, assume that we are done. + if (static_cast(numchars) < length) { + DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n", + handle->session_type_, id); + *flags |= NGHTTP2_DATA_FLAG_EOF; + // Sending trailers is not permitted with this provider. + } + + return numchars; +} + +// Called by nghttp2 to collect the data to pack within a DATA frame. +// The buf is the DATA frame buffer that needs to be filled with at most +// length bytes. flags is used to control what nghttp2 does next. +ssize_t Nghttp2Session::OnStreamRead(nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data) { + Nghttp2Session* handle = static_cast(user_data); + DEBUG_HTTP2("Nghttp2Session %d: reading outbound data for stream %d\n", + handle->session_type_, id); + Nghttp2Stream* stream = handle->FindStream(id); + size_t remaining = length; + size_t offset = 0; + + // While there is data in the queue, copy data into buf until it is full. + // There may be data left over, which will be sent the next time nghttp + // calls this callback. + while (stream->queue_head_ != nullptr) { + DEBUG_HTTP2("Nghttp2Session %d: processing outbound data chunk\n", + handle->session_type_); + nghttp2_stream_write_queue* head = stream->queue_head_; + while (stream->queue_head_index_ < head->nbufs) { + if (remaining == 0) { + goto end; + } + + unsigned int n = stream->queue_head_index_; + // len is the number of bytes in head->bufs[n] that are yet to be written + size_t len = head->bufs[n].len - stream->queue_head_offset_; + size_t bytes_to_write = len < remaining ? len : remaining; + memcpy(buf + offset, + head->bufs[n].base + stream->queue_head_offset_, + bytes_to_write); + offset += bytes_to_write; + remaining -= bytes_to_write; + if (bytes_to_write < len) { + stream->queue_head_offset_ += bytes_to_write; + } else { + stream->queue_head_index_++; + stream->queue_head_offset_ = 0; + } + } + stream->queue_head_offset_ = 0; + stream->queue_head_index_ = 0; + stream->queue_head_ = head->next; + head->cb(head->req, 0); + delete head; + } + stream->queue_tail_ = nullptr; + + end: + // If we are no longer writable and there is no more data in the queue, + // then we need to set the NGHTTP2_DATA_FLAG_EOF flag. + // If we are still writable but there is not yet any data to send, set the + // NGHTTP2_ERR_DEFERRED flag. This will put the stream into a pending state + // that will wait for data to become available. + // If neither of these flags are set, then nghttp2 will call this callback + // again to get the data for the next DATA frame. + int writable = stream->queue_head_ != nullptr || stream->IsWritable(); + if (offset == 0 && writable && stream->queue_head_ == nullptr) { + DEBUG_HTTP2("Nghttp2Session %d: deferring stream %d\n", + handle->session_type_, id); + return NGHTTP2_ERR_DEFERRED; + } + if (!writable) { + DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n", + handle->session_type_, id); + *flags |= NGHTTP2_DATA_FLAG_EOF; + + // Only when we are done sending the last chunk of data do we check for + // any trailing headers that are to be sent. This is the only opportunity + // we have to make this check. If there are trailers, then the + // NGHTTP2_DATA_FLAG_NO_END_STREAM flag must be set. + MaybeStackBuffer trailers; + handle->OnTrailers(stream, &trailers); + if (trailers.length() > 0) { + DEBUG_HTTP2("Nghttp2Session %d: sending trailers for stream %d, " + "count: %d\n", handle->session_type_, id, trailers.length()); + *flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + nghttp2_submit_trailer(session, + stream->id(), + *trailers, + trailers.length()); + } + for (size_t n = 0; n < trailers.length(); n++) { + free(trailers[n].name); + free(trailers[n].value); + } + } + assert(offset <= length); + return offset; +} + +Freelist + data_chunk_free_list; + +Freelist stream_free_list; + +Freelist header_free_list; + +Freelist + data_chunks_free_list; + +Nghttp2Session::Callbacks Nghttp2Session::callback_struct_saved[2] = { + Callbacks(false), + Callbacks(true) +}; + +} // namespace http2 +} // namespace node diff --git a/src/node_http2_core.h b/src/node_http2_core.h new file mode 100644 index 00000000000000..10acd7736b419f --- /dev/null +++ b/src/node_http2_core.h @@ -0,0 +1,465 @@ +#ifndef SRC_NODE_HTTP2_CORE_H_ +#define SRC_NODE_HTTP2_CORE_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "util.h" +#include "util-inl.h" +#include "uv.h" +#include "nghttp2/nghttp2.h" + +#include +#include + +namespace node { +namespace http2 { + +#ifdef NODE_DEBUG_HTTP2 + +// Adapted from nghttp2 own debug printer +static inline void _debug_vfprintf(const char *fmt, va_list args) { + vfprintf(stderr, fmt, args); +} + +void inline debug_vfprintf(const char *format, ...) { + va_list args; + va_start(args, format); + _debug_vfprintf(format, args); + va_end(args); +} + +#define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__); +#else +#define DEBUG_HTTP2(...) \ + do { \ + } while (0) +#endif + +class Nghttp2Session; +class Nghttp2Stream; + +struct nghttp2_stream_write_t; +struct nghttp2_data_chunk_t; +struct nghttp2_data_chunks_t; + +#define MAX_BUFFER_COUNT 10 +#define SEND_BUFFER_RECOMMENDED_SIZE 4096 + +enum nghttp2_session_type { + NGHTTP2_SESSION_SERVER, + NGHTTP2_SESSION_CLIENT +}; + +enum nghttp2_shutdown_flags { + NGHTTP2_SHUTDOWN_FLAG_GRACEFUL +}; + +enum nghttp2_stream_flags { + NGHTTP2_STREAM_FLAG_NONE = 0x0, + // Writable side has ended + NGHTTP2_STREAM_FLAG_SHUT = 0x1, + // Reading has started + NGHTTP2_STREAM_READ_START = 0x2, + // Reading is paused + NGHTTP2_STREAM_READ_PAUSED = 0x4, + // Stream is closed + NGHTTP2_STREAM_CLOSED = 0x8, + // Stream is destroyed + NGHTTP2_STREAM_DESTROYED = 0x10, + // Stream is being destroyed + NGHTTP2_STREAM_DESTROYING = 0x20 +}; + + +// Callbacks +typedef void (*nghttp2_stream_write_cb)( + nghttp2_stream_write_t* req, + int status); + +struct nghttp2_stream_write_queue { + unsigned int nbufs = 0; + nghttp2_stream_write_t* req = nullptr; + nghttp2_stream_write_cb cb = nullptr; + nghttp2_stream_write_queue* next = nullptr; + MaybeStackBuffer bufs; +}; + +struct nghttp2_header_list { + nghttp2_rcbuf* name = nullptr; + nghttp2_rcbuf* value = nullptr; + nghttp2_header_list* next = nullptr; +}; + +// Handle Types +class Nghttp2Session { + public: + // Initializes the session instance + inline int Init( + uv_loop_t*, + const nghttp2_session_type type = NGHTTP2_SESSION_SERVER, + nghttp2_option* options = nullptr, + nghttp2_mem* mem = nullptr); + + // Frees this session instance + inline int Free(); + + // Returns the pointer to the identified stream, or nullptr if + // the stream does not exist + inline Nghttp2Stream* FindStream(int32_t id); + + // Submits a new request. If the request is a success, assigned + // will be a pointer to the Nghttp2Stream instance assigned. + // This only works if the session is a client session. + inline int32_t SubmitRequest( + nghttp2_priority_spec* prispec, + nghttp2_nv* nva, + size_t len, + Nghttp2Stream** assigned = nullptr, + bool emptyPayload = true); + + // Submits a notice to the connected peer that the session is in the + // process of shutting down. + inline void SubmitShutdownNotice(); + + // Submits a SETTINGS frame to the connected peer. + inline int SubmitSettings(const nghttp2_settings_entry iv[], size_t niv); + + // Write data to the session + inline ssize_t Write(const uv_buf_t* bufs, unsigned int nbufs); + + // Returns the nghttp2 library session + inline nghttp2_session* session() { return session_; } + + protected: + // Adds a stream instance to this session + inline void AddStream(Nghttp2Stream* stream); + + // Removes a stream instance from this session + inline void RemoveStream(int32_t id); + + virtual void Send(uv_buf_t* buf, + size_t length) {} + virtual void OnHeaders(Nghttp2Stream* stream, + nghttp2_header_list* headers, + nghttp2_headers_category cat, + uint8_t flags) {} + virtual void OnStreamClose(int32_t id, uint32_t code) {} + virtual void OnDataChunk(Nghttp2Stream* stream, + nghttp2_data_chunk_t* chunk) {} + virtual void OnSettings(bool ack) {} + virtual void OnPriority(int32_t id, + int32_t parent, + int32_t weight, + int8_t exclusive) {} + virtual void OnGoAway(int32_t lastStreamID, + uint32_t errorCode, + uint8_t* data, + size_t length) {} + virtual void OnFrameError(int32_t id, + uint8_t type, + int error_code) {} + virtual ssize_t GetPadding(size_t frameLength, + size_t maxFrameLength) { return 0; } + virtual void OnTrailers(Nghttp2Stream* stream, + MaybeStackBuffer* nva) {} + virtual void OnFreeSession() {} + virtual void AllocateSend(size_t suggested_size, uv_buf_t* buf) = 0; + + virtual bool HasGetPaddingCallback() { return false; } + + private: + inline void SendPendingData(); + inline void HandleHeadersFrame(const nghttp2_frame* frame); + inline void HandlePriorityFrame(const nghttp2_frame* frame); + inline void HandleDataFrame(const nghttp2_frame* frame); + inline void HandleGoawayFrame(const nghttp2_frame* frame); + + /* callbacks for nghttp2 */ +#ifdef NODE_DEBUG_HTTP2 + static int OnNghttpError(nghttp2_session* session, + const char* message, + size_t len, + void* user_data); +#endif + + static int OnBeginHeadersCallback(nghttp2_session* session, + const nghttp2_frame* frame, + void* user_data); + static int OnHeaderCallback(nghttp2_session* session, + const nghttp2_frame* frame, + nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags, + void* user_data); + static int OnFrameReceive(nghttp2_session* session, + const nghttp2_frame* frame, + void* user_data); + static int OnFrameNotSent(nghttp2_session* session, + const nghttp2_frame* frame, + int error_code, + void* user_data); + static int OnStreamClose(nghttp2_session* session, + int32_t id, + uint32_t code, + void* user_data); + static int OnDataChunkReceived(nghttp2_session* session, + uint8_t flags, + int32_t id, + const uint8_t *data, + size_t len, + void* user_data); + static ssize_t OnStreamReadFD(nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data); + static ssize_t OnStreamRead(nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data); + static ssize_t OnSelectPadding(nghttp2_session* session, + const nghttp2_frame* frame, + size_t maxPayloadLen, + void* user_data); + + struct Callbacks { + inline explicit Callbacks(bool kHasGetPaddingCallback); + inline ~Callbacks(); + + nghttp2_session_callbacks* callbacks; + }; + + /* Use callback_struct_saved[kHasGetPaddingCallback ? 1 : 0] */ + static Callbacks callback_struct_saved[2]; + + nghttp2_session* session_; + uv_loop_t* loop_; + uv_prepare_t prep_; + nghttp2_session_type session_type_; + std::unordered_map streams_; + + friend class Nghttp2Stream; +}; + + + +class Nghttp2Stream { + public: + static inline Nghttp2Stream* Init( + int32_t id, + Nghttp2Session* session, + nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS); + + inline ~Nghttp2Stream() { + CHECK_EQ(session_, nullptr); + CHECK_EQ(queue_head_, nullptr); + CHECK_EQ(queue_tail_, nullptr); + CHECK_EQ(data_chunks_head_, nullptr); + CHECK_EQ(data_chunks_tail_, nullptr); + CHECK_EQ(current_headers_head_, nullptr); + CHECK_EQ(current_headers_tail_, nullptr); + DEBUG_HTTP2("Nghttp2Stream %d: freed\n", id_); + } + + inline void FlushDataChunks(bool done = false); + + // Resets the state of the stream instance to defaults + inline void ResetState( + int32_t id, + Nghttp2Session* session, + nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS); + + // Destroy this stream instance and free all held memory. + // Note that this will free queued outbound and inbound + // data chunks and inbound headers, so it's important not + // to call this until those are fully consumed. + // + // Also note: this does not actually destroy the instance. + // instead, it frees the held memory, removes the stream + // from the parent session, and returns the instance to + // the FreeList so that it can be reused. + inline void Destroy(); + + // Returns true if this stream has been destroyed + inline bool IsDestroyed() const { + return (flags_ & NGHTTP2_STREAM_DESTROYED) == NGHTTP2_STREAM_DESTROYED; + } + + inline bool IsDestroying() const { + return (flags_ & NGHTTP2_STREAM_DESTROYING) == NGHTTP2_STREAM_DESTROYING; + } + + // Queue outbound chunks of data to be sent on this stream + inline int Write( + nghttp2_stream_write_t* req, + const uv_buf_t bufs[], + unsigned int nbufs, + nghttp2_stream_write_cb cb); + + // Initiate a response on this stream. + inline int SubmitResponse(nghttp2_nv* nva, + size_t len, + bool emptyPayload = false); + + // Send data read from a file descriptor as the response on this stream. + inline int SubmitFile(int fd, nghttp2_nv* nva, size_t len); + + // Submit informational headers for this stream + inline int SubmitInfo(nghttp2_nv* nva, size_t len); + + // Submit a PRIORITY frame for this stream + inline int SubmitPriority(nghttp2_priority_spec* prispec, + bool silent = false); + + // Submits an RST_STREAM frame using the given code + inline int SubmitRstStream(const uint32_t code); + + // Submits a PUSH_PROMISE frame with this stream as the parent. + inline int SubmitPushPromise( + nghttp2_nv* nva, + size_t len, + Nghttp2Stream** assigned = nullptr, + bool writable = true); + + // Marks the Writable side of the stream as being shutdown + inline void Shutdown() { + flags_ |= NGHTTP2_STREAM_FLAG_SHUT; + nghttp2_session_resume_data(session_->session(), id_); + } + + // Returns true if this stream is writable. + inline bool IsWritable() const { + return (flags_ & NGHTTP2_STREAM_FLAG_SHUT) == 0; + } + + // Start Reading. If there are queued data chunks, they are pushed into + // the session to be emitted at the JS side + inline void ReadStart(); + + // Stop/Pause Reading. + inline void ReadStop(); + + // Returns true if reading is paused + inline bool IsPaused() const { + return (flags_ & NGHTTP2_STREAM_READ_PAUSED) == NGHTTP2_STREAM_READ_PAUSED; + } + + // Returns true if this stream is in the reading state, which occurs when + // the NGHTTP2_STREAM_READ_START flag has been set and the + // NGHTTP2_STREAM_READ_PAUSED flag is *not* set. + inline bool IsReading() const { + return ((flags_ & NGHTTP2_STREAM_READ_START) == NGHTTP2_STREAM_READ_START) + && ((flags_ & NGHTTP2_STREAM_READ_PAUSED) == 0); + } + + inline void Close(int32_t code) { + DEBUG_HTTP2("Nghttp2Stream %d: closing with code %d\n", id_, code); + flags_ |= NGHTTP2_STREAM_CLOSED; + code_ = code; + session_->OnStreamClose(id_, code); + DEBUG_HTTP2("Nghttp2Stream %d: closed\n", id_); + } + + // Returns true if this stream has been closed either by receiving or + // sending an RST_STREAM frame. + inline bool IsClosed() const { + return (flags_ & NGHTTP2_STREAM_CLOSED) == NGHTTP2_STREAM_CLOSED; + } + + // Returns the RST_STREAM code used to close this stream + inline int32_t code() const { + return code_; + } + + // Returns the stream identifier for this stream + inline int32_t id() const { + return id_; + } + + inline nghttp2_header_list* headers() const { + return current_headers_head_; + } + + inline nghttp2_headers_category headers_category() const { + return current_headers_category_; + } + + inline void FreeHeaders(); + + void StartHeaders(nghttp2_headers_category category) { + DEBUG_HTTP2("Nghttp2Stream %d: starting headers, category: %d\n", + id_, category); + // We shouldn't be in the middle of a headers block already. + // Something bad happened if this fails + CHECK_EQ(current_headers_head_, nullptr); + CHECK_EQ(current_headers_tail_, nullptr); + current_headers_category_ = category; + } + + private: + // The Parent HTTP/2 Session + Nghttp2Session* session_ = nullptr; + + // The Stream Identifier + int32_t id_ = 0; + + // Internal state flags + int flags_ = 0; + + // Outbound Data... This is the data written by the JS layer that is + // waiting to be written out to the socket. + nghttp2_stream_write_queue* queue_head_ = nullptr; + nghttp2_stream_write_queue* queue_tail_ = nullptr; + unsigned int queue_head_index_ = 0; + size_t queue_head_offset_ = 0; + size_t fd_offset_ = 0; + + // The Current Headers block... As headers are received for this stream, + // they are temporarily stored here until the OnFrameReceived is called + // signalling the end of the HEADERS frame + nghttp2_header_list* current_headers_head_ = nullptr; + nghttp2_header_list* current_headers_tail_ = nullptr; + nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS; + + // Inbound Data... This is the data received via DATA frames for this stream. + nghttp2_data_chunk_t* data_chunks_head_ = nullptr; + nghttp2_data_chunk_t* data_chunks_tail_ = nullptr; + + // The RST_STREAM code used to close this stream + int32_t code_ = NGHTTP2_NO_ERROR; + + int32_t prev_local_window_size_ = 65535; + + friend class Nghttp2Session; +}; + +struct nghttp2_stream_write_t { + void* data; + int status; + Nghttp2Stream* handle; + nghttp2_stream_write_queue* item; +}; + +struct nghttp2_data_chunk_t { + uv_buf_t buf; + nghttp2_data_chunk_t* next = nullptr; +}; + +struct nghttp2_data_chunks_t { + unsigned int nbufs = 0; + uv_buf_t buf[MAX_BUFFER_COUNT]; + + inline ~nghttp2_data_chunks_t(); +}; + +} // namespace http2 +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_HTTP2_CORE_H_ diff --git a/src/node_internals.h b/src/node_internals.h index 297e6fc307796a..c3d34cb3ca824d 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -66,6 +66,9 @@ extern std::string openssl_config; // that is used by lib/module.js extern bool config_preserve_symlinks; +// Set in node.cc by ParseArgs when --expose-http2 is used. +extern bool config_expose_http2; + // Set in node.cc by ParseArgs when --expose-internals or --expose_internals is // used. // Used in node_config.cc to set a constant on process.binding('config') diff --git a/src/stream_base.cc b/src/stream_base.cc index 51bad94a4fabc0..3e94054546d69b 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -408,6 +408,7 @@ void StreamBase::AfterWrite(WriteWrap* req_wrap, int status) { // Unref handle property Local req_wrap_obj = req_wrap->object(); req_wrap_obj->Delete(env->context(), env->handle_string()).FromJust(); + wrap->OnAfterWrite(req_wrap); Local argv[] = { diff --git a/src/stream_base.h b/src/stream_base.h index 68c82d243f2913..1b486e61db150e 100644 --- a/src/stream_base.h +++ b/src/stream_base.h @@ -89,6 +89,17 @@ class WriteWrap: public ReqWrap, static const size_t kAlignSize = 16; + WriteWrap(Environment* env, + v8::Local obj, + StreamBase* wrap, + DoneCb cb) + : ReqWrap(env, obj, AsyncWrap::PROVIDER_WRITEWRAP), + StreamReq(cb), + wrap_(wrap), + storage_size_(0) { + Wrap(obj, this); + } + protected: WriteWrap(Environment* env, v8::Local obj, diff --git a/vcbuild.bat b/vcbuild.bat index 3e41d45e8b95ff..30b557c26b0826 100644 --- a/vcbuild.bat +++ b/vcbuild.bat @@ -48,6 +48,8 @@ set js_test_suites=async-hooks inspector known_issues message parallel sequentia set v8_test_options= set v8_build_options= set "common_test_suites=%js_test_suites% doctool addons addons-napi&set build_addons=1&set build_addons_napi=1" +set http2_debug= +set nghttp2_debug= :next-arg if "%1"=="" goto args-done @@ -107,6 +109,8 @@ if /i "%1"=="enable-vtune" set enable_vtune_arg=1&goto arg-ok if /i "%1"=="dll" set dll=1&goto arg-ok if /i "%1"=="static" set enable_static=1&goto arg-ok if /i "%1"=="no-NODE-OPTIONS" set no_NODE_OPTIONS=1&goto arg-ok +if /i "%1"=="debug-http2" set debug_http2=1&goto arg-ok +if /i "%1"=="debug-nghttp2" set debug_nghttp2=1&goto arg-ok echo Error: invalid command line option `%1`. exit /b 1 @@ -144,6 +148,9 @@ if defined dll set configure_flags=%configure_flags% --shared if defined enable_static set configure_flags=%configure_flags% --enable-static if defined no_NODE_OPTIONS set configure_flags=%configure_flags% --without-node-options +REM if defined debug_http2 set configure_flags=%configure_flags% --debug-http2 +REM if defined debug_nghttp2 set configure_flags=%configure_flags% --debug-nghttp2 + if "%i18n_arg%"=="full-icu" set configure_flags=%configure_flags% --with-intl=full-icu if "%i18n_arg%"=="small-icu" set configure_flags=%configure_flags% --with-intl=small-icu if "%i18n_arg%"=="intl-none" set configure_flags=%configure_flags% --with-intl=none From 3eb61b00de97e83414d91af3db0a857baf3a5733 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 10:29:42 -0700 Subject: [PATCH 58/97] http2: add tests and benchmarks Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- benchmark/README.md | 6 + benchmark/_http-benchmarkers.js | 55 +++- benchmark/http2/respond-with-fd.js | 43 +++ benchmark/http2/simple.js | 38 +++ test/common/index.js | 9 + test/parallel/test-async-wrap-getasyncid.js | 5 + .../test-dgram-bind-default-address.js | 0 test/parallel/test-http2-binding.js | 229 ++++++++++++++++ test/parallel/test-http2-client-data-end.js | 90 +++++++ ...est-http2-client-destroy-before-connect.js | 28 ++ ...est-http2-client-destroy-before-request.js | 28 ++ test/parallel/test-http2-client-destroy.js | 54 ++++ ...st-http2-client-priority-before-connect.js | 37 +++ ...t-http2-client-rststream-before-connect.js | 34 +++ .../test-http2-client-set-priority.js | 49 ++++ ...st-http2-client-settings-before-connect.js | 63 +++++ ...st-http2-client-shutdown-before-connect.js | 23 ++ .../test-http2-client-socket-destroy.js | 46 ++++ ...p2-client-stream-destroy-before-connect.js | 63 +++++ .../test-http2-client-unescaped-path.js | 37 +++ test/parallel/test-http2-client-upload.js | 44 ++++ .../test-http2-client-write-before-connect.js | 53 ++++ ...test-http2-compat-serverrequest-headers.js | 70 +++++ .../test-http2-compat-serverrequest.js | 52 ++++ ...ompat-serverresponse-createpushresponse.js | 79 ++++++ .../test-http2-compat-serverresponse-end.js | 77 ++++++ ...st-http2-compat-serverresponse-finished.js | 37 +++ ...ttp2-compat-serverresponse-flushheaders.js | 43 +++ ...est-http2-compat-serverresponse-headers.js | 83 ++++++ ...-http2-compat-serverresponse-statuscode.js | 76 ++++++ ...tp2-compat-serverresponse-statusmessage.js | 52 ++++ ...http2-compat-serverresponse-write-no-cb.js | 98 +++++++ ...t-http2-compat-serverresponse-writehead.js | 44 ++++ test/parallel/test-http2-connect-method.js | 71 +++++ test/parallel/test-http2-connect.js | 29 ++ test/parallel/test-http2-cookies.js | 62 +++++ .../test-http2-create-client-connect.js | 88 +++++++ ...test-http2-create-client-secure-session.js | 75 ++++++ .../test-http2-create-client-session.js | 61 +++++ test/parallel/test-http2-date-header.js | 28 ++ test/parallel/test-http2-dont-override.js | 48 ++++ test/parallel/test-http2-getpackedsettings.js | 131 +++++++++ test/parallel/test-http2-goaway-opaquedata.js | 38 +++ test/parallel/test-http2-head-request.js | 57 ++++ test/parallel/test-http2-https-fallback.js | 146 +++++++++++ test/parallel/test-http2-info-headers.js | 85 ++++++ .../test-http2-max-concurrent-streams.js | 67 +++++ test/parallel/test-http2-methods.js | 48 ++++ .../test-http2-misused-pseudoheaders.js | 61 +++++ .../test-http2-multi-content-length.js | 58 ++++ test/parallel/test-http2-multiheaders.js | 60 +++++ test/parallel/test-http2-multiplex.js | 59 +++++ test/parallel/test-http2-noflag.js | 8 + ...-http2-options-max-headers-block-length.js | 48 ++++ ...test-http2-options-max-reserved-streams.js | 73 ++++++ test/parallel/test-http2-padding-callback.js | 50 ++++ test/parallel/test-http2-priority-event.js | 60 +++++ test/parallel/test-http2-respond-file-204.js | 41 +++ test/parallel/test-http2-respond-file-304.js | 44 ++++ .../test-http2-respond-file-compat.js | 23 ++ .../test-http2-respond-file-fd-invalid.js | 37 +++ test/parallel/test-http2-respond-file-fd.js | 46 ++++ test/parallel/test-http2-respond-file-push.js | 81 ++++++ test/parallel/test-http2-respond-file.js | 51 ++++ .../parallel/test-http2-response-splitting.js | 75 ++++++ test/parallel/test-http2-serve-file.js | 82 ++++++ ...-http2-server-destroy-before-additional.js | 38 +++ .../test-http2-server-destroy-before-push.js | 38 +++ ...est-http2-server-destroy-before-respond.js | 38 +++ .../test-http2-server-destroy-before-write.js | 38 +++ .../test-http2-server-push-disabled.js | 53 ++++ .../parallel/test-http2-server-push-stream.js | 58 ++++ .../test-http2-server-rst-before-respond.js | 45 ++++ test/parallel/test-http2-server-rst-stream.js | 72 +++++ test/parallel/test-http2-server-set-header.js | 36 +++ ...st-http2-server-shutdown-before-respond.js | 32 +++ .../test-http2-server-socket-destroy.js | 57 ++++ test/parallel/test-http2-server-startup.js | 78 ++++++ test/parallel/test-http2-session-settings.js | 110 ++++++++ .../test-http2-session-stream-state.js | 97 +++++++ test/parallel/test-http2-single-headers.js | 59 +++++ .../test-http2-status-code-invalid.js | 40 +++ test/parallel/test-http2-status-code.js | 40 +++ test/parallel/test-http2-timeouts.js | 32 +++ test/parallel/test-http2-too-many-settings.js | 60 +++++ test/parallel/test-http2-trailers.js | 44 ++++ test/parallel/test-http2-util-asserts.js | 43 +++ test/parallel/test-http2-util-headers-list.js | 248 ++++++++++++++++++ test/parallel/test-http2-window-size.js | 102 +++++++ test/parallel/test-http2-withflag.js | 7 + test/parallel/test-http2-write-callbacks.js | 36 +++ .../parallel/test-http2-write-empty-string.js | 40 +++ test/parallel/test-http2-zero-length-write.js | 50 ++++ test/parallel/test-process-versions.js | 2 +- .../test-tls-disable-renegotiation.js | 0 95 files changed, 5327 insertions(+), 2 deletions(-) create mode 100644 benchmark/http2/respond-with-fd.js create mode 100644 benchmark/http2/simple.js mode change 100755 => 100644 test/parallel/test-dgram-bind-default-address.js create mode 100644 test/parallel/test-http2-binding.js create mode 100644 test/parallel/test-http2-client-data-end.js create mode 100644 test/parallel/test-http2-client-destroy-before-connect.js create mode 100644 test/parallel/test-http2-client-destroy-before-request.js create mode 100644 test/parallel/test-http2-client-destroy.js create mode 100644 test/parallel/test-http2-client-priority-before-connect.js create mode 100644 test/parallel/test-http2-client-rststream-before-connect.js create mode 100644 test/parallel/test-http2-client-set-priority.js create mode 100644 test/parallel/test-http2-client-settings-before-connect.js create mode 100644 test/parallel/test-http2-client-shutdown-before-connect.js create mode 100644 test/parallel/test-http2-client-socket-destroy.js create mode 100644 test/parallel/test-http2-client-stream-destroy-before-connect.js create mode 100644 test/parallel/test-http2-client-unescaped-path.js create mode 100644 test/parallel/test-http2-client-upload.js create mode 100644 test/parallel/test-http2-client-write-before-connect.js create mode 100644 test/parallel/test-http2-compat-serverrequest-headers.js create mode 100644 test/parallel/test-http2-compat-serverrequest.js create mode 100644 test/parallel/test-http2-compat-serverresponse-createpushresponse.js create mode 100644 test/parallel/test-http2-compat-serverresponse-end.js create mode 100644 test/parallel/test-http2-compat-serverresponse-finished.js create mode 100644 test/parallel/test-http2-compat-serverresponse-flushheaders.js create mode 100644 test/parallel/test-http2-compat-serverresponse-headers.js create mode 100644 test/parallel/test-http2-compat-serverresponse-statuscode.js create mode 100644 test/parallel/test-http2-compat-serverresponse-statusmessage.js create mode 100644 test/parallel/test-http2-compat-serverresponse-write-no-cb.js create mode 100644 test/parallel/test-http2-compat-serverresponse-writehead.js create mode 100644 test/parallel/test-http2-connect-method.js create mode 100644 test/parallel/test-http2-connect.js create mode 100644 test/parallel/test-http2-cookies.js create mode 100644 test/parallel/test-http2-create-client-connect.js create mode 100644 test/parallel/test-http2-create-client-secure-session.js create mode 100644 test/parallel/test-http2-create-client-session.js create mode 100644 test/parallel/test-http2-date-header.js create mode 100644 test/parallel/test-http2-dont-override.js create mode 100644 test/parallel/test-http2-getpackedsettings.js create mode 100644 test/parallel/test-http2-goaway-opaquedata.js create mode 100644 test/parallel/test-http2-head-request.js create mode 100644 test/parallel/test-http2-https-fallback.js create mode 100755 test/parallel/test-http2-info-headers.js create mode 100644 test/parallel/test-http2-max-concurrent-streams.js create mode 100644 test/parallel/test-http2-methods.js create mode 100644 test/parallel/test-http2-misused-pseudoheaders.js create mode 100644 test/parallel/test-http2-multi-content-length.js create mode 100644 test/parallel/test-http2-multiheaders.js create mode 100644 test/parallel/test-http2-multiplex.js create mode 100644 test/parallel/test-http2-noflag.js create mode 100644 test/parallel/test-http2-options-max-headers-block-length.js create mode 100644 test/parallel/test-http2-options-max-reserved-streams.js create mode 100644 test/parallel/test-http2-padding-callback.js create mode 100644 test/parallel/test-http2-priority-event.js create mode 100644 test/parallel/test-http2-respond-file-204.js create mode 100644 test/parallel/test-http2-respond-file-304.js create mode 100644 test/parallel/test-http2-respond-file-compat.js create mode 100644 test/parallel/test-http2-respond-file-fd-invalid.js create mode 100644 test/parallel/test-http2-respond-file-fd.js create mode 100644 test/parallel/test-http2-respond-file-push.js create mode 100644 test/parallel/test-http2-respond-file.js create mode 100644 test/parallel/test-http2-response-splitting.js create mode 100644 test/parallel/test-http2-serve-file.js create mode 100644 test/parallel/test-http2-server-destroy-before-additional.js create mode 100644 test/parallel/test-http2-server-destroy-before-push.js create mode 100644 test/parallel/test-http2-server-destroy-before-respond.js create mode 100644 test/parallel/test-http2-server-destroy-before-write.js create mode 100644 test/parallel/test-http2-server-push-disabled.js create mode 100644 test/parallel/test-http2-server-push-stream.js create mode 100644 test/parallel/test-http2-server-rst-before-respond.js create mode 100644 test/parallel/test-http2-server-rst-stream.js create mode 100644 test/parallel/test-http2-server-set-header.js create mode 100644 test/parallel/test-http2-server-shutdown-before-respond.js create mode 100644 test/parallel/test-http2-server-socket-destroy.js create mode 100644 test/parallel/test-http2-server-startup.js create mode 100644 test/parallel/test-http2-session-settings.js create mode 100644 test/parallel/test-http2-session-stream-state.js create mode 100644 test/parallel/test-http2-single-headers.js create mode 100644 test/parallel/test-http2-status-code-invalid.js create mode 100644 test/parallel/test-http2-status-code.js create mode 100644 test/parallel/test-http2-timeouts.js create mode 100644 test/parallel/test-http2-too-many-settings.js create mode 100644 test/parallel/test-http2-trailers.js create mode 100644 test/parallel/test-http2-util-asserts.js create mode 100644 test/parallel/test-http2-util-headers-list.js create mode 100644 test/parallel/test-http2-window-size.js create mode 100644 test/parallel/test-http2-withflag.js create mode 100644 test/parallel/test-http2-write-callbacks.js create mode 100644 test/parallel/test-http2-write-empty-string.js create mode 100644 test/parallel/test-http2-zero-length-write.js mode change 100755 => 100644 test/parallel/test-tls-disable-renegotiation.js diff --git a/benchmark/README.md b/benchmark/README.md index 17c733e6eb0b6c..dfdf319b9cb311 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -97,6 +97,12 @@ directory, see [the guide on benchmarks](../doc/guides/writing-and-running-bench Benchmarks for the http subsystem. + + http2 + + Benchmarks for the http2 subsystem. + + misc diff --git a/benchmark/_http-benchmarkers.js b/benchmark/_http-benchmarkers.js index 3f17f05f831170..f9359b13d5e9e4 100644 --- a/benchmark/_http-benchmarkers.js +++ b/benchmark/_http-benchmarkers.js @@ -111,10 +111,63 @@ class TestDoubleBenchmarker { } } +/** + * HTTP/2 Benchmarker + */ +class H2LoadBenchmarker { + constructor() { + this.name = 'h2load'; + this.executable = 'h2load'; + const result = child_process.spawnSync(this.executable, ['-h']); + this.present = !(result.error && result.error.code === 'ENOENT'); + } + + create(options) { + const args = []; + if (typeof options.requests === 'number') + args.push('-n', options.requests); + if (typeof options.clients === 'number') + args.push('-c', options.clients); + if (typeof options.threads === 'number') + args.push('-t', options.threads); + if (typeof options.maxConcurrentStreams === 'number') + args.push('-m', options.maxConcurrentStreams); + if (typeof options.initialWindowSize === 'number') + args.push('-w', options.initialWindowSize); + if (typeof options.sessionInitialWindowSize === 'number') + args.push('-W', options.sessionInitialWindowSize); + if (typeof options.rate === 'number') + args.push('-r', options.rate); + if (typeof options.ratePeriod === 'number') + args.push(`--rate-period=${options.ratePeriod}`); + if (typeof options.duration === 'number') + args.push('-T', options.duration); + if (typeof options.timeout === 'number') + args.push('-N', options.timeout); + if (typeof options.headerTableSize === 'number') + args.push(`--header-table-size=${options.headerTableSize}`); + if (typeof options.encoderHeaderTableSize === 'number') { + args.push( + `--encoder-header-table-size=${options.encoderHeaderTableSize}`); + } + const scheme = options.scheme || 'http'; + const host = options.host || '127.0.0.1'; + args.push(`${scheme}://${host}:${options.port}${options.path}`); + const child = child_process.spawn(this.executable, args); + return child; + } + + processResults(output) { + const rex = /(\d+(?:\.\d+)) req\/s/; + return rex.exec(output)[1]; + } +} + const http_benchmarkers = [ new WrkBenchmarker(), new AutocannonBenchmarker(), - new TestDoubleBenchmarker() + new TestDoubleBenchmarker(), + new H2LoadBenchmarker() ]; const benchmarkers = {}; diff --git a/benchmark/http2/respond-with-fd.js b/benchmark/http2/respond-with-fd.js new file mode 100644 index 00000000000000..d7a312c78bf4da --- /dev/null +++ b/benchmark/http2/respond-with-fd.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common.js'); +const PORT = common.PORT; +const path = require('path'); +const fs = require('fs'); + +const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html'); + +var bench = common.createBenchmark(main, { + requests: [100, 1000, 10000, 100000, 1000000], + streams: [100, 200, 1000], + clients: [1, 2] +}, { flags: ['--expose-http2', '--no-warnings'] }); + +function main(conf) { + + fs.open(file, 'r', (err, fd) => { + if (err) + throw err; + + const n = +conf.requests; + const m = +conf.streams; + const c = +conf.clients; + const http2 = require('http2'); + const server = http2.createServer(); + server.on('stream', (stream) => { + stream.respondWithFD(fd); + stream.on('error', (err) => {}); + }); + server.listen(PORT, () => { + bench.http({ + path: '/', + requests: n, + maxConcurrentStreams: m, + clients: c, + threads: c + }, () => server.close()); + }); + + }); + +} diff --git a/benchmark/http2/simple.js b/benchmark/http2/simple.js new file mode 100644 index 00000000000000..d12b20fc5ac773 --- /dev/null +++ b/benchmark/http2/simple.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common.js'); +const PORT = common.PORT; + +const path = require('path'); +const fs = require('fs'); + +const file = path.join(path.resolve(__dirname, '../fixtures'), 'alice.html'); + +var bench = common.createBenchmark(main, { + requests: [100, 1000, 10000, 100000], + streams: [100, 200, 1000], + clients: [1, 2] +}, { flags: ['--expose-http2', '--no-warnings'] }); + +function main(conf) { + const n = +conf.requests; + const m = +conf.streams; + const c = +conf.clients; + const http2 = require('http2'); + const server = http2.createServer(); + server.on('stream', (stream) => { + const out = fs.createReadStream(file); + stream.respond(); + out.pipe(stream); + stream.on('error', (err) => {}); + }); + server.listen(PORT, () => { + bench.http({ + path: '/', + requests: n, + maxConcurrentStreams: m, + clients: c, + threads: c + }, () => { server.close(); }); + }); +} diff --git a/test/common/index.js b/test/common/index.js index fc14cdacacc587..a5ca4cec576e74 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -816,3 +816,12 @@ exports.hijackStdout = hijackStdWritable.bind(null, 'stdout'); exports.hijackStderr = hijackStdWritable.bind(null, 'stderr'); exports.restoreStdout = restoreWritable.bind(null, 'stdout'); exports.restoreStderr = restoreWritable.bind(null, 'stderr'); + +let fd = 2; +exports.firstInvalidFD = function firstInvalidFD() { + // Get first known bad file descriptor. + try { + while (fs.fstatSync(++fd)); + } catch (e) {} + return fd; +}; diff --git a/test/parallel/test-async-wrap-getasyncid.js b/test/parallel/test-async-wrap-getasyncid.js index 57d6f86ebe5ca8..ce51408a6b678b 100644 --- a/test/parallel/test-async-wrap-getasyncid.js +++ b/test/parallel/test-async-wrap-getasyncid.js @@ -19,6 +19,11 @@ const providers = Object.assign({}, process.binding('async_wrap').Providers); process.removeAllListeners('uncaughtException'); hooks.disable(); delete providers.NONE; // Should never be used. + + // TODO(jasnell): Test for these + delete providers.HTTP2SESSION; + delete providers.HTTP2SESSIONSHUTDOWNWRAP; + const obj_keys = Object.keys(providers); if (obj_keys.length > 0) process._rawDebug(obj_keys); diff --git a/test/parallel/test-dgram-bind-default-address.js b/test/parallel/test-dgram-bind-default-address.js old mode 100755 new mode 100644 diff --git a/test/parallel/test-http2-binding.js b/test/parallel/test-http2-binding.js new file mode 100644 index 00000000000000..c26549d3615981 --- /dev/null +++ b/test/parallel/test-http2-binding.js @@ -0,0 +1,229 @@ +// Flags: --expose-http2 +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.doesNotThrow(() => process.binding('http2')); + +const binding = process.binding('http2'); +const http2 = require('http2'); + +assert(binding.Http2Session); +assert.strictEqual(typeof binding.Http2Session, 'function'); + +const settings = http2.getDefaultSettings(); +assert.strictEqual(settings.headerTableSize, 4096); +assert.strictEqual(settings.enablePush, true); +assert.strictEqual(settings.initialWindowSize, 65535); +assert.strictEqual(settings.maxFrameSize, 16384); + +assert.strictEqual(binding.nghttp2ErrorString(-517), + 'GOAWAY has already been sent'); + +// assert constants are present +assert(binding.constants); +assert.strictEqual(typeof binding.constants, 'object'); +const constants = binding.constants; + +const expectedStatusCodes = { + HTTP_STATUS_CONTINUE: 100, + HTTP_STATUS_SWITCHING_PROTOCOLS: 101, + HTTP_STATUS_PROCESSING: 102, + HTTP_STATUS_OK: 200, + HTTP_STATUS_CREATED: 201, + HTTP_STATUS_ACCEPTED: 202, + HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION: 203, + HTTP_STATUS_NO_CONTENT: 204, + HTTP_STATUS_RESET_CONTENT: 205, + HTTP_STATUS_PARTIAL_CONTENT: 206, + HTTP_STATUS_MULTI_STATUS: 207, + HTTP_STATUS_ALREADY_REPORTED: 208, + HTTP_STATUS_IM_USED: 226, + HTTP_STATUS_MULTIPLE_CHOICES: 300, + HTTP_STATUS_MOVED_PERMANENTLY: 301, + HTTP_STATUS_FOUND: 302, + HTTP_STATUS_SEE_OTHER: 303, + HTTP_STATUS_NOT_MODIFIED: 304, + HTTP_STATUS_USE_PROXY: 305, + HTTP_STATUS_TEMPORARY_REDIRECT: 307, + HTTP_STATUS_PERMANENT_REDIRECT: 308, + HTTP_STATUS_BAD_REQUEST: 400, + HTTP_STATUS_UNAUTHORIZED: 401, + HTTP_STATUS_PAYMENT_REQUIRED: 402, + HTTP_STATUS_FORBIDDEN: 403, + HTTP_STATUS_NOT_FOUND: 404, + HTTP_STATUS_METHOD_NOT_ALLOWED: 405, + HTTP_STATUS_NOT_ACCEPTABLE: 406, + HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED: 407, + HTTP_STATUS_REQUEST_TIMEOUT: 408, + HTTP_STATUS_CONFLICT: 409, + HTTP_STATUS_GONE: 410, + HTTP_STATUS_LENGTH_REQUIRED: 411, + HTTP_STATUS_PRECONDITION_FAILED: 412, + HTTP_STATUS_PAYLOAD_TOO_LARGE: 413, + HTTP_STATUS_URI_TOO_LONG: 414, + HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: 415, + HTTP_STATUS_RANGE_NOT_SATISFIABLE: 416, + HTTP_STATUS_EXPECTATION_FAILED: 417, + HTTP_STATUS_TEAPOT: 418, + HTTP_STATUS_MISDIRECTED_REQUEST: 421, + HTTP_STATUS_UNPROCESSABLE_ENTITY: 422, + HTTP_STATUS_LOCKED: 423, + HTTP_STATUS_FAILED_DEPENDENCY: 424, + HTTP_STATUS_UNORDERED_COLLECTION: 425, + HTTP_STATUS_UPGRADE_REQUIRED: 426, + HTTP_STATUS_PRECONDITION_REQUIRED: 428, + HTTP_STATUS_TOO_MANY_REQUESTS: 429, + HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE: 431, + HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS: 451, + HTTP_STATUS_INTERNAL_SERVER_ERROR: 500, + HTTP_STATUS_NOT_IMPLEMENTED: 501, + HTTP_STATUS_BAD_GATEWAY: 502, + HTTP_STATUS_SERVICE_UNAVAILABLE: 503, + HTTP_STATUS_GATEWAY_TIMEOUT: 504, + HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED: 505, + HTTP_STATUS_VARIANT_ALSO_NEGOTIATES: 506, + HTTP_STATUS_INSUFFICIENT_STORAGE: 507, + HTTP_STATUS_LOOP_DETECTED: 508, + HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED: 509, + HTTP_STATUS_NOT_EXTENDED: 510, + HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED: 511 +}; + +const expectedHeaderNames = { + HTTP2_HEADER_STATUS: ':status', + HTTP2_HEADER_METHOD: ':method', + HTTP2_HEADER_AUTHORITY: ':authority', + HTTP2_HEADER_SCHEME: ':scheme', + HTTP2_HEADER_PATH: ':path', + HTTP2_HEADER_DATE: 'date', + HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset', + HTTP2_HEADER_ACCEPT_ENCODING: 'accept-encoding', + HTTP2_HEADER_ACCEPT_LANGUAGE: 'accept-language', + HTTP2_HEADER_ACCEPT_RANGES: 'accept-ranges', + HTTP2_HEADER_ACCEPT: 'accept', + HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: 'access-control-allow-origin', + HTTP2_HEADER_AGE: 'age', + HTTP2_HEADER_ALLOW: 'allow', + HTTP2_HEADER_AUTHORIZATION: 'authorization', + HTTP2_HEADER_CACHE_CONTROL: 'cache-control', + HTTP2_HEADER_CONTENT_DISPOSITION: 'content-disposition', + HTTP2_HEADER_CONTENT_ENCODING: 'content-encoding', + HTTP2_HEADER_CONTENT_LANGUAGE: 'content-language', + HTTP2_HEADER_CONTENT_LENGTH: 'content-length', + HTTP2_HEADER_CONTENT_LOCATION: 'content-location', + HTTP2_HEADER_CONTENT_RANGE: 'content-range', + HTTP2_HEADER_CONTENT_TYPE: 'content-type', + HTTP2_HEADER_COOKIE: 'cookie', + HTTP2_HEADER_CONNECTION: 'connection', + HTTP2_HEADER_ETAG: 'etag', + HTTP2_HEADER_EXPECT: 'expect', + HTTP2_HEADER_EXPIRES: 'expires', + HTTP2_HEADER_FROM: 'from', + HTTP2_HEADER_HOST: 'host', + HTTP2_HEADER_IF_MATCH: 'if-match', + HTTP2_HEADER_IF_MODIFIED_SINCE: 'if-modified-since', + HTTP2_HEADER_IF_NONE_MATCH: 'if-none-match', + HTTP2_HEADER_IF_RANGE: 'if-range', + HTTP2_HEADER_IF_UNMODIFIED_SINCE: 'if-unmodified-since', + HTTP2_HEADER_LAST_MODIFIED: 'last-modified', + HTTP2_HEADER_LINK: 'link', + HTTP2_HEADER_LOCATION: 'location', + HTTP2_HEADER_MAX_FORWARDS: 'max-forwards', + HTTP2_HEADER_PREFER: 'prefer', + HTTP2_HEADER_PROXY_AUTHENTICATE: 'proxy-authenticate', + HTTP2_HEADER_PROXY_AUTHORIZATION: 'proxy-authorization', + HTTP2_HEADER_PROXY_CONNECTION: 'proxy-connection', + HTTP2_HEADER_RANGE: 'range', + HTTP2_HEADER_REFERER: 'referer', + HTTP2_HEADER_REFRESH: 'refresh', + HTTP2_HEADER_RETRY_AFTER: 'retry-after', + HTTP2_HEADER_SERVER: 'server', + HTTP2_HEADER_SET_COOKIE: 'set-cookie', + HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: 'strict-transport-security', + HTTP2_HEADER_TRANSFER_ENCODING: 'transfer-encoding', + HTTP2_HEADER_USER_AGENT: 'user-agent', + HTTP2_HEADER_VARY: 'vary', + HTTP2_HEADER_VIA: 'via', + HTTP2_HEADER_WWW_AUTHENTICATE: 'www-authenticate', + HTTP2_HEADER_KEEP_ALIVE: 'keep-alive', + HTTP2_HEADER_CONTENT_MD5: 'content-md5', + HTTP2_HEADER_TE: 'te', + HTTP2_HEADER_UPGRADE: 'upgrade', + HTTP2_HEADER_HTTP2_SETTINGS: 'http2-settings' +}; + +const expectedNGConstants = { + NGHTTP2_SESSION_SERVER: 0, + NGHTTP2_SESSION_CLIENT: 1, + NGHTTP2_STREAM_STATE_IDLE: 1, + NGHTTP2_STREAM_STATE_OPEN: 2, + NGHTTP2_STREAM_STATE_RESERVED_LOCAL: 3, + NGHTTP2_STREAM_STATE_RESERVED_REMOTE: 4, + NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: 5, + NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: 6, + NGHTTP2_STREAM_STATE_CLOSED: 7, + NGHTTP2_HCAT_REQUEST: 0, + NGHTTP2_HCAT_RESPONSE: 1, + NGHTTP2_HCAT_PUSH_RESPONSE: 2, + NGHTTP2_HCAT_HEADERS: 3, + NGHTTP2_NO_ERROR: 0, + NGHTTP2_PROTOCOL_ERROR: 1, + NGHTTP2_INTERNAL_ERROR: 2, + NGHTTP2_FLOW_CONTROL_ERROR: 3, + NGHTTP2_SETTINGS_TIMEOUT: 4, + NGHTTP2_STREAM_CLOSED: 8, + NGHTTP2_FRAME_SIZE_ERROR: 6, + NGHTTP2_REFUSED_STREAM: 7, + NGHTTP2_CANCEL: 8, + NGHTTP2_COMPRESSION_ERROR: 9, + NGHTTP2_CONNECT_ERROR: 10, + NGHTTP2_ENHANCE_YOUR_CALM: 11, + NGHTTP2_INADEQUATE_SECURITY: 12, + NGHTTP2_HTTP_1_1_REQUIRED: 13, + NGHTTP2_NV_FLAG_NONE: 0, + NGHTTP2_NV_FLAG_NO_INDEX: 1, + NGHTTP2_ERR_DEFERRED: -508, + NGHTTP2_ERR_NOMEM: -901, + NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: -509, + NGHTTP2_ERR_INVALID_ARGUMENT: -501, + NGHTTP2_ERR_STREAM_CLOSED: -510, + NGHTTP2_ERR_FRAME_SIZE_ERROR: -522, + NGHTTP2_FLAG_NONE: 0, + NGHTTP2_FLAG_END_STREAM: 1, + NGHTTP2_FLAG_END_HEADERS: 4, + NGHTTP2_FLAG_ACK: 1, + NGHTTP2_FLAG_PADDED: 8, + NGHTTP2_FLAG_PRIORITY: 32, + NGHTTP2_DEFAULT_WEIGHT: 16, + NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: 1, + NGHTTP2_SETTINGS_ENABLE_PUSH: 2, + NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3, + NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4, + NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5, + NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6 +}; + +const defaultSettings = { + DEFAULT_SETTINGS_HEADER_TABLE_SIZE: 4096, + DEFAULT_SETTINGS_ENABLE_PUSH: 1, + DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 65535, + DEFAULT_SETTINGS_MAX_FRAME_SIZE: 16384 +}; + +for (const name of Object.keys(constants)) { + if (name.startsWith('HTTP_STATUS_')) { + assert.strictEqual(expectedStatusCodes[name], constants[name], + `Expected status code match for ${name}`); + } else if (name.startsWith('HTTP2_HEADER_')) { + assert.strictEqual(expectedHeaderNames[name], constants[name], + `Expected header name match for ${name}`); + } else if (name.startsWith('NGHTTP2_')) { + assert.strictEqual(expectedNGConstants[name], constants[name], + `Expected ng constant match for ${name}`); + } else if (name.startsWith('DEFAULT_SETTINGS_')) { + assert.strictEqual(defaultSettings[name], constants[name], + `Expected default setting match for ${name}`); + } +} diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js new file mode 100644 index 00000000000000..3bd72f138ba60e --- /dev/null +++ b/test/parallel/test-http2-client-data-end.js @@ -0,0 +1,90 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers, flags) => { + const port = server.address().port; + if (headers[':path'] === '/') { + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${port}`, + }, (push, headers) => { + push.respond({ + 'content-type': 'text/html', + ':status': 200, + 'x-push-data': 'pushed by server', + }); + push.write('pushed by server '); + // Sending in next immediate ensures that a second data frame + // will be sent to the client, which will cause the 'data' event + // to fire multiple times. + setImmediate(() => { + push.end('data'); + }); + stream.end('st'); + }); + } + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write('te'); +})); + + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const headers = { ':path': '/' }; + const client = http2.connect(`http://localhost:${port}`); + + const req = client.request(headers); + + let expected = 2; + function maybeClose() { + if (--expected === 0) { + server.close(); + client.destroy(); + } + } + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + })); + + client.on('stream', common.mustCall((stream, headers, flags) => { + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':path'], '/foobar'); + assert.strictEqual(headers[':authority'], `localhost:${port}`); + stream.on('push', common.mustCall((headers, flags) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + assert.strictEqual(headers['x-push-data'], 'pushed by server'); + })); + + stream.setEncoding('utf8'); + let pushData = ''; + stream.on('data', common.mustCall((d) => { + pushData += d; + }, 2)); + stream.on('end', common.mustCall(() => { + assert.strictEqual(pushData, 'pushed by server data'); + maybeClose(); + })); + })); + + let data = ''; + + req.setEncoding('utf8'); + req.on('data', common.mustCall((d) => data += d)); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'test'); + maybeClose(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-client-destroy-before-connect.js b/test/parallel/test-http2-client-destroy-before-connect.js new file mode 100644 index 00000000000000..1f6b087dd220b3 --- /dev/null +++ b/test/parallel/test-http2-client-destroy-before-connect.js @@ -0,0 +1,28 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + client.destroy(); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-destroy-before-request.js b/test/parallel/test-http2-client-destroy-before-request.js new file mode 100644 index 00000000000000..71519d5903b58f --- /dev/null +++ b/test/parallel/test-http2-client-destroy-before-request.js @@ -0,0 +1,28 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + client.destroy(); + + assert.throws(() => client.request({ ':path': '/' }), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SESSION', + message: /^The session has been destroyed$/ + })); + + server.close(); + +})); diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js new file mode 100644 index 00000000000000..56cfec5d65a223 --- /dev/null +++ b/test/parallel/test-http2-client-destroy.js @@ -0,0 +1,54 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); +server.listen(0); + +server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const destroyCallbacks = [ + (client) => client.destroy(), + (client) => client.socket.destroy() + ]; + + let remaining = destroyCallbacks.length; + + destroyCallbacks.forEach((destroyCallback) => { + const client = h2.connect(`http://localhost:${port}`); + client.on('connect', common.mustCall(() => { + const socket = client.socket; + + assert(client.socket, 'client session has associated socket'); + assert(!client.destroyed, + 'client has not been destroyed before destroy is called'); + assert(!socket.destroyed, + 'socket has not been destroyed before destroy is called'); + + // Ensure that 'close' event is emitted + client.on('close', common.mustCall()); + + destroyCallback(client); + + assert(!client.socket, 'client.socket undefined after destroy is called'); + + // Must must be closed + client.on('close', common.mustCall(() => { + assert(client.destroyed); + })); + + // socket will close on process.nextTick + socket.on('close', common.mustCall(() => { + assert(socket.destroyed); + })); + + if (--remaining === 0) { + server.close(); + } + })); + }); +})); diff --git a/test/parallel/test-http2-client-priority-before-connect.js b/test/parallel/test-http2-client-priority-before-connect.js new file mode 100644 index 00000000000000..68933b2d83bbf1 --- /dev/null +++ b/test/parallel/test-http2-client-priority-before-connect.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + client.priority(req, {}); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js new file mode 100644 index 00000000000000..33b6cb354fe225 --- /dev/null +++ b/test/parallel/test-http2-client-rststream-before-connect.js @@ -0,0 +1,34 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + client.rstStream(req, 0); + assert.strictEqual(req.rstCode, 0); + + // make sure that destroy is called + req._destroy = common.mustCall(req._destroy.bind(req)); + + req.on('streamClosed', common.mustCall((code) => { + assert.strictEqual(req.destroyed, true); + assert.strictEqual(code, 0); + server.close(); + client.destroy(); + })); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall()); + req.end(); +})); diff --git a/test/parallel/test-http2-client-set-priority.js b/test/parallel/test-http2-client-set-priority.js new file mode 100644 index 00000000000000..314a88a63c2d16 --- /dev/null +++ b/test/parallel/test-http2-client-set-priority.js @@ -0,0 +1,49 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const checkWeight = (actual, expect) => { + const server = http2.createServer(); + server.on('stream', common.mustCall((stream, headers, flags) => { + assert.strictEqual(stream.state.weight, expect); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('test'); + })); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const headers = { ':path': '/' }; + const req = client.request(headers, { weight: actual }); + + req.on('data', common.mustCall(() => {})); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + })); +}; + +// when client weight is lower than 1, weight is 1 +checkWeight(-1, 1); +checkWeight(0, 1); + +// 1 - 256 is correct weight +checkWeight(1, 1); +checkWeight(16, 16); +checkWeight(256, 256); + +// when client weight is higher than 256, weight is 256 +checkWeight(257, 256); +checkWeight(512, 256); + +// when client weight is undefined, weight is default 16 +checkWeight(undefined, 16); diff --git a/test/parallel/test-http2-client-settings-before-connect.js b/test/parallel/test-http2-client-settings-before-connect.js new file mode 100644 index 00000000000000..9391502479a7f9 --- /dev/null +++ b/test/parallel/test-http2-client-settings-before-connect.js @@ -0,0 +1,63 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + assert.throws(() => client.settings({headerTableSize: -1}), + RangeError); + assert.throws(() => client.settings({headerTableSize: 2 ** 32}), + RangeError); + assert.throws(() => client.settings({initialWindowSize: -1}), + RangeError); + assert.throws(() => client.settings({initialWindowSize: 2 ** 32}), + RangeError); + assert.throws(() => client.settings({maxFrameSize: 1}), + RangeError); + assert.throws(() => client.settings({maxFrameSize: 2 ** 24}), + RangeError); + assert.throws(() => client.settings({maxConcurrentStreams: -1}), + RangeError); + assert.throws(() => client.settings({maxConcurrentStreams: 2 ** 31}), + RangeError); + assert.throws(() => client.settings({maxHeaderListSize: -1}), + RangeError); + assert.throws(() => client.settings({maxHeaderListSize: 2 ** 32}), + RangeError); + ['a', 1, 0, null, {}].forEach((i) => { + assert.throws(() => client.settings({enablePush: i}), TypeError); + }); + + client.settings({ maxFrameSize: 1234567 }); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-shutdown-before-connect.js b/test/parallel/test-http2-client-shutdown-before-connect.js new file mode 100644 index 00000000000000..203963bf57721e --- /dev/null +++ b/test/parallel/test-http2-client-shutdown-before-connect.js @@ -0,0 +1,23 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + client.shutdown({graceful: true}, common.mustCall(() => { + server.close(); + client.destroy(); + })); + +})); diff --git a/test/parallel/test-http2-client-socket-destroy.js b/test/parallel/test-http2-client-socket-destroy.js new file mode 100644 index 00000000000000..fe2d92753172a8 --- /dev/null +++ b/test/parallel/test-http2-client-socket-destroy.js @@ -0,0 +1,46 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); +const body = + '

this is some data

'; + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream) { + // The stream aborted event must have been triggered + stream.on('aborted', common.mustCall()); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write(body); +} + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const client = h2.connect(`http://localhost:${this.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall(() => { + // send a premature socket close + client.socket.destroy(); + })); + req.on('data', common.mustNotCall()); + + req.on('end', common.mustCall(() => { + server.close(); + })); + + // On the client, the close event must call + client.on('close', common.mustCall()); + req.end(); + +})); diff --git a/test/parallel/test-http2-client-stream-destroy-before-connect.js b/test/parallel/test-http2-client-stream-destroy-before-connect.js new file mode 100644 index 00000000000000..5ab0cac5082aed --- /dev/null +++ b/test/parallel/test-http2-client-stream-destroy-before-connect.js @@ -0,0 +1,63 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const NGHTTP2_INTERNAL_ERROR = h2.constants.NGHTTP2_INTERNAL_ERROR; + +const server = h2.createServer(); + +// Do not mustCall the server side callbacks, they may or may not be called +// depending on the OS. The determination is based largely on operating +// system specific timings +server.on('stream', (stream) => { + // Do not wrap in a must call or use common.expectsError (which now uses + // must call). The error may or may not be reported depending on operating + // system specific timings. + stream.on('error', (err) => { + if (err) { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR'); + assert.strictEqual(err.message, 'Stream closed with error code 2'); + } + }); + stream.respond({}); + stream.end(); +}); + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + const err = new Error('test'); + req.destroy(err); + + req.on('error', common.mustCall((err) => { + const fn = err.code === 'ERR_HTTP2_STREAM_ERROR' ? + common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 2' + }) : + common.expectsError({ + type: Error, + message: 'test' + }); + fn(err); + }, 2)); + + req.on('streamClosed', common.mustCall((code) => { + assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); + assert.strictEqual(code, NGHTTP2_INTERNAL_ERROR); + server.close(); + client.destroy(); + })); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall()); + +})); diff --git a/test/parallel/test-http2-client-unescaped-path.js b/test/parallel/test-http2-client-unescaped-path.js new file mode 100644 index 00000000000000..d92d40492e204c --- /dev/null +++ b/test/parallel/test-http2-client-unescaped-path.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustNotCall()); + +const count = 32; + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = count; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + // nghttp2 will catch the bad header value for us. + function doTest(i) { + const req = client.request({ ':path': `bad${String.fromCharCode(i)}path` }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 1' + })); + req.on('streamClosed', common.mustCall(maybeClose)); + } + + for (let i = 0; i <= count; i += 1) + doTest(i); +})); diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js new file mode 100644 index 00000000000000..4ce7da878e1fd2 --- /dev/null +++ b/test/parallel/test-http2-client-upload.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +// Verifies that uploading data from a client works + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const path = require('path'); + +const loc = path.join(common.fixturesDir, 'person.jpg'); +let fileData; + +assert(fs.existsSync(loc)); + +fs.readFile(loc, common.mustCall((err, data) => { + assert.ifError(err); + fileData = data; + + const server = http2.createServer(); + + server.on('stream', common.mustCall((stream) => { + let data = Buffer.alloc(0); + stream.on('data', (chunk) => data = Buffer.concat([data, chunk])); + stream.on('end', common.mustCall(() => { + assert.deepStrictEqual(data, fileData); + })); + stream.respond(); + stream.end(); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + fs.createReadStream(loc).pipe(req); + })); +})); diff --git a/test/parallel/test-http2-client-write-before-connect.js b/test/parallel/test-http2-client-write-before-connect.js new file mode 100644 index 00000000000000..f58fc5c43f69b6 --- /dev/null +++ b/test/parallel/test-http2-client-write-before-connect.js @@ -0,0 +1,53 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_METHOD, + HTTP2_METHOD_POST +} = h2.constants; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + let data = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'some data more data'); + })); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); + req.write('some data '); + req.write('more data'); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-compat-serverrequest-headers.js b/test/parallel/test-http2-compat-serverrequest-headers.js new file mode 100644 index 00000000000000..32af86314b1675 --- /dev/null +++ b/test/parallel/test-http2-compat-serverrequest-headers.js @@ -0,0 +1,70 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerRequest should have header helpers + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const expected = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + 'foo-bar': 'abc123' + }; + + assert.strictEqual(request.method, expected[':method']); + assert.strictEqual(request.scheme, expected[':scheme']); + assert.strictEqual(request.path, expected[':path']); + assert.strictEqual(request.url, expected[':path']); + assert.strictEqual(request.authority, expected[':authority']); + + const headers = request.headers; + for (const [name, value] of Object.entries(expected)) { + assert.strictEqual(headers[name], value); + } + + const rawHeaders = request.rawHeaders; + for (const [name, value] of Object.entries(expected)) { + const position = rawHeaders.indexOf(name); + assert.notStrictEqual(position, -1); + assert.strictEqual(rawHeaders[position + 1], value); + } + + request.url = '/one'; + assert.strictEqual(request.url, '/one'); + assert.strictEqual(request.path, '/one'); + + request.path = '/two'; + assert.strictEqual(request.url, '/two'); + assert.strictEqual(request.path, '/two'); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}`, + 'foo-bar': 'abc123' + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverrequest.js b/test/parallel/test-http2-compat-serverrequest.js new file mode 100644 index 00000000000000..d54f554848ce09 --- /dev/null +++ b/test/parallel/test-http2-compat-serverrequest.js @@ -0,0 +1,52 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const net = require('net'); + +// Http2ServerRequest should expose convenience properties + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const expected = { + statusCode: null, + version: '2.0', + httpVersionMajor: 2, + httpVersionMinor: 0 + }; + + assert.strictEqual(request.statusCode, expected.statusCode); + assert.strictEqual(request.httpVersion, expected.version); + assert.strictEqual(request.httpVersionMajor, expected.httpVersionMajor); + assert.strictEqual(request.httpVersionMinor, expected.httpVersionMinor); + + assert.ok(request.socket instanceof net.Socket); + assert.ok(request.connection instanceof net.Socket); + assert.strictEqual(request.socket, request.connection); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/foobar', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js new file mode 100644 index 00000000000000..68e438d62ff96d --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js @@ -0,0 +1,79 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Push a request & response + +const pushExpect = 'This is a server-initiated response'; +const servExpect = 'This is a client-initiated response'; + +const server = h2.createServer((request, response) => { + assert.strictEqual(response.stream.id % 2, 1); + response.write(servExpect); + + response.createPushResponse({ + ':path': '/pushed', + ':method': 'GET' + }, common.mustCall((error, push) => { + assert.ifError(error); + assert.strictEqual(push.stream.id % 2, 0); + push.end(pushExpect); + response.end(); + })); +}); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + + const client = h2.connect(`http://localhost:${port}`, common.mustCall(() => { + const headers = { + ':path': '/', + ':method': 'GET', + }; + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + client.destroy(); + server.close(); + } + } + + const req = client.request(headers); + + client.on('stream', common.mustCall((pushStream, headers) => { + assert.strictEqual(headers[':path'], '/pushed'); + assert.strictEqual(headers[':method'], 'GET'); + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':authority'], `localhost:${port}`); + + let actual = ''; + pushStream.on('push', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert(headers['date']); + })); + pushStream.setEncoding('utf8'); + pushStream.on('data', (chunk) => actual += chunk); + pushStream.on('end', common.mustCall(() => { + assert.strictEqual(actual, pushExpect); + maybeClose(); + })); + })); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert(headers['date']); + })); + + let actual = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(actual, servExpect); + maybeClose(); + })); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-end.js b/test/parallel/test-http2-compat-serverresponse-end.js new file mode 100644 index 00000000000000..1274f3d6b3c148 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-end.js @@ -0,0 +1,77 @@ +// Flags: --expose-http2 +'use strict'; + +const { strictEqual } = require('assert'); +const { mustCall, mustNotCall } = require('../common'); +const { + createServer, + connect, + constants: { + HTTP2_HEADER_STATUS, + HTTP_STATUS_OK + } +} = require('http2'); + +{ + // Http2ServerResponse.end callback is called only the first time, + // but may be invoked repeatedly without throwing errors. + const server = createServer(mustCall((request, response) => { + response.end(mustCall(() => { + server.close(); + })); + response.end(mustNotCall()); + })); + server.listen(0, mustCall(() => { + const { port } = server.address(); + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('data', mustNotCall()); + request.on('end', mustCall(() => client.destroy())); + request.end(); + request.resume(); + })); + })); +} + +{ + // Http2ServerResponse.end is not necessary on HEAD requests since the stream + // is already closed. Headers, however, can still be sent to the client. + const server = createServer(mustCall((request, response) => { + strictEqual(response.finished, true); + response.writeHead(HTTP_STATUS_OK, { foo: 'bar' }); + response.flushHeaders(); + response.end(mustNotCall()); + })); + server.listen(0, mustCall(() => { + const { port } = server.address(); + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'HEAD', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', mustCall((headers, flags) => { + strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK); + strictEqual(flags, 5); // the end of stream flag is set + strictEqual(headers.foo, 'bar'); + })); + request.on('data', mustNotCall()); + request.on('end', mustCall(() => { + client.destroy(); + server.close(); + })); + request.end(); + request.resume(); + })); + })); +} diff --git a/test/parallel/test-http2-compat-serverresponse-finished.js b/test/parallel/test-http2-compat-serverresponse-finished.js new file mode 100644 index 00000000000000..e5739e5ac3e2f6 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-finished.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.finished + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.on('finish', common.mustCall(function() { + server.close(); + })); + assert.strictEqual(response.finished, false); + response.end(); + assert.strictEqual(response.finished, true); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-flushheaders.js b/test/parallel/test-http2-compat-serverresponse-flushheaders.js new file mode 100644 index 00000000000000..4bfe4909121c19 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-flushheaders.js @@ -0,0 +1,43 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.flushHeaders + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.flushHeaders(); + response.flushHeaders(); // Idempotent + response.writeHead(400, {'foo-bar': 'abc123'}); // Ignored + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers, flags) { + assert.strictEqual(headers['foo-bar'], undefined); + assert.strictEqual(headers[':status'], 200); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-headers.js b/test/parallel/test-http2-compat-serverresponse-headers.js new file mode 100644 index 00000000000000..28bd36ce2e46c5 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-headers.js @@ -0,0 +1,83 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse should support checking and reading custom headers + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const real = 'foo-bar'; + const fake = 'bar-foo'; + const denormalised = ` ${real.toUpperCase()}\n\t`; + const expectedValue = 'abc123'; + + response.setHeader(real, expectedValue); + + assert.strictEqual(response.hasHeader(real), true); + assert.strictEqual(response.hasHeader(fake), false); + assert.strictEqual(response.hasHeader(denormalised), true); + assert.strictEqual(response.getHeader(real), expectedValue); + assert.strictEqual(response.getHeader(denormalised), expectedValue); + assert.strictEqual(response.getHeader(fake), undefined); + + response.removeHeader(fake); + assert.strictEqual(response.hasHeader(fake), false); + + response.setHeader(real, expectedValue); + assert.strictEqual(response.getHeader(real), expectedValue); + assert.strictEqual(response.hasHeader(real), true); + response.removeHeader(real); + assert.strictEqual(response.hasHeader(real), false); + + response.setHeader(denormalised, expectedValue); + assert.strictEqual(response.getHeader(denormalised), expectedValue); + assert.strictEqual(response.hasHeader(denormalised), true); + response.removeHeader(denormalised); + assert.strictEqual(response.hasHeader(denormalised), false); + + assert.throws(function() { + response.setHeader(':status', 'foobar'); + }, Error); + assert.throws(function() { + response.setHeader(real, null); + }, TypeError); + assert.throws(function() { + response.setHeader(real, undefined); + }, TypeError); + + response.setHeader(real, expectedValue); + const expectedHeaderNames = [real]; + assert.deepStrictEqual(response.getHeaderNames(), expectedHeaderNames); + const expectedHeaders = {[real]: expectedValue}; + assert.deepStrictEqual(response.getHeaders(), expectedHeaders); + + response.getHeaders()[fake] = fake; + assert.strictEqual(response.hasHeader(fake), false); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-statuscode.js b/test/parallel/test-http2-compat-serverresponse-statuscode.js new file mode 100644 index 00000000000000..201a63c379bc8b --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-statuscode.js @@ -0,0 +1,76 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse should have a statusCode property + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const expectedDefaultStatusCode = 200; + const realStatusCodes = { + continue: 100, + ok: 200, + multipleChoices: 300, + badRequest: 400, + internalServerError: 500 + }; + const fakeStatusCodes = { + tooLow: 99, + tooHigh: 600 + }; + + assert.strictEqual(response.statusCode, expectedDefaultStatusCode); + + assert.doesNotThrow(function() { + response.statusCode = realStatusCodes.ok; + response.statusCode = realStatusCodes.multipleChoices; + response.statusCode = realStatusCodes.badRequest; + response.statusCode = realStatusCodes.internalServerError; + }); + + assert.throws(function() { + response.statusCode = realStatusCodes.continue; + }, common.expectsError({ + code: 'ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + type: RangeError + })); + assert.throws(function() { + response.statusCode = fakeStatusCodes.tooLow; + }, common.expectsError({ + code: 'ERR_HTTP2_STATUS_INVALID', + type: RangeError + })); + assert.throws(function() { + response.statusCode = fakeStatusCodes.tooHigh; + }, common.expectsError({ + code: 'ERR_HTTP2_STATUS_INVALID', + type: RangeError + })); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage.js b/test/parallel/test-http2-compat-serverresponse-statusmessage.js new file mode 100644 index 00000000000000..08822c99390835 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage.js @@ -0,0 +1,52 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.writeHead should accept an optional status message + +const unsupportedWarned = common.mustCall(1); +process.on('warning', ({name, message}) => { + const expectedMessage = + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)'; + if (name === 'UnsupportedWarning' && message === expectedMessage) + unsupportedWarned(); +}); + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + const statusCode = 200; + const statusMessage = 'OK'; + const headers = {'foo-bar': 'abc123'}; + response.writeHead(statusCode, statusMessage, headers); + + response.on('finish', common.mustCall(function() { + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['foo-bar'], 'abc123'); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-compat-serverresponse-write-no-cb.js b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js new file mode 100644 index 00000000000000..2428cebd8bcf09 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js @@ -0,0 +1,98 @@ +// Flags: --expose-http2 +'use strict'; + +const { throws } = require('assert'); +const { mustCall, mustNotCall, expectsError } = require('../common'); +const { createServer, connect } = require('http2'); + +// Http2ServerResponse.write does not imply there is a callback + +const expectedError = expectsError({ + code: 'ERR_HTTP2_STREAM_CLOSED', + message: 'The stream is already closed' +}, 2); + +{ + const server = createServer(); + server.listen(0, mustCall(() => { + const port = server.address().port; + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.end(); + request.resume(); + })); + + server.once('request', mustCall((request, response) => { + client.destroy(); + response.stream.session.on('close', mustCall(() => { + response.on('error', mustNotCall()); + throws( + () => { response.write('muahaha'); }, + /The stream is already closed/ + ); + server.close(); + })); + })); + })); +} + +{ + const server = createServer(); + server.listen(0, mustCall(() => { + const port = server.address().port; + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'get', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.end(); + request.resume(); + })); + + server.once('request', mustCall((request, response) => { + client.destroy(); + response.stream.session.on('close', mustCall(() => { + response.write('muahaha', mustCall(expectedError)); + server.close(); + })); + })); + })); +} + +{ + const server = createServer(); + server.listen(0, mustCall(() => { + const port = server.address().port; + const url = `http://localhost:${port}`; + const client = connect(url, mustCall(() => { + const headers = { + ':path': '/', + ':method': 'get', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.end(); + request.resume(); + })); + + server.once('request', mustCall((request, response) => { + response.stream.session.on('close', mustCall(() => { + response.write('muahaha', 'utf8', mustCall(expectedError)); + server.close(); + })); + client.destroy(); + })); + })); +} diff --git a/test/parallel/test-http2-compat-serverresponse-writehead.js b/test/parallel/test-http2-compat-serverresponse-writehead.js new file mode 100644 index 00000000000000..b4c531d3393282 --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-writehead.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.writeHead should override previous headers + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.setHeader('foo-bar', 'def456'); + response.writeHead(500); + response.writeHead(418, {'foo-bar': 'abc123'}); // Override + + response.on('finish', common.mustCall(function() { + assert.doesNotThrow(() => { response.writeHead(300); }); + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers['foo-bar'], 'abc123'); + assert.strictEqual(headers[':status'], 418); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); diff --git a/test/parallel/test-http2-connect-method.js b/test/parallel/test-http2-connect-method.js new file mode 100644 index 00000000000000..05ff96a3cd1320 --- /dev/null +++ b/test/parallel/test-http2-connect-method.js @@ -0,0 +1,71 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); +const http2 = require('http2'); +const { URL } = require('url'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + NGHTTP2_CONNECT_ERROR +} = http2.constants; + +const server = net.createServer(common.mustCall((socket) => { + let data = ''; + socket.setEncoding('utf8'); + socket.on('data', (chunk) => data += chunk); + socket.on('end', common.mustCall(() => { + assert.strictEqual(data, 'hello'); + })); + socket.on('close', common.mustCall()); + socket.end('hello'); +})); + +server.listen(0, common.mustCall(() => { + + const port = server.address().port; + + const proxy = http2.createServer(); + proxy.on('stream', common.mustCall((stream, headers) => { + if (headers[HTTP2_HEADER_METHOD] !== 'CONNECT') { + stream.rstWithRefused(); + return; + } + const auth = new URL(`tcp://${headers[HTTP2_HEADER_AUTHORITY]}`); + assert.strictEqual(auth.hostname, 'localhost'); + assert.strictEqual(+auth.port, port); + const socket = net.connect(auth.port, auth.hostname, () => { + stream.respond(); + socket.pipe(stream); + stream.pipe(socket); + }); + socket.on('close', common.mustCall()); + socket.on('error', (error) => { + stream.rstStream(NGHTTP2_CONNECT_ERROR); + }); + })); + + proxy.listen(0, () => { + const client = http2.connect(`http://localhost:${proxy.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_METHOD]: 'CONNECT', + [HTTP2_HEADER_AUTHORITY]: `localhost:${port}`, + }); + + req.on('response', common.mustCall()); + let data = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => data += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'hello'); + client.destroy(); + proxy.close(); + server.close(); + })); + req.end('hello'); + }); +})); diff --git a/test/parallel/test-http2-connect.js b/test/parallel/test-http2-connect.js new file mode 100644 index 00000000000000..305ea034c902e4 --- /dev/null +++ b/test/parallel/test-http2-connect.js @@ -0,0 +1,29 @@ +// Flags: --expose-http2 +'use strict'; + +const { mustCall } = require('../common'); +const { doesNotThrow } = require('assert'); +const { createServer, connect } = require('http2'); + +const server = createServer(); +server.listen(0, mustCall(() => { + const authority = `http://localhost:${server.address().port}`; + const options = {}; + const listener = () => mustCall(); + + const clients = new Set(); + doesNotThrow(() => clients.add(connect(authority))); + doesNotThrow(() => clients.add(connect(authority, options))); + doesNotThrow(() => clients.add(connect(authority, options, listener()))); + doesNotThrow(() => clients.add(connect(authority, listener()))); + + for (const client of clients) { + client.once('connect', mustCall((headers) => { + client.destroy(); + clients.delete(client); + if (clients.size === 0) { + server.close(); + } + })); + } +})); diff --git a/test/parallel/test-http2-cookies.js b/test/parallel/test-http2-cookies.js new file mode 100644 index 00000000000000..297b3966df9f18 --- /dev/null +++ b/test/parallel/test-http2-cookies.js @@ -0,0 +1,62 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const setCookie = [ + 'a=b', + 'c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly' +]; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + + assert(Array.isArray(headers.abc)); + assert.strictEqual(headers.abc.length, 3); + assert.strictEqual(headers.abc[0], '1'); + assert.strictEqual(headers.abc[1], '2'); + assert.strictEqual(headers.abc[2], '3'); + assert.strictEqual(typeof headers.cookie, 'string'); + assert.strictEqual(headers.cookie, 'a=b; c=d; e=f'); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200, + 'set-cookie': setCookie + }); + + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + ':path': '/', + abc: [1, 2, 3], + cookie: ['a=b', 'c=d', 'e=f'], + }); + req.resume(); + + req.on('response', common.mustCall((headers) => { + assert(Array.isArray(headers['set-cookie'])); + assert.deepStrictEqual(headers['set-cookie'], setCookie, + 'set-cookie header does not match'); + })); + + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-create-client-connect.js b/test/parallel/test-http2-create-client-connect.js new file mode 100644 index 00000000000000..8173dc3d08658f --- /dev/null +++ b/test/parallel/test-http2-create-client-connect.js @@ -0,0 +1,88 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests http2.connect() + +const common = require('../common'); +const fs = require('fs'); +const h2 = require('http2'); +const path = require('path'); +const url = require('url'); +const URL = url.URL; + +{ + const server = h2.createServer(); + server.listen(0); + + server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const items = [ + [`http://localhost:${port}`], + [new URL(`http://localhost:${port}`)], + [url.parse(`http://localhost:${port}`)], + [{port: port}, {protocol: 'http:'}], + [{port: port, hostname: '127.0.0.1'}, {protocol: 'http:'}] + ]; + + let count = items.length; + + const maybeClose = common.mustCall((client) => { + client.destroy(); + if (--count === 0) { + setImmediate(() => server.close()); + } + }, items.length); + + items.forEach((i) => { + const client = + h2.connect.apply(null, i) + .on('connect', common.mustCall(() => maybeClose(client))); + }); + + // Will fail because protocol does not match the server. + h2.connect({port: port, protocol: 'https:'}) + .on('socketError', common.mustCall()); + })); +} + + +{ + + const options = { + key: fs.readFileSync(path.join(common.fixturesDir, 'keys/agent3-key.pem')), + cert: fs.readFileSync(path.join(common.fixturesDir, 'keys/agent3-cert.pem')) + }; + + const server = h2.createSecureServer(options); + server.listen(0); + + server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const opts = {rejectUnauthorized: false}; + + const items = [ + [`https://localhost:${port}`, opts], + [new URL(`https://localhost:${port}`), opts], + [url.parse(`https://localhost:${port}`), opts], + [{port: port, protocol: 'https:'}, opts], + [{port: port, hostname: '127.0.0.1', protocol: 'https:'}, opts] + ]; + + let count = items.length; + + const maybeClose = common.mustCall((client) => { + client.destroy(); + if (--count === 0) { + setImmediate(() => server.close()); + } + }, items.length); + + items.forEach((i) => { + const client = + h2.connect.apply(null, i) + .on('connect', common.mustCall(() => maybeClose(client))); + }); + })); +} diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js new file mode 100644 index 00000000000000..9b1cf4a0c9ee86 --- /dev/null +++ b/test/parallel/test-http2-create-client-secure-session.js @@ -0,0 +1,75 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const tls = require('tls'); +const h2 = require('http2'); + +function loadKey(keyname) { + return fs.readFileSync( + path.join(common.fixturesDir, 'keys', keyname), 'binary'); +} + +function onStream(stream, headers) { + const socket = stream.session.socket; + assert(headers[':authority'].startsWith(socket.servername)); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end(JSON.stringify({ + servername: socket.servername, + alpnProtocol: socket.alpnProtocol + })); +} + +function verifySecureSession(key, cert, ca, opts) { + const server = h2.createSecureServer({cert, key}); + server.on('stream', common.mustCall(onStream)); + server.listen(0); + server.on('listening', common.mustCall(function() { + const headers = { ':path': '/' }; + if (!opts) { + opts = {}; + } + opts.secureContext = tls.createSecureContext({ca}); + const client = h2.connect(`https://localhost:${this.address().port}`, opts, function() { + const req = client.request(headers); + + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200, 'status code is set'); + assert.strictEqual(headers['content-type'], 'text/html', + 'content type is set'); + assert(headers['date'], 'there is a date'); + })); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (d) => data += d); + req.on('end', common.mustCall(() => { + const jsonData = JSON.parse(data); + assert.strictEqual(jsonData.servername, opts.servername || 'localhost'); + assert.strictEqual(jsonData.alpnProtocol, 'h2'); + server.close(); + client.socket.destroy(); + })); + req.end(); + }); + })); +} + +// The server can be connected as 'localhost'. +verifySecureSession( + loadKey('agent8-key.pem'), + loadKey('agent8-cert.pem'), + loadKey('fake-startcom-root-cert.pem')); + +// Custom servername is specified. +verifySecureSession( + loadKey('agent1-key.pem'), + loadKey('agent1-cert.pem'), + loadKey('ca1-cert.pem'), + {servername: 'agent1'}); diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js new file mode 100644 index 00000000000000..c1c6ce1bfea62e --- /dev/null +++ b/test/parallel/test-http2-create-client-session.js @@ -0,0 +1,61 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const body = + '

this is some data

'; + +const server = h2.createServer(); +const count = 100; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream, count)); + +function onStream(stream, headers, flags) { + assert.strictEqual(headers[':scheme'], 'http'); + assert.ok(headers[':authority']); + assert.strictEqual(headers[':method'], 'GET'); + assert.strictEqual(flags, 5); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end(body); +} + +server.listen(0); + +let expected = count; + +server.on('listening', common.mustCall(function() { + + const client = h2.connect(`http://localhost:${this.address().port}`); + + const headers = { ':path': '/' }; + + for (let n = 0; n < count; n++) { + const req = client.request(headers); + + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200, 'status code is set'); + assert.strictEqual(headers['content-type'], 'text/html', + 'content type is set'); + assert(headers['date'], 'there is a date'); + })); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (d) => data += d); + req.on('end', common.mustCall(() => { + assert.strictEqual(body, data); + if (--expected === 0) { + server.close(); + client.destroy(); + } + })); + req.end(); + } + +})); diff --git a/test/parallel/test-http2-date-header.js b/test/parallel/test-http2-date-header.js new file mode 100644 index 00000000000000..d9a73b2ef61d4b --- /dev/null +++ b/test/parallel/test-http2-date-header.js @@ -0,0 +1,28 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + // Date header is defaulted + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall((headers) => { + // The date header must be set to a non-invalid value + assert.notStrictEqual((new Date()).toString(), 'Invalid Date'); + })); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-dont-override.js b/test/parallel/test-http2-dont-override.js new file mode 100644 index 00000000000000..55b29580fbc9f4 --- /dev/null +++ b/test/parallel/test-http2-dont-override.js @@ -0,0 +1,48 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const options = {}; + +const server = http2.createServer(options); + +// options are defaulted but the options are not modified +assert.deepStrictEqual(Object.keys(options), []); + +server.on('stream', common.mustCall((stream) => { + const headers = {}; + const options = {}; + stream.respond(headers, options); + + // The headers are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(headers), []); + + // Options are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(options), []); + + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const headers = {}; + const options = {}; + + const req = client.request(headers, options); + + // The headers are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(headers), []); + + // Options are defaulted but the original object is not modified + assert.deepStrictEqual(Object.keys(options), []); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js new file mode 100644 index 00000000000000..0c1a1bcceea255 --- /dev/null +++ b/test/parallel/test-http2-getpackedsettings.js @@ -0,0 +1,131 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x05, + 0x00, 0x00, 0x40, 0x00, 0x00, 0x04, 0x00, 0x00, + 0xff, 0xff, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); +const val = http2.getPackedSettings(http2.getDefaultSettings()); +assert.deepStrictEqual(val, check); + +[ + ['headerTableSize', 0], + ['headerTableSize', 2 ** 32 - 1], + ['initialWindowSize', 0], + ['initialWindowSize', 2 ** 32 - 1], + ['maxFrameSize', 16384], + ['maxFrameSize', 2 ** 24 - 1], + ['maxConcurrentStreams', 0], + ['maxConcurrentStreams', 2 ** 31 - 1], + ['maxHeaderListSize', 0], + ['maxHeaderListSize', 2 ** 32 - 1] +].forEach((i) => { + assert.doesNotThrow(() => http2.getPackedSettings({ [i[0]]: i[1] })); +}); + +assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: true })); +assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false })); + +[ + ['headerTableSize', -1], + ['headerTableSize', 2 ** 32], + ['initialWindowSize', -1], + ['initialWindowSize', 2 ** 32], + ['maxFrameSize', 16383], + ['maxFrameSize', 2 ** 24], + ['maxConcurrentStreams', -1], + ['maxConcurrentStreams', 2 ** 31], + ['maxHeaderListSize', -1], + ['maxHeaderListSize', 2 ** 32] +].forEach((i) => { + assert.throws(() => { + http2.getPackedSettings({ [i[0]]: i[1] }); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: RangeError, + message: `Invalid value for setting "${i[0]}": ${i[1]}` + })); +}); + +[ + 1, null, '', Infinity, new Date(), {}, NaN, [false] +].forEach((i) => { + assert.throws(() => { + http2.getPackedSettings({ enablePush: i }); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: TypeError, + message: `Invalid value for setting "enablePush": ${i}` + })); +}); + +{ + const check = Buffer.from([ + 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00, + 0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); + + const packed = http2.getPackedSettings({ + headerTableSize: 100, + initialWindowSize: 100, + maxFrameSize: 20000, + maxConcurrentStreams: 200, + maxHeaderListSize: 100, + enablePush: true, + foo: 'ignored' + }); + assert.strictEqual(packed.length, 36); + assert.deepStrictEqual(packed, check); +} + +{ + const packed = Buffer.from([ + 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x03, 0x00, 0x00, + 0x00, 0xc8, 0x00, 0x05, 0x00, 0x00, 0x4e, 0x20, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x64, 0x00, 0x06, 0x00, 0x00, 0x00, 0x64, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x01]); + + [1, true, '', [], {}, NaN].forEach((i) => { + assert.throws(() => { + http2.getUnpackedSettings(i); + }, common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "buf" argument must be one of type Buffer or Uint8Array' + })); + }); + + assert.throws(() => { + http2.getUnpackedSettings(packed.slice(5)); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', + type: RangeError, + message: 'Packed settings length must be a multiple of six' + })); + + const settings = http2.getUnpackedSettings(packed); + + assert(settings); + assert.strictEqual(settings.headerTableSize, 100); + assert.strictEqual(settings.initialWindowSize, 100); + assert.strictEqual(settings.maxFrameSize, 20000); + assert.strictEqual(settings.maxConcurrentStreams, 200); + assert.strictEqual(settings.maxHeaderListSize, 100); + assert.strictEqual(settings.enablePush, true); +} + +{ + const packed = Buffer.from([0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF]); + + assert.throws(() => { + http2.getUnpackedSettings(packed, {validate: true}); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: RangeError, + message: 'Invalid value for setting "maxConcurrentStreams": 4294967295' + })); +} diff --git a/test/parallel/test-http2-goaway-opaquedata.js b/test/parallel/test-http2-goaway-opaquedata.js new file mode 100644 index 00000000000000..e5904adf3bee99 --- /dev/null +++ b/test/parallel/test-http2-goaway-opaquedata.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); + +server.on('stream', common.mustCall((stream) => { + stream.session.shutdown({ + errorCode: 1, + opaqueData: data + }); + stream.end(); + stream.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 7' + }))); +})); + +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + client.on('goaway', common.mustCall((code, lastStreamID, buf) => { + assert.deepStrictEqual(code, 1); + assert.deepStrictEqual(lastStreamID, 0); + assert.deepStrictEqual(data, buf); + server.close(); + })); + const req = client.request({ ':path': '/' }); + req.resume(); + req.on('end', common.mustCall()); + req.end(); + +}); diff --git a/test/parallel/test-http2-head-request.js b/test/parallel/test-http2-head-request.js new file mode 100644 index 00000000000000..07f0eb6c93298f --- /dev/null +++ b/test/parallel/test-http2-head-request.js @@ -0,0 +1,57 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const errCheck = common.expectsError({ + type: Error, + message: 'write after end' +}, 2); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_STATUS, + HTTP2_METHOD_HEAD, +} = http2.constants; + +const server = http2.createServer(); +server.on('stream', (stream, headers) => { + + assert.strictEqual(headers[HTTP2_HEADER_METHOD], HTTP2_METHOD_HEAD); + + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + + // Because this is a head request, the outbound stream is closed automatically + stream.on('error', common.mustCall(errCheck)); + stream.write('data'); +}); + + +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_HEAD, + [HTTP2_HEADER_PATH]: '/' + }); + + // Because it is a HEAD request, the payload is meaningless. The + // option.endStream flag is set automatically making the stream + // non-writable. + req.on('error', common.mustCall(errCheck)); + req.write('data'); + + req.on('response', common.mustCall((headers, flags) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200); + assert.strictEqual(flags, 5); // the end of stream flag is set + })); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +}); diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js new file mode 100644 index 00000000000000..b0424397f22696 --- /dev/null +++ b/test/parallel/test-http2-https-fallback.js @@ -0,0 +1,146 @@ +// Flags: --expose-http2 +'use strict'; + +const { + fixturesDir, + mustCall, + mustNotCall +} = require('../common'); +const { strictEqual } = require('assert'); +const { join } = require('path'); +const { readFileSync } = require('fs'); +const { createSecureContext } = require('tls'); +const { createSecureServer, connect } = require('http2'); +const { get } = require('https'); +const { parse } = require('url'); +const { connect: tls } = require('tls'); + +const countdown = (count, done) => () => --count === 0 && done(); + +function loadKey(keyname) { + return readFileSync(join(fixturesDir, 'keys', keyname)); +} + +const key = loadKey('agent8-key.pem'); +const cert = loadKey('agent8-cert.pem'); +const ca = loadKey('fake-startcom-root-cert.pem'); + +const clientOptions = { secureContext: createSecureContext({ ca }) }; + +function onRequest(request, response) { + const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ? + request.stream.session : request; + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ + alpnProtocol, + httpVersion: request.httpVersion + })); +} + +function onSession(session) { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'https', + ':authority': `localhost:${this.server.address().port}` + }; + + const request = session.request(headers); + request.on('response', mustCall((headers) => { + strictEqual(headers[':status'], 200); + strictEqual(headers['content-type'], 'application/json'); + })); + request.setEncoding('utf8'); + let raw = ''; + request.on('data', (chunk) => { raw += chunk; }); + request.on('end', mustCall(() => { + const { alpnProtocol, httpVersion } = JSON.parse(raw); + strictEqual(alpnProtocol, 'h2'); + strictEqual(httpVersion, '2.0'); + + session.destroy(); + this.cleanup(); + })); + request.end(); +} + +// HTTP/2 & HTTP/1.1 server +{ + const server = createSecureServer( + { cert, key, allowHTTP1: true }, + mustCall(onRequest, 2) + ); + + server.listen(0); + + server.on('listening', mustCall(() => { + const { port } = server.address(); + const origin = `https://localhost:${port}`; + + const cleanup = countdown(2, () => server.close()); + + // HTTP/2 client + connect( + origin, + clientOptions, + mustCall(onSession.bind({ cleanup, server })) + ); + + // HTTP/1.1 client + get( + Object.assign(parse(origin), clientOptions), + mustCall((response) => { + strictEqual(response.statusCode, 200); + strictEqual(response.statusMessage, 'OK'); + strictEqual(response.headers['content-type'], 'application/json'); + + response.setEncoding('utf8'); + let raw = ''; + response.on('data', (chunk) => { raw += chunk; }); + response.on('end', mustCall(() => { + const { alpnProtocol, httpVersion } = JSON.parse(raw); + strictEqual(alpnProtocol, false); + strictEqual(httpVersion, '1.1'); + + cleanup(); + })); + }) + ); + })); +} + +// HTTP/2-only server +{ + const server = createSecureServer( + { cert, key }, + mustCall(onRequest) + ); + + server.on('unknownProtocol', mustCall((socket) => { + socket.destroy(); + }, 2)); + + server.listen(0); + + server.on('listening', mustCall(() => { + const { port } = server.address(); + const origin = `https://localhost:${port}`; + + const cleanup = countdown(3, () => server.close()); + + // HTTP/2 client + connect( + origin, + clientOptions, + mustCall(onSession.bind({ cleanup, server })) + ); + + // HTTP/1.1 client + get(Object.assign(parse(origin), clientOptions), mustNotCall()) + .on('error', mustCall(cleanup)); + + // Incompatible ALPN TLS client + tls(Object.assign({ port, ALPNProtocols: ['fake'] }, clientOptions)) + .on('error', mustCall(cleanup)); + })); +} diff --git a/test/parallel/test-http2-info-headers.js b/test/parallel/test-http2-info-headers.js new file mode 100755 index 00000000000000..c5d93d514f5d28 --- /dev/null +++ b/test/parallel/test-http2-info-headers.js @@ -0,0 +1,85 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +const status101regex = + /^HTTP status code 101 \(Switching Protocols\) is forbidden in HTTP\/2$/; +const afterRespondregex = + /^Cannot specify additional headers after response initiated$/; + +function onStream(stream, headers, flags) { + + assert.throws(() => stream.additionalHeaders({ ':status': 201 }), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_INFO_STATUS', + type: RangeError, + message: /^Invalid informational status code: 201$/ + })); + + assert.throws(() => stream.additionalHeaders({ ':status': 101 }), + common.expectsError({ + code: 'ERR_HTTP2_STATUS_101', + type: Error, + message: status101regex + })); + + // Can send more than one + stream.additionalHeaders({ ':status': 100 }); + stream.additionalHeaders({ ':status': 100 }); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + + assert.throws(() => stream.additionalHeaders({ abc: 123 }), + common.expectsError({ + code: 'ERR_HTTP2_HEADERS_AFTER_RESPOND', + type: Error, + message: afterRespondregex + })); + + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/'}); + + // The additionalHeaders method does not exist on client stream + assert.strictEqual(req.additionalHeaders, undefined); + + // Additional informational headers + req.on('headers', common.mustCall((headers) => { + assert.notStrictEqual(headers, undefined); + assert.strictEqual(headers[':status'], 100); + }, 2)); + + // Response headers + req.on('response', common.mustCall((headers) => { + assert.notStrictEqual(headers, undefined); + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + })); + + req.resume(); + + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-max-concurrent-streams.js b/test/parallel/test-http2-max-concurrent-streams.js new file mode 100644 index 00000000000000..6725a7c7545a90 --- /dev/null +++ b/test/parallel/test-http2-max-concurrent-streams.js @@ -0,0 +1,67 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_STATUS, + HTTP2_HEADER_PATH, + HTTP2_METHOD_POST +} = h2.constants; + +// Only allow one stream to be open at a time +const server = h2.createServer({ settings: { maxConcurrentStreams: 1 }}); + +// The stream handler must be called only once +server.on('stream', common.mustCall((stream) => { + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + stream.end('hello world'); +})); +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + let reqs = 2; + function onEnd() { + if (--reqs === 0) { + server.close(); + client.destroy(); + } + } + + client.on('remoteSettings', common.mustCall((settings) => { + assert.strictEqual(settings.maxConcurrentStreams, 1); + })); + + // This one should go through with no problems + const req1 = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST + }); + req1.on('aborted', common.mustNotCall()); + req1.on('response', common.mustCall()); + req1.resume(); + req1.on('end', onEnd); + req1.end(); + + // This one should be aborted + const req2 = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST + }); + req2.on('aborted', common.mustCall()); + req2.on('response', common.mustNotCall()); + req2.resume(); + req2.on('end', onEnd); + req2.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 7' + }))); + +})); diff --git a/test/parallel/test-http2-methods.js b/test/parallel/test-http2-methods.js new file mode 100644 index 00000000000000..1a8828f22c7363 --- /dev/null +++ b/test/parallel/test-http2-methods.js @@ -0,0 +1,48 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +const methods = [undefined, 'GET', 'POST', 'PATCH', 'FOO', 'A B C']; +let expected = methods.length; + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream, expected)); + +function onStream(stream, headers, flags) { + const method = headers[':method']; + assert.notStrictEqual(method, undefined); + assert(methods.includes(method), `method ${method} not included`); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const headers = { ':path': '/' }; + + methods.forEach((method) => { + headers[':method'] = method; + const req = client.request(headers); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + if (--expected === 0) { + server.close(); + client.destroy(); + } + })); + req.end(); + }); +})); diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js new file mode 100644 index 00000000000000..e356169d26e7e6 --- /dev/null +++ b/test/parallel/test-http2-misused-pseudoheaders.js @@ -0,0 +1,61 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + + [ + ':path', + ':authority', + ':method', + ':scheme' + ].forEach((i) => { + assert.throws(() => stream.respond({[i]: '/'}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' + })); + }); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + + // This will cause an error to be emitted on the stream because + // using a pseudo-header in a trailer is forbidden. + stream.on('fetchTrailers', (trailers) => { + trailers[':status'] = 'bar'; + }); + + stream.on('error', common.expectsError({ + code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' + })); + + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-multi-content-length.js b/test/parallel/test-http2-multi-content-length.js new file mode 100644 index 00000000000000..5dcd56990be5dd --- /dev/null +++ b/test/parallel/test-http2-multi-content-length.js @@ -0,0 +1,58 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = 3; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + // Request 1 will fail because there are two content-length header values + const req = client.request({ + ':method': 'POST', + 'content-length': 1, + 'Content-Length': 2 + }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: 'Header field "content-length" must have only a single value' + })); + req.on('error', common.mustCall(maybeClose)); + req.end('a'); + + // Request 2 will succeed + const req2 = client.request({ + ':method': 'POST', + 'content-length': 1 + }); + req2.resume(); + req2.on('end', common.mustCall(maybeClose)); + req2.end('a'); + + // Request 3 will fail because nghttp2 does not allow the content-length + // header to be set for non-payload bearing requests... + const req3 = client.request({ 'content-length': 1}); + req3.resume(); + req3.on('end', common.mustCall(maybeClose)); + req3.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 1' + })); +})); diff --git a/test/parallel/test-http2-multiheaders.js b/test/parallel/test-http2-multiheaders.js new file mode 100644 index 00000000000000..d7b8f56d51ac30 --- /dev/null +++ b/test/parallel/test-http2-multiheaders.js @@ -0,0 +1,60 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +const src = Object.create(null); +src.accept = [ 'abc', 'def' ]; +src.Accept = 'ghijklmnop'; +src['www-authenticate'] = 'foo'; +src['WWW-Authenticate'] = 'bar'; +src['WWW-AUTHENTICATE'] = 'baz'; +src['proxy-authenticate'] = 'foo'; +src['Proxy-Authenticate'] = 'bar'; +src['PROXY-AUTHENTICATE'] = 'baz'; +src['x-foo'] = 'foo'; +src['X-Foo'] = 'bar'; +src['X-FOO'] = 'baz'; +src.constructor = 'foo'; +src.Constructor = 'bar'; +src.CONSTRUCTOR = 'baz'; +// eslint-disable-next-line no-proto +src['__proto__'] = 'foo'; +src['__PROTO__'] = 'bar'; +src['__Proto__'] = 'baz'; + +function checkHeaders(headers) { + assert.deepStrictEqual(headers['accept'], + [ 'abc', 'def', 'ghijklmnop' ]); + assert.deepStrictEqual(headers['www-authenticate'], + [ 'foo', 'bar', 'baz' ]); + assert.deepStrictEqual(headers['proxy-authenticate'], + [ 'foo', 'bar', 'baz' ]); + assert.deepStrictEqual(headers['x-foo'], [ 'foo', 'bar', 'baz' ]); + assert.deepStrictEqual(headers['constructor'], [ 'foo', 'bar', 'baz' ]); + // eslint-disable-next-line no-proto + assert.deepStrictEqual(headers['__proto__'], [ 'foo', 'bar', 'baz' ]); +} + +server.on('stream', common.mustCall((stream, headers) => { + assert.strictEqual(headers[':path'], '/'); + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':method'], 'GET'); + checkHeaders(headers); + stream.respond(src); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(src); + req.on('response', common.mustCall(checkHeaders)); + req.on('streamClosed', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-multiplex.js b/test/parallel/test-http2-multiplex.js new file mode 100644 index 00000000000000..b6b81c73a654c6 --- /dev/null +++ b/test/parallel/test-http2-multiplex.js @@ -0,0 +1,59 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests opening 100 concurrent simultaneous uploading streams over a single +// connection and makes sure that the data for each is appropriately echoed. + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +const count = 100; + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.pipe(stream); +}, count)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = count; + + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + function doRequest() { + const req = client.request({ ':method': 'POST '}); + + let data = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => data += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'abcdefghij'); + maybeClose(); + })); + + let n = 0; + function writeChunk() { + if (n < 10) { + req.write(String.fromCharCode(97 + n)); + setTimeout(writeChunk, 10); + } else { + req.end(); + } + n++; + } + + writeChunk(); + } + + for (let n = 0; n < count; n++) + doRequest(); +})); diff --git a/test/parallel/test-http2-noflag.js b/test/parallel/test-http2-noflag.js new file mode 100644 index 00000000000000..a1e0e8b72c79e9 --- /dev/null +++ b/test/parallel/test-http2-noflag.js @@ -0,0 +1,8 @@ +// The --expose-http2 flag is not set +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.throws(() => require('http2'), + /^Error: Cannot find module 'http2'$/); diff --git a/test/parallel/test-http2-options-max-headers-block-length.js b/test/parallel/test-http2-options-max-headers-block-length.js new file mode 100644 index 00000000000000..41e8d549b4a9a3 --- /dev/null +++ b/test/parallel/test-http2-options-max-headers-block-length.js @@ -0,0 +1,48 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustNotCall()); +server.listen(0); + +server.on('listening', common.mustCall(() => { + + // Setting the maxSendHeaderBlockLength, then attempting to send a + // headers block that is too big should cause a 'meError' to + // be emitted, and will cause the stream to be shutdown. + const options = { + maxSendHeaderBlockLength: 10 + }; + + const client = h2.connect(`http://localhost:${server.address().port}`, + options); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + + req.on('frameError', common.mustCall((type, code) => { + assert.strictEqual(code, h2.constants.NGHTTP2_ERR_FRAME_SIZE_ERROR); + })); + + req.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 7' + }))); + + req.end(); + +})); diff --git a/test/parallel/test-http2-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js new file mode 100644 index 00000000000000..1173b58e287de2 --- /dev/null +++ b/test/parallel/test-http2-options-max-reserved-streams.js @@ -0,0 +1,73 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall((stream) => { + stream.respond({ ':status': 200 }); + + // The first pushStream will complete as normal + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${server.address().port}`, + }, common.mustCall((pushedStream) => { + pushedStream.respond({ ':status': 200 }); + pushedStream.end(); + pushedStream.on('aborted', common.mustNotCall()); + })); + + // The second pushStream will be aborted because the client + // will reject it due to the maxReservedRemoteStreams option + // being set to only 1 + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${server.address().port}`, + }, common.mustCall((pushedStream) => { + pushedStream.respond({ ':status': 200 }); + pushedStream.on('aborted', common.mustCall()); + pushedStream.on('error', common.mustCall(common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: 'Stream closed with error code 8' + }))); + })); + + stream.end('hello world'); +})); +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const options = { + maxReservedRemoteStreams: 1 + }; + + const client = h2.connect(`http://localhost:${server.address().port}`, + options); + + const req = client.request({ ':path': '/' }); + + // Because maxReservedRemoteStream is 1, the stream event + // must only be emitted once, even tho the server sends + // two push streams. + client.on('stream', common.mustCall((stream) => { + stream.resume(); + stream.on('end', common.mustCall()); + })); + + req.on('response', common.mustCall()); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-padding-callback.js b/test/parallel/test-http2-padding-callback.js new file mode 100644 index 00000000000000..610b636fdcc263 --- /dev/null +++ b/test/parallel/test-http2-padding-callback.js @@ -0,0 +1,50 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const { PADDING_STRATEGY_CALLBACK } = h2.constants; + +function selectPadding(frameLen, max) { + assert.strictEqual(typeof frameLen, 'number'); + assert.strictEqual(typeof max, 'number'); + assert(max >= frameLen); + return max; +} + +// selectPadding will be called three times: +// 1. For the client request headers frame +// 2. For the server response headers frame +// 3. For the server response data frame +const options = { + paddingStrategy: PADDING_STRATEGY_CALLBACK, + selectPadding: common.mustCall(selectPadding, 3) +}; + +const server = h2.createServer(options); +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`, + options); + + const req = client.request({ ':path': '/' }); + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-priority-event.js b/test/parallel/test-http2-priority-event.js new file mode 100644 index 00000000000000..bbb248265e8402 --- /dev/null +++ b/test/parallel/test-http2-priority-event.js @@ -0,0 +1,60 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onPriority(stream, parent, weight, exclusive) { + assert.strictEqual(stream, 1); + assert.strictEqual(parent, 0); + assert.strictEqual(weight, 1); + assert.strictEqual(exclusive, false); +} + +function onStream(stream, headers, flags) { + stream.priority({ + parent: 0, + weight: 1, + exclusive: false + }); + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('priority', common.mustCall(onPriority)); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':path': '/'}); + + client.on('connect', () => { + req.priority({ + parent: 0, + weight: 1, + exclusive: false + }); + }); + + req.on('priority', common.mustCall(onPriority)); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-respond-file-204.js b/test/parallel/test-http2-respond-file-204.js new file mode 100644 index 00000000000000..66840e57adac91 --- /dev/null +++ b/test/parallel/test-http2-respond-file-204.js @@ -0,0 +1,41 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_STATUS +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + assert.throws(() => { + stream.respondWithFile(fname, { + [HTTP2_HEADER_STATUS]: 204, + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }); + }, common.expectsError({ + code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN', + type: Error, + message: 'Responses with 204 status must not have a payload' + })); + stream.respond({}); + stream.end(); +}); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall()); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-304.js b/test/parallel/test-http2-respond-file-304.js new file mode 100644 index 00000000000000..0b279223f14d5d --- /dev/null +++ b/test/parallel/test-http2-respond-file-304.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_STATUS +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFile(fname, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }, { + statCheck(stat, headers) { + // abort the send and return a 304 Not Modified instead + stream.respond({ [HTTP2_HEADER_STATUS]: 304 }); + return false; + } + }); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 304); + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE, undefined]); + })); + + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-compat.js b/test/parallel/test-http2-respond-file-compat.js new file mode 100644 index 00000000000000..be256ee2bfa416 --- /dev/null +++ b/test/parallel/test-http2-respond-file-compat.js @@ -0,0 +1,23 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const path = require('path'); + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); + +const server = http2.createServer(common.mustCall((request, response) => { + response.stream.respondWithFile(fname); +})); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall()); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + req.end(); + req.resume(); +}); diff --git a/test/parallel/test-http2-respond-file-fd-invalid.js b/test/parallel/test-http2-respond-file-fd-invalid.js new file mode 100644 index 00000000000000..f46dbd9dc1d1a4 --- /dev/null +++ b/test/parallel/test-http2-respond-file-fd-invalid.js @@ -0,0 +1,37 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); + +const { + NGHTTP2_INTERNAL_ERROR +} = http2.constants; + +const errorCheck = common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: `Stream closed with error code ${NGHTTP2_INTERNAL_ERROR}` +}, 2); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFD(common.firstInvalidFD()); + stream.on('error', common.mustCall(errorCheck)); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall()); + req.on('error', common.mustCall(errorCheck)); + req.on('data', common.mustNotCall()); + req.on('end', common.mustCall(() => { + assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-fd.js b/test/parallel/test-http2-respond-file-fd.js new file mode 100644 index 00000000000000..4e982bca3cfd35 --- /dev/null +++ b/test/parallel/test-http2-respond-file-fd.js @@ -0,0 +1,46 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_CONTENT_LENGTH +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); +const data = fs.readFileSync(fname); +const stat = fs.statSync(fname); +const fd = fs.openSync(fname, 'r'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFD(fd, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', + [HTTP2_HEADER_CONTENT_LENGTH]: stat.size, + }); +}); +server.on('close', common.mustCall(() => fs.closeSync(fd))); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length); + })); + req.setEncoding('utf8'); + let check = ''; + req.on('data', (chunk) => check += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8')); + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file-push.js b/test/parallel/test-http2-respond-file-push.js new file mode 100644 index 00000000000000..1c2476f173463a --- /dev/null +++ b/test/parallel/test-http2-respond-file-push.js @@ -0,0 +1,81 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_LAST_MODIFIED +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); +const data = fs.readFileSync(fname); +const stat = fs.statSync(fname); +const fd = fs.openSync(fname, 'r'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond({}); + stream.end(); + + stream.pushStream({ + ':path': '/file.txt', + ':method': 'GET' + }, (stream) => { + stream.respondWithFD(fd, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain', + [HTTP2_HEADER_CONTENT_LENGTH]: stat.size, + [HTTP2_HEADER_LAST_MODIFIED]: stat.mtime.toUTCString() + }); + }); + + stream.end(); +}); + +server.on('close', common.mustCall(() => fs.closeSync(fd))); + +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + + let expected = 2; + function maybeClose() { + if (--expected === 0) { + server.close(); + client.destroy(); + } + } + + const req = client.request({}); + + req.on('response', common.mustCall()); + + client.on('stream', common.mustCall((stream, headers) => { + + stream.on('push', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length); + assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED], + stat.mtime.toUTCString()); + })); + + stream.setEncoding('utf8'); + let check = ''; + stream.on('data', (chunk) => check += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8')); + maybeClose(); + })); + + })); + + req.resume(); + req.on('end', maybeClose); + + req.end(); +}); diff --git a/test/parallel/test-http2-respond-file.js b/test/parallel/test-http2-respond-file.js new file mode 100644 index 00000000000000..81babb58fadcd1 --- /dev/null +++ b/test/parallel/test-http2-respond-file.js @@ -0,0 +1,51 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const { + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_LAST_MODIFIED +} = http2.constants; + +const fname = path.resolve(common.fixturesDir, 'elipses.txt'); +const data = fs.readFileSync(fname); +const stat = fs.statSync(fname); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respondWithFile(fname, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }, { + statCheck(stat, headers) { + headers[HTTP2_HEADER_LAST_MODIFIED] = stat.mtime.toUTCString(); + headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size; + } + }); +}); +server.listen(0, () => { + + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length); + assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED], + stat.mtime.toUTCString()); + })); + req.setEncoding('utf8'); + let check = ''; + req.on('data', (chunk) => check += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8')); + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-response-splitting.js b/test/parallel/test-http2-response-splitting.js new file mode 100644 index 00000000000000..088c675389f5ba --- /dev/null +++ b/test/parallel/test-http2-response-splitting.js @@ -0,0 +1,75 @@ +// Flags: --expose-http2 +'use strict'; + +// Response splitting is no longer an issue with HTTP/2. The underlying +// nghttp2 implementation automatically strips out the header values that +// contain invalid characters. + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const { URL } = require('url'); + +// Response splitting example, credit: Amit Klein, Safebreach +const str = '/welcome?lang=bar%c4%8d%c4%8aContent­Length:%200%c4%8d%c4%8a%c' + + '4%8d%c4%8aHTTP/1.1%20200%20OK%c4%8d%c4%8aContent­Length:%202' + + '0%c4%8d%c4%8aLast­Modified:%20Mon,%2027%20Oct%202003%2014:50:18' + + '%20GMT%c4%8d%c4%8aContent­Type:%20text/html%c4%8d%c4%8a%c4%8' + + 'd%c4%8a%3chtml%3eGotcha!%3c/html%3e'; + +// Response splitting example, credit: Сковорода Никита Андреевич (@ChALkeR) +const x = 'fooഊSet-Cookie: foo=barഊഊ'; +const y = 'foo⠊Set-Cookie: foo=bar'; + +let remaining = 3; + +function makeUrl(headers) { + return `${headers[':scheme']}://${headers[':authority']}${headers[':path']}`; +} + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers) => { + + const obj = Object.create(null); + switch (remaining--) { + case 3: + const url = new URL(makeUrl(headers)); + obj[':status'] = 302; + obj.Location = `/foo?lang=${url.searchParams.get('lang')}`; + break; + case 2: + obj.foo = x; + break; + case 1: + obj.foo = y; + break; + } + stream.respond(obj); + stream.end(); +}, 3)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + function maybeClose() { + if (remaining === 0) { + server.close(); + client.destroy(); + } + } + + function doTest(path, key, expected) { + const req = client.request({ ':path': path }); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers.foo, undefined); + assert.strictEqual(headers.location, undefined); + })); + req.resume(); + req.on('end', common.mustCall(maybeClose)); + } + + doTest(str, 'location', str); + doTest('/', 'foo', x); + doTest('/', 'foo', y); + +})); diff --git a/test/parallel/test-http2-serve-file.js b/test/parallel/test-http2-serve-file.js new file mode 100644 index 00000000000000..109270327489eb --- /dev/null +++ b/test/parallel/test-http2-serve-file.js @@ -0,0 +1,82 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const path = require('path'); +const tls = require('tls'); + +const ajs_data = fs.readFileSync(path.resolve(common.fixturesDir, 'a.js'), + 'utf8'); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_STATUS +} = http2.constants; + +function loadKey(keyname) { + return fs.readFileSync( + path.join(common.fixturesDir, 'keys', keyname), 'binary'); +} + +const key = loadKey('agent8-key.pem'); +const cert = loadKey('agent8-cert.pem'); +const ca = loadKey('fake-startcom-root-cert.pem'); + +const server = http2.createSecureServer({key, cert}); + +server.on('stream', (stream, headers) => { + const name = headers[HTTP2_HEADER_PATH].slice(1); + const file = path.resolve(common.fixturesDir, name); + fs.stat(file, (err, stat) => { + if (err != null || stat.isDirectory()) { + stream.respond({ [HTTP2_HEADER_STATUS]: 404 }); + stream.end(); + } else { + stream.respond({ [HTTP2_HEADER_STATUS]: 200 }); + const str = fs.createReadStream(file); + str.pipe(stream); + } + }); +}); + +server.listen(8000, () => { + + const secureContext = tls.createSecureContext({ca}); + const client = http2.connect(`https://localhost:${server.address().port}`, + { secureContext }); + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + client.destroy(); + server.close(); + } + } + + // Request for a file that does exist, response is 200 + const req1 = client.request({ [HTTP2_HEADER_PATH]: '/a.js' }, + { endStream: true }); + req1.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 200); + })); + let req1_data = ''; + req1.setEncoding('utf8'); + req1.on('data', (chunk) => req1_data += chunk); + req1.on('end', common.mustCall(() => { + assert.strictEqual(req1_data, ajs_data); + maybeClose(); + })); + + // Request for a file that does not exist, response is 404 + const req2 = client.request({ [HTTP2_HEADER_PATH]: '/does_not_exist' }, + { endStream: true }); + req2.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_STATUS], 404); + })); + req2.on('data', common.mustNotCall()); + req2.on('end', common.mustCall(() => maybeClose())); + +}); diff --git a/test/parallel/test-http2-server-destroy-before-additional.js b/test/parallel/test-http2-server-destroy-before-additional.js new file mode 100644 index 00000000000000..9aff3b6abf987b --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-additional.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.additionalHeaders({}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-destroy-before-push.js b/test/parallel/test-http2-server-destroy-before-push.js new file mode 100644 index 00000000000000..9c1628a18bf671 --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-push.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.pushStream({}, common.mustNotCall()), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-destroy-before-respond.js b/test/parallel/test-http2-server-destroy-before-respond.js new file mode 100644 index 00000000000000..acb020d5bd7e3e --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-respond.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.respond({}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-destroy-before-write.js b/test/parallel/test-http2-server-destroy-before-write.js new file mode 100644 index 00000000000000..533aace208c44b --- /dev/null +++ b/test/parallel/test-http2-server-destroy-before-write.js @@ -0,0 +1,38 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.session.destroy(); + assert.throws(() => stream.write('data'), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + type: Error + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('response', common.mustNotCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-server-push-disabled.js b/test/parallel/test-http2-server-push-disabled.js new file mode 100644 index 00000000000000..1fedf2229378ef --- /dev/null +++ b/test/parallel/test-http2-server-push-disabled.js @@ -0,0 +1,53 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('session', common.mustCall((session) => { + // Verify that the settings disabling push is received + session.on('remoteSettings', common.mustCall((settings) => { + assert.strictEqual(settings.enablePush, false); + })); +})); + +server.on('stream', common.mustCall((stream) => { + + // The client has disabled push streams, so pushAllowed must be false, + // and pushStream() must throw. + assert.strictEqual(stream.pushAllowed, false); + + assert.throws(() => { + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${server.address().port}`, + }, common.mustNotCall()); + }, common.expectsError({ + code: 'ERR_HTTP2_PUSH_DISABLED', + type: Error + })); + + stream.respond({ ':status': 200 }); + stream.end('test'); +})); + +server.listen(0, common.mustCall(() => { + const options = {settings: { enablePush: false }}; + const client = http2.connect(`http://localhost:${server.address().port}`, + options); + const req = client.request({ ':path': '/' }); + + // Because push stream sre disabled, this must not be called. + client.on('stream', common.mustNotCall()); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-server-push-stream.js b/test/parallel/test-http2-server-push-stream.js new file mode 100644 index 00000000000000..c2f34ed517c6c9 --- /dev/null +++ b/test/parallel/test-http2-server-push-stream.js @@ -0,0 +1,58 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream, headers) => { + const port = server.address().port; + if (headers[':path'] === '/') { + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${port}`, + }, (push, headers) => { + push.respond({ + 'content-type': 'text/html', + ':status': 200, + 'x-push-data': 'pushed by server', + }); + push.end('pushed by server data'); + stream.end('test'); + }); + } + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const headers = { ':path': '/' }; + const client = http2.connect(`http://localhost:${port}`); + const req = client.request(headers); + + client.on('stream', common.mustCall((stream, headers) => { + assert.strictEqual(headers[':scheme'], 'http'); + assert.strictEqual(headers[':path'], '/foobar'); + assert.strictEqual(headers[':authority'], `localhost:${port}`); + stream.on('push', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + assert.strictEqual(headers['content-type'], 'text/html'); + assert.strictEqual(headers['x-push-data'], 'pushed by server'); + })); + })); + + let data = ''; + + req.on('data', common.mustCall((d) => data += d)); + req.on('end', common.mustCall(() => { + assert.strictEqual(data, 'test'); + server.close(); + client.destroy(); + })); + req.end(); +})); diff --git a/test/parallel/test-http2-server-rst-before-respond.js b/test/parallel/test-http2-server-rst-before-respond.js new file mode 100644 index 00000000000000..015e11311f7544 --- /dev/null +++ b/test/parallel/test-http2-server-rst-before-respond.js @@ -0,0 +1,45 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.rstStream(); + + assert.throws(() => { + stream.additionalHeaders({ + ':status': 123, + abc: 123 + }); + }, common.expectsError({ + code: 'ERR_HTTP2_INVALID_STREAM', + message: /^The stream has been destroyed$/ + })); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.on('headers', common.mustNotCall()); + + req.on('streamClosed', common.mustCall((code) => { + assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, code); + server.close(); + client.destroy(); + })); + + req.on('response', common.mustNotCall()); + +})); diff --git a/test/parallel/test-http2-server-rst-stream.js b/test/parallel/test-http2-server-rst-stream.js new file mode 100644 index 00000000000000..30a9db49afc239 --- /dev/null +++ b/test/parallel/test-http2-server-rst-stream.js @@ -0,0 +1,72 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_METHOD_POST, + NGHTTP2_CANCEL, + NGHTTP2_NO_ERROR, + NGHTTP2_PROTOCOL_ERROR, + NGHTTP2_REFUSED_STREAM, + NGHTTP2_INTERNAL_ERROR +} = http2.constants; + +const errCheck = common.expectsError({ code: 'ERR_HTTP2_STREAM_ERROR' }, 8); + +function checkRstCode(rstMethod, expectRstCode) { + const server = http2.createServer(); + server.on('stream', (stream, headers, flags) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write('test'); + if (rstMethod === 'rstStream') + stream[rstMethod](expectRstCode); + else + stream[rstMethod](); + + if (expectRstCode > NGHTTP2_NO_ERROR) { + stream.on('error', common.mustCall(errCheck)); + } + }); + + server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const headers = { + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST + }; + const req = client.request(headers); + + req.setEncoding('utf8'); + req.on('streamClosed', common.mustCall((actualRstCode) => { + assert.strictEqual( + expectRstCode, actualRstCode, `${rstMethod} is not match rstCode`); + server.close(); + client.destroy(); + })); + req.on('data', common.mustCall()); + req.on('aborted', common.mustCall()); + req.on('end', common.mustCall()); + + if (expectRstCode > NGHTTP2_NO_ERROR) { + req.on('error', common.mustCall(errCheck)); + } + + })); +} + +checkRstCode('rstStream', NGHTTP2_NO_ERROR); +checkRstCode('rstWithNoError', NGHTTP2_NO_ERROR); +checkRstCode('rstWithProtocolError', NGHTTP2_PROTOCOL_ERROR); +checkRstCode('rstWithCancel', NGHTTP2_CANCEL); +checkRstCode('rstWithRefuse', NGHTTP2_REFUSED_STREAM); +checkRstCode('rstWithInternalError', NGHTTP2_INTERNAL_ERROR); diff --git a/test/parallel/test-http2-server-set-header.js b/test/parallel/test-http2-server-set-header.js new file mode 100644 index 00000000000000..8b0e82fd8a50d5 --- /dev/null +++ b/test/parallel/test-http2-server-set-header.js @@ -0,0 +1,36 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const body = + '

this is some data

'; + +const server = http2.createServer((req, res) => { + res.setHeader('foobar', 'baz'); + res.setHeader('X-POWERED-BY', 'node-test'); + res.end(body); +}); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const headers = { ':path': '/' }; + const req = client.request(headers); + req.setEncoding('utf8'); + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers['foobar'], 'baz'); + assert.strictEqual(headers['x-powered-by'], 'node-test'); + })); + + let data = ''; + req.on('data', (d) => data += d); + req.on('end', () => { + assert.strictEqual(body, data); + server.close(); + client.destroy(); + }); + req.end(); +})); + +server.on('error', common.mustNotCall()); diff --git a/test/parallel/test-http2-server-shutdown-before-respond.js b/test/parallel/test-http2-server-shutdown-before-respond.js new file mode 100644 index 00000000000000..c7ffee7e31532e --- /dev/null +++ b/test/parallel/test-http2-server-shutdown-before-respond.js @@ -0,0 +1,32 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + const session = stream.session; + stream.session.shutdown({graceful: true}, common.mustCall(() => { + session.destroy(); + })); + stream.respond({}); + stream.end('data'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':path': '/' }); + + req.resume(); + req.on('end', common.mustCall(() => server.close())); + req.end(); +})); diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js new file mode 100644 index 00000000000000..c10bbd0ccbe0c5 --- /dev/null +++ b/test/parallel/test-http2-server-socket-destroy.js @@ -0,0 +1,57 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); +const assert = require('assert'); + +const { + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_METHOD_POST +} = h2.constants; + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.write('test'); + + const socket = stream.session.socket; + + // When the socket is destroyed, the close events must be triggered + // on the socket, server and session. + socket.on('close', common.mustCall()); + server.on('close', common.mustCall()); + stream.session.on('close', common.mustCall(() => server.close())); + + // Also, the aborted event must be triggered on the stream + stream.on('aborted', common.mustCall()); + + assert.notStrictEqual(stream.session, undefined); + socket.destroy(); + assert.strictEqual(stream.session, undefined); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); + + req.on('aborted', common.mustCall()); + req.on('end', common.mustCall()); + req.on('response', common.mustCall()); + req.on('data', common.mustCall()); + + client.on('close', common.mustCall()); +})); diff --git a/test/parallel/test-http2-server-startup.js b/test/parallel/test-http2-server-startup.js new file mode 100644 index 00000000000000..c2e94f3ac4502a --- /dev/null +++ b/test/parallel/test-http2-server-startup.js @@ -0,0 +1,78 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests the basic operation of creating a plaintext or TLS +// HTTP2 server. The server does not do anything at this point +// other than start listening. + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); +const path = require('path'); +const tls = require('tls'); +const net = require('net'); +const fs = require('fs'); + +const options = { + key: fs.readFileSync( + path.resolve(common.fixturesDir, 'keys/agent2-key.pem')), + cert: fs.readFileSync( + path.resolve(common.fixturesDir, 'keys/agent2-cert.pem')) +}; + +// There should not be any throws +assert.doesNotThrow(() => { + + const serverTLS = http2.createSecureServer(options, () => {}); + + serverTLS.listen(0, common.mustCall(() => serverTLS.close())); + + // There should not be an error event reported either + serverTLS.on('error', common.mustNotCall()); +}); + +// There should not be any throws +assert.doesNotThrow(() => { + const server = http2.createServer(options, common.mustNotCall()); + + server.listen(0, common.mustCall(() => server.close())); + + // There should not be an error event reported either + server.on('error', common.mustNotCall()); +}); + +// Test the plaintext server socket timeout +{ + let client; + const server = http2.createServer(); + server.on('timeout', common.mustCall(() => { + server.close(); + if (client) + client.end(); + })); + server.setTimeout(common.platformTimeout(1000)); + server.listen(0, common.mustCall(() => { + const port = server.address().port; + client = net.connect(port, common.mustCall()); + })); +} + +// Test the secure server socket timeout +{ + let client; + const server = http2.createSecureServer(options); + server.on('timeout', common.mustCall(() => { + server.close(); + if (client) + client.end(); + })); + server.setTimeout(common.platformTimeout(1000)); + server.listen(0, common.mustCall(() => { + const port = server.address().port; + client = tls.connect({ + port: port, + rejectUnauthorized: false, + ALPNProtocols: ['h2'] + }, common.mustCall()); + })); +} diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js new file mode 100644 index 00000000000000..bc9877e23e38f6 --- /dev/null +++ b/test/parallel/test-http2-session-settings.js @@ -0,0 +1,110 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +server.on('stream', common.mustCall(onStream)); + +function assertSettings(settings) { + assert.strictEqual(typeof settings, 'object'); + assert.strictEqual(typeof settings.headerTableSize, 'number'); + assert.strictEqual(typeof settings.enablePush, 'boolean'); + assert.strictEqual(typeof settings.initialWindowSize, 'number'); + assert.strictEqual(typeof settings.maxFrameSize, 'number'); + assert.strictEqual(typeof settings.maxConcurrentStreams, 'number'); + assert.strictEqual(typeof settings.maxHeaderListSize, 'number'); +} + +function onStream(stream, headers, flags) { + + assertSettings(stream.session.localSettings); + assertSettings(stream.session.remoteSettings); + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`, { + settings: { + enablePush: false, + initialWindowSize: 123456 + } + }); + + client.on('localSettings', common.mustCall((settings) => { + assert(settings); + assert.strictEqual(settings.enablePush, false); + assert.strictEqual(settings.initialWindowSize, 123456); + assert.strictEqual(settings.maxFrameSize, 16384); + }, 2)); + client.on('remoteSettings', common.mustCall((settings) => { + assert(settings); + })); + + const headers = { ':path': '/' }; + + const req = client.request(headers); + + req.on('connect', common.mustCall(() => { + // pendingSettingsAck will be true if a SETTINGS frame + // has been sent but we are still waiting for an acknowledgement + assert(client.pendingSettingsAck); + })); + + // State will only be valid after connect event is emitted + req.on('ready', common.mustCall(() => { + assert.doesNotThrow(() => { + client.settings({ + maxHeaderListSize: 1 + }); + }); + + // Verify valid error ranges + [ + ['headerTableSize', -1], + ['headerTableSize', 2 ** 32], + ['initialWindowSize', -1], + ['initialWindowSize', 2 ** 32], + ['maxFrameSize', 16383], + ['maxFrameSize', 2 ** 24], + ['maxHeaderListSize', -1], + ['maxHeaderListSize', 2 ** 32] + ].forEach((i) => { + const settings = {}; + settings[i[0]] = i[1]; + assert.throws(() => client.settings(settings), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: RangeError + })); + }); + [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => { + assert.throws(() => client.settings({enablePush: i}), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + type: TypeError + })); + }); + + })); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-session-stream-state.js b/test/parallel/test-http2-session-stream-state.js new file mode 100644 index 00000000000000..9ba56f958c43da --- /dev/null +++ b/test/parallel/test-http2-session-stream-state.js @@ -0,0 +1,97 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + + // Test Stream State. + { + const state = stream.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.state, 'number'); + assert.strictEqual(typeof state.weight, 'number'); + assert.strictEqual(typeof state.sumDependencyWeight, 'number'); + assert.strictEqual(typeof state.localClose, 'number'); + assert.strictEqual(typeof state.remoteClose, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + } + + // Test Session State. + { + const state = stream.session.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.effectiveLocalWindowSize, 'number'); + assert.strictEqual(typeof state.effectiveRecvDataLength, 'number'); + assert.strictEqual(typeof state.nextStreamID, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + assert.strictEqual(typeof state.lastProcStreamID, 'number'); + assert.strictEqual(typeof state.remoteWindowSize, 'number'); + assert.strictEqual(typeof state.outboundQueueSize, 'number'); + assert.strictEqual(typeof state.deflateDynamicTableSize, 'number'); + assert.strictEqual(typeof state.inflateDynamicTableSize, 'number'); + } + + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.end('hello world'); +} + +server.listen(0); + +server.on('listening', common.mustCall(() => { + + const client = h2.connect(`http://localhost:${server.address().port}`); + + const headers = { ':path': '/' }; + + const req = client.request(headers); + + // State will only be valid after connect event is emitted + req.on('connect', common.mustCall(() => { + + // Test Stream State. + { + const state = req.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.state, 'number'); + assert.strictEqual(typeof state.weight, 'number'); + assert.strictEqual(typeof state.sumDependencyWeight, 'number'); + assert.strictEqual(typeof state.localClose, 'number'); + assert.strictEqual(typeof state.remoteClose, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + } + + // Test Session State + { + const state = req.session.state; + assert.strictEqual(typeof state, 'object'); + assert.strictEqual(typeof state.effectiveLocalWindowSize, 'number'); + assert.strictEqual(typeof state.effectiveRecvDataLength, 'number'); + assert.strictEqual(typeof state.nextStreamID, 'number'); + assert.strictEqual(typeof state.localWindowSize, 'number'); + assert.strictEqual(typeof state.lastProcStreamID, 'number'); + assert.strictEqual(typeof state.remoteWindowSize, 'number'); + assert.strictEqual(typeof state.outboundQueueSize, 'number'); + assert.strictEqual(typeof state.deflateDynamicTableSize, 'number'); + assert.strictEqual(typeof state.inflateDynamicTableSize, 'number'); + } + })); + + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-single-headers.js b/test/parallel/test-http2-single-headers.js new file mode 100644 index 00000000000000..49918acc474bcb --- /dev/null +++ b/test/parallel/test-http2-single-headers.js @@ -0,0 +1,59 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const http2 = require('http2'); + +const server = http2.createServer(); + +// Each of these headers must appear only once +const singles = [ + 'content-type', + 'user-agent', + 'referer', + 'authorization', + 'proxy-authorization', + 'if-modified-since', + 'if-unmodified-since', + 'from', + 'location', + 'max-forwards' +]; + +server.on('stream', common.mustNotCall()); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = singles.length * 2; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + + singles.forEach((i) => { + const req = client.request({ + [i]: 'abc', + [i.toUpperCase()]: 'xyz' + }); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: `Header field "${i}" must have only a single value` + })); + req.on('error', common.mustCall(maybeClose)); + + const req2 = client.request({ + [i]: ['abc', 'xyz'] + }); + req2.on('error', common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + type: Error, + message: `Header field "${i}" must have only a single value` + })); + req2.on('error', common.mustCall(maybeClose)); + }); + +})); diff --git a/test/parallel/test-http2-status-code-invalid.js b/test/parallel/test-http2-status-code-invalid.js new file mode 100644 index 00000000000000..cb8c9072f73c3b --- /dev/null +++ b/test/parallel/test-http2-status-code-invalid.js @@ -0,0 +1,40 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +function expectsError(code) { + return common.expectsError({ + code: 'ERR_HTTP2_STATUS_INVALID', + type: RangeError, + message: `Invalid status code: ${code}` + }); +} + +server.on('stream', common.mustCall((stream) => { + + // Anything lower than 100 and greater than 599 is rejected + [ 99, 700, 1000 ].forEach((i) => { + assert.throws(() => stream.respond({ ':status': i }), expectsError(i)); + }); + + stream.respond(); + stream.end(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 200); + })); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-status-code.js b/test/parallel/test-http2-status-code.js new file mode 100644 index 00000000000000..f094d981c36eec --- /dev/null +++ b/test/parallel/test-http2-status-code.js @@ -0,0 +1,40 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const codes = [ 200, 202, 300, 400, 404, 451, 500 ]; +let test = 0; + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + const status = codes[test++]; + stream.respond({ ':status': status }, { endStream: true }); +}, 7)); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = codes.length; + function maybeClose() { + if (--remaining === 0) { + client.destroy(); + server.close(); + } + } + + function doTest(expected) { + const req = client.request(); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], expected); + })); + req.resume(); + req.on('end', common.mustCall(maybeClose)); + } + + for (let n = 0; n < codes.length; n++) + doTest(codes[n]); +})); diff --git a/test/parallel/test-http2-timeouts.js b/test/parallel/test-http2-timeouts.js new file mode 100644 index 00000000000000..132496e1fcdc33 --- /dev/null +++ b/test/parallel/test-http2-timeouts.js @@ -0,0 +1,32 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall((stream) => { + stream.setTimeout(1, common.mustCall(() => { + stream.respond({':status': 200}); + stream.end('hello world'); + })); +})); +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.setTimeout(1, common.mustCall(() => { + const req = client.request({ ':path': '/' }); + req.setTimeout(1, common.mustCall(() => { + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + })); + })); +})); diff --git a/test/parallel/test-http2-too-many-settings.js b/test/parallel/test-http2-too-many-settings.js new file mode 100644 index 00000000000000..4a64645df14174 --- /dev/null +++ b/test/parallel/test-http2-too-many-settings.js @@ -0,0 +1,60 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests that attempting to send too many non-acknowledged +// settings frames will result in a throw. + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +const maxPendingAck = 2; +const server = h2.createServer({ maxPendingAck: maxPendingAck + 1 }); + +let clients = 2; + +function doTest(session) { + for (let n = 0; n < maxPendingAck; n++) + assert.doesNotThrow(() => session.settings({ enablePush: false })); + assert.throws(() => session.settings({ enablePush: false }), + common.expectsError({ + code: 'ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + type: Error + })); +} + +server.on('stream', common.mustNotCall()); + +server.once('session', common.mustCall((session) => doTest(session))); + +server.listen(0); + +const closeServer = common.mustCall(() => { + if (--clients === 0) + server.close(); +}, clients); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`, + { maxPendingAck: maxPendingAck + 1 }); + let remaining = maxPendingAck + 1; + + client.on('close', closeServer); + client.on('localSettings', common.mustCall(() => { + if (--remaining <= 0) { + client.destroy(); + } + }, maxPendingAck + 1)); + client.on('connect', common.mustCall(() => doTest(client))); +})); + +// Setting maxPendingAck to 0, defaults it to 1 +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`, + { maxPendingAck: 0 }); + + client.on('close', closeServer); + client.on('localSettings', common.mustCall(() => { + client.destroy(); + })); +})); diff --git a/test/parallel/test-http2-trailers.js b/test/parallel/test-http2-trailers.js new file mode 100644 index 00000000000000..35a5dbf28a1289 --- /dev/null +++ b/test/parallel/test-http2-trailers.js @@ -0,0 +1,44 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); +const body = + '

this is some data

'; +const trailerKey = 'test-trailer'; +const trailerValue = 'testing'; + +const server = h2.createServer(); + +// we use the lower-level API here +server.on('stream', common.mustCall(onStream)); + +function onStream(stream, headers, flags) { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + stream.on('fetchTrailers', function(trailers) { + trailers[trailerKey] = trailerValue; + }); + stream.end(body); +} + +server.listen(0); + +server.on('listening', common.mustCall(function() { + const client = h2.connect(`http://localhost:${this.address().port}`); + const req = client.request({':path': '/'}); + req.on('data', common.mustCall()); + req.on('trailers', common.mustCall((headers) => { + assert.strictEqual(headers[trailerKey], trailerValue); + req.end(); + })); + req.on('end', common.mustCall(() => { + server.close(); + client.destroy(); + })); + req.end(); + +})); diff --git a/test/parallel/test-http2-util-asserts.js b/test/parallel/test-http2-util-asserts.js new file mode 100644 index 00000000000000..fd902f01a3d29a --- /dev/null +++ b/test/parallel/test-http2-util-asserts.js @@ -0,0 +1,43 @@ +// Flags: --expose-internals --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { + assertIsObject, + assertWithinRange, +} = require('internal/http2/util'); + +[ + undefined, + {}, + Object.create(null), + new Date(), + new (class Foo {})() +].forEach((i) => { + assert.doesNotThrow(() => assertIsObject(i, 'foo', 'object')); +}); + +[ + 1, + false, + 'hello', + NaN, + Infinity, + [], + [{}] +].forEach((i) => { + assert.throws(() => assertIsObject(i, 'foo', 'object'), + common.expectsError({ + code: 'ERR_INVALID_ARG_TYPE', + message: /^The "foo" argument must be of type object$/ + })); +}); + +assert.doesNotThrow(() => assertWithinRange('foo', 1, 0, 2)); + +assert.throws(() => assertWithinRange('foo', 1, 2, 3), + common.expectsError({ + code: 'ERR_HTTP2_INVALID_SETTING_VALUE', + message: /^Invalid value for setting "foo": 1$/ + })); diff --git a/test/parallel/test-http2-util-headers-list.js b/test/parallel/test-http2-util-headers-list.js new file mode 100644 index 00000000000000..d19c78a2b3c3ff --- /dev/null +++ b/test/parallel/test-http2-util-headers-list.js @@ -0,0 +1,248 @@ +// Flags: --expose-internals --expose-http2 +'use strict'; + +// Tests the internal utility function that is used to prepare headers +// to pass to the internal binding layer. + +const common = require('../common'); +const assert = require('assert'); +const { mapToHeaders } = require('internal/http2/util'); + +const { + HTTP2_HEADER_STATUS, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_PATH, + HTTP2_HEADER_AGE, + HTTP2_HEADER_AUTHORIZATION, + HTTP2_HEADER_CONTENT_ENCODING, + HTTP2_HEADER_CONTENT_LANGUAGE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_CONTENT_LOCATION, + HTTP2_HEADER_CONTENT_MD5, + HTTP2_HEADER_CONTENT_RANGE, + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_DATE, + HTTP2_HEADER_ETAG, + HTTP2_HEADER_EXPIRES, + HTTP2_HEADER_FROM, + HTTP2_HEADER_IF_MATCH, + HTTP2_HEADER_IF_MODIFIED_SINCE, + HTTP2_HEADER_IF_NONE_MATCH, + HTTP2_HEADER_IF_RANGE, + HTTP2_HEADER_IF_UNMODIFIED_SINCE, + HTTP2_HEADER_LAST_MODIFIED, + HTTP2_HEADER_MAX_FORWARDS, + HTTP2_HEADER_PROXY_AUTHORIZATION, + HTTP2_HEADER_RANGE, + HTTP2_HEADER_REFERER, + HTTP2_HEADER_RETRY_AFTER, + HTTP2_HEADER_USER_AGENT, + + HTTP2_HEADER_ACCEPT_CHARSET, + HTTP2_HEADER_ACCEPT_ENCODING, + HTTP2_HEADER_ACCEPT_LANGUAGE, + HTTP2_HEADER_ACCEPT_RANGES, + HTTP2_HEADER_ACCEPT, + HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + HTTP2_HEADER_ALLOW, + HTTP2_HEADER_CACHE_CONTROL, + HTTP2_HEADER_CONTENT_DISPOSITION, + HTTP2_HEADER_COOKIE, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_LINK, + HTTP2_HEADER_PREFER, + HTTP2_HEADER_PROXY_AUTHENTICATE, + HTTP2_HEADER_REFRESH, + HTTP2_HEADER_SERVER, + HTTP2_HEADER_SET_COOKIE, + HTTP2_HEADER_STRICT_TRANSPORT_SECURITY, + HTTP2_HEADER_VARY, + HTTP2_HEADER_VIA, + HTTP2_HEADER_WWW_AUTHENTICATE, + + HTTP2_HEADER_CONNECTION, + HTTP2_HEADER_UPGRADE, + HTTP2_HEADER_HTTP2_SETTINGS, + HTTP2_HEADER_TE, + HTTP2_HEADER_TRANSFER_ENCODING, + HTTP2_HEADER_HOST, + HTTP2_HEADER_KEEP_ALIVE, + HTTP2_HEADER_PROXY_CONNECTION +} = process.binding('http2').constants; + +{ + const headers = { + 'abc': 1, + ':status': 200, + ':path': 'abc', + 'xyz': [1, '2', { toString() { return '3'; } }, 4], + 'foo': [], + 'BAR': [1] + }; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':path', 'abc' ], + [ ':status', '200' ], + [ 'abc', '1' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ], + [ 'bar', '1' ] + ]); +} + +{ + const headers = { + 'abc': 1, + ':path': 'abc', + ':status': [200], + ':authority': [], + 'xyz': [1, 2, 3, 4] + }; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':status', '200' ], + [ ':path', 'abc' ], + [ 'abc', '1' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ] + ]); +} + +{ + const headers = { + 'abc': 1, + ':path': 'abc', + 'xyz': [1, 2, 3, 4], + '': 1, + ':status': 200, + [Symbol('test')]: 1 // Symbol keys are ignored + }; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':status', '200' ], + [ ':path', 'abc' ], + [ 'abc', '1' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ] + ]); +} + +{ + // Only own properties are used + const base = { 'abc': 1 }; + const headers = Object.create(base); + headers[':path'] = 'abc'; + headers.xyz = [1, 2, 3, 4]; + headers.foo = []; + headers[':status'] = 200; + + assert.deepStrictEqual(mapToHeaders(headers), [ + [ ':status', '200' ], + [ ':path', 'abc' ], + [ 'xyz', '1' ], + [ 'xyz', '2' ], + [ 'xyz', '3' ], + [ 'xyz', '4' ] + ]); +} + +// The following are not allowed to have multiple values +[ + HTTP2_HEADER_STATUS, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_PATH, + HTTP2_HEADER_AGE, + HTTP2_HEADER_AUTHORIZATION, + HTTP2_HEADER_CONTENT_ENCODING, + HTTP2_HEADER_CONTENT_LANGUAGE, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_CONTENT_LOCATION, + HTTP2_HEADER_CONTENT_MD5, + HTTP2_HEADER_CONTENT_RANGE, + HTTP2_HEADER_CONTENT_TYPE, + HTTP2_HEADER_DATE, + HTTP2_HEADER_ETAG, + HTTP2_HEADER_EXPIRES, + HTTP2_HEADER_FROM, + HTTP2_HEADER_IF_MATCH, + HTTP2_HEADER_IF_MODIFIED_SINCE, + HTTP2_HEADER_IF_NONE_MATCH, + HTTP2_HEADER_IF_RANGE, + HTTP2_HEADER_IF_UNMODIFIED_SINCE, + HTTP2_HEADER_LAST_MODIFIED, + HTTP2_HEADER_MAX_FORWARDS, + HTTP2_HEADER_PROXY_AUTHORIZATION, + HTTP2_HEADER_RANGE, + HTTP2_HEADER_REFERER, + HTTP2_HEADER_RETRY_AFTER, + HTTP2_HEADER_USER_AGENT +].forEach((name) => { + const msg = `Header field "${name}" must have only a single value`; + common.expectsError({ + code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', + message: msg + })(mapToHeaders({[name]: [1, 2, 3]})); +}); + +[ + HTTP2_HEADER_ACCEPT_CHARSET, + HTTP2_HEADER_ACCEPT_ENCODING, + HTTP2_HEADER_ACCEPT_LANGUAGE, + HTTP2_HEADER_ACCEPT_RANGES, + HTTP2_HEADER_ACCEPT, + HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + HTTP2_HEADER_ALLOW, + HTTP2_HEADER_CACHE_CONTROL, + HTTP2_HEADER_CONTENT_DISPOSITION, + HTTP2_HEADER_COOKIE, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_LINK, + HTTP2_HEADER_PREFER, + HTTP2_HEADER_PROXY_AUTHENTICATE, + HTTP2_HEADER_REFRESH, + HTTP2_HEADER_SERVER, + HTTP2_HEADER_SET_COOKIE, + HTTP2_HEADER_STRICT_TRANSPORT_SECURITY, + HTTP2_HEADER_VARY, + HTTP2_HEADER_VIA, + HTTP2_HEADER_WWW_AUTHENTICATE +].forEach((name) => { + assert(!(mapToHeaders({[name]: [1, 2, 3]}) instanceof Error), name); +}); + +const regex = + /^HTTP\/1 Connection specific headers are forbidden$/; +[ + HTTP2_HEADER_CONNECTION, + HTTP2_HEADER_UPGRADE, + HTTP2_HEADER_HTTP2_SETTINGS, + HTTP2_HEADER_TE, + HTTP2_HEADER_TRANSFER_ENCODING, + HTTP2_HEADER_HOST, + HTTP2_HEADER_PROXY_CONNECTION, + HTTP2_HEADER_KEEP_ALIVE, + 'Connection', + 'Upgrade', + 'HTTP2-Settings', + 'TE', + 'Transfer-Encoding', + 'Proxy-Connection', + 'Keep-Alive' +].forEach((name) => { + common.expectsError({ + code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS', + message: regex + })(mapToHeaders({[name]: 'abc'})); +}); + +assert(!(mapToHeaders({ te: 'trailers' }) instanceof Error)); diff --git a/test/parallel/test-http2-window-size.js b/test/parallel/test-http2-window-size.js new file mode 100644 index 00000000000000..d914e99f6aa195 --- /dev/null +++ b/test/parallel/test-http2-window-size.js @@ -0,0 +1,102 @@ +// Flags: --expose-http2 +'use strict'; + +// This test ensures that servers are able to send data independent of window +// size. +// TODO: This test makes large buffer allocations (128KiB) and should be tested +// on smaller / IoT platforms in case this poses problems for these targets. + +const assert = require('assert'); +const common = require('../common'); +const h2 = require('http2'); + +// Given a list of buffers and an initial window size, have a server write +// each buffer to the HTTP2 Writable stream, and let the client verify that +// all of the bytes were sent correctly +function run(buffers, initialWindowSize) { + return new Promise((resolve, reject) => { + const expectedBuffer = Buffer.concat(buffers); + + const server = h2.createServer(); + server.on('stream', (stream) => { + let i = 0; + const writeToStream = () => { + const cont = () => { + i++; + if (i < buffers.length) { + setImmediate(writeToStream); + } else { + stream.end(); + } + }; + const drained = stream.write(buffers[i]); + if (drained) { + cont(); + } else { + stream.once('drain', cont); + } + }; + writeToStream(); + }); + server.listen(0); + + server.on('listening', common.mustCall(function() { + const port = this.address().port; + + const client = + h2.connect({ + authority: 'localhost', + protocol: 'http:', + port + }, { + settings: { + initialWindowSize + } + }).on('connect', common.mustCall(() => { + const req = client.request({ + ':method': 'GET', + ':path': '/' + }); + const responses = []; + req.on('data', (data) => { + responses.push(data); + }); + req.on('end', common.mustCall(() => { + const actualBuffer = Buffer.concat(responses); + assert.strictEqual(Buffer.compare(actualBuffer, expectedBuffer), 0); + // shut down + client.destroy(); + server.close(() => { + resolve(); + }); + })); + req.end(); + })); + })); + }); +} + +const bufferValueRange = [0, 1, 2, 3]; +const buffersList = [ + bufferValueRange.map((a) => Buffer.alloc(1 << 4, a)), + bufferValueRange.map((a) => Buffer.alloc((1 << 8) - 1, a)), +// Specifying too large of a value causes timeouts on some platforms +// bufferValueRange.map((a) => Buffer.alloc(1 << 17, a)) +]; +const initialWindowSizeList = [ + 1 << 4, + (1 << 8) - 1, + 1 << 8, + 1 << 17, + undefined // use default window size which is (1 << 16) - 1 +]; + +// Call `run` on each element in the cartesian product of buffersList and +// initialWindowSizeList. +let p = Promise.resolve(); +for (const buffers of buffersList) { + for (const initialWindowSize of initialWindowSizeList) { + p = p.then(() => run(buffers, initialWindowSize)); + } +} +p.then(common.mustCall(() => {})); diff --git a/test/parallel/test-http2-withflag.js b/test/parallel/test-http2-withflag.js new file mode 100644 index 00000000000000..557ec40e643eba --- /dev/null +++ b/test/parallel/test-http2-withflag.js @@ -0,0 +1,7 @@ +// Flags: --expose-http2 +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.doesNotThrow(() => require('http2')); diff --git a/test/parallel/test-http2-write-callbacks.js b/test/parallel/test-http2-write-callbacks.js new file mode 100644 index 00000000000000..b371ebf681e472 --- /dev/null +++ b/test/parallel/test-http2-write-callbacks.js @@ -0,0 +1,36 @@ +// Flags: --expose-http2 +'use strict'; + +// Verifies that write callbacks are called + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + stream.write('abc', common.mustCall(() => { + stream.end('xyz'); + })); + let actual = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => actual += chunk); + stream.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':method': 'POST' }); + req.write('abc', common.mustCall(() => { + req.end('xyz'); + })); + let actual = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz'))); + req.on('streamClosed', common.mustCall(() => { + client.destroy(); + server.close(); + })); +})); diff --git a/test/parallel/test-http2-write-empty-string.js b/test/parallel/test-http2-write-empty-string.js new file mode 100644 index 00000000000000..74675fd67d9ef2 --- /dev/null +++ b/test/parallel/test-http2-write-empty-string.js @@ -0,0 +1,40 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const server = http2.createServer(function(request, response) { + response.writeHead(200, {'Content-Type': 'text/plain'}); + response.write('1\n'); + response.write(''); + response.write('2\n'); + response.write(''); + response.end('3\n'); + + this.close(); +}); + +server.listen(0, common.mustCall(function() { + const client = http2.connect(`http://localhost:${this.address().port}`); + const headers = { ':path': '/' }; + const req = client.request(headers).setEncoding('ascii'); + + let res = ''; + + req.on('response', common.mustCall(function(headers) { + assert.strictEqual(200, headers[':status']); + })); + + req.on('data', (chunk) => { + res += chunk; + }); + + req.on('end', common.mustCall(function() { + assert.strictEqual('1\n2\n3\n', res); + client.destroy(); + })); + + req.end(); +})); diff --git a/test/parallel/test-http2-zero-length-write.js b/test/parallel/test-http2-zero-length-write.js new file mode 100644 index 00000000000000..5f4f0681d4baf4 --- /dev/null +++ b/test/parallel/test-http2-zero-length-write.js @@ -0,0 +1,50 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http2 = require('http2'); + +const { Readable } = require('stream'); + +function getSrc() { + const chunks = [ '', 'asdf', '', 'foo', '', 'bar', '' ]; + return new Readable({ + read() { + const chunk = chunks.shift(); + if (chunk !== undefined) + this.push(chunk); + else + this.push(null); + } + }); +} + +const expect = 'asdffoobar'; + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + let actual = ''; + stream.respond(); + stream.resume(); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => actual += chunk); + stream.on('end', common.mustCall(() => { + getSrc().pipe(stream); + assert.strictEqual(actual, expect); + })); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + let actual = ''; + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall()); + req.on('data', (chunk) => actual += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(actual, expect); + server.close(); + client.destroy(); + })); + getSrc().pipe(req); +})); diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js index 2a9a676e8d6c2a..e4da5380217e9a 100644 --- a/test/parallel/test-process-versions.js +++ b/test/parallel/test-process-versions.js @@ -3,7 +3,7 @@ const common = require('../common'); const assert = require('assert'); const expected_keys = ['ares', 'http_parser', 'modules', 'node', - 'uv', 'v8', 'zlib']; + 'uv', 'v8', 'zlib', 'nghttp2']; if (common.hasCrypto) { expected_keys.push('openssl'); diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js old mode 100755 new mode 100644 From a49146e446d0aebc69957734df65fa8a8e9411db Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 10:32:25 -0700 Subject: [PATCH 59/97] http2: remove redundant return in test Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- test/parallel/test-tls-disable-renegotiation.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js index 9e9a672768649c..0efa935e432950 100644 --- a/test/parallel/test-tls-disable-renegotiation.js +++ b/test/parallel/test-tls-disable-renegotiation.js @@ -5,10 +5,9 @@ const fs = require('fs'); // Tests that calling disableRenegotiation on a TLSSocket stops renegotiation. -if (!common.hasCrypto) { +if (!common.hasCrypto) common.skip('missing crypto'); - return; -} + const tls = require('tls'); const options = { From e8cc193bcc2bc7796a7d3a8efede41fdad11c31f Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 10:43:33 -0700 Subject: [PATCH 60/97] http2: fix documentation nits Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- doc/api/errors.md | 2 +- doc/api/http2.md | 12 ++++++------ doc/guides/writing-and-running-benchmarks.md | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 7e4d594bb13e34..93d6a25173cc17 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -773,7 +773,7 @@ been closed. ### ERR_HTTP2_STREAM_ERROR -Used when a non-zero error code has been specified in an RST_STREAM frame. +Used when a non-zero error code has been specified in an `RST_STREAM` frame. ### ERR_HTTP2_STREAM_SELF_DEPENDENCY diff --git a/doc/api/http2.md b/doc/api/http2.md index ca696de81e3aac..9beb2cac922986 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -559,7 +559,7 @@ when: * A new HTTP/2 `HEADERS` frame with a previously unused stream ID is received; * The `http2stream.pushStream()` method is called. -On the client side, instances of [`ClientHttp2Stream`[] are created when the +On the client side, instances of [`ClientHttp2Stream`][] are created when the `http2session.request()` method is called. *Note*: On the client, the `Http2Stream` instance returned by @@ -1451,7 +1451,7 @@ added: REPLACEME * `settings` {[Settings Object][]} * Returns: {Buffer} -Returns a [Buffer][] instance containing serialized representation of the given +Returns a `Buffer` instance containing serialized representation of the given HTTP/2 settings as specified in the [HTTP/2][] specification. This is intended for use with the `HTTP2-Settings` header field. @@ -1691,13 +1691,13 @@ TBD [`net.Socket`]: net.html [`tls.TLSSocket`]: tls.html [`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener -[ClientHttp2Stream]: #http2_class_clienthttp2stream -[Compatibility API: #http2_compatibility_api +[`ClientHttp2Stream`]: #http2_class_clienthttp2stream +[Compatibility API]: #http2_compatibility_api [`Duplex`]: stream.html#stream_class_stream_duplex [Headers Object]: #http2_headers_object -[Http2Stream]: #http2_class_http2stream +[`Http2Stream`]: #http2_class_http2stream [Http2Session and Sockets]: #http2_http2sesion_and_sockets -[ServerHttp2Stream]: #http2_class_serverhttp2stream +[`ServerHttp2Stream`]: #http2_class_serverhttp2stream [Settings Object]: #http2_settings_object [Using options.selectPadding]: #http2_using_options_selectpadding [error code]: #error_codes diff --git a/doc/guides/writing-and-running-benchmarks.md b/doc/guides/writing-and-running-benchmarks.md index 7aeb9728aaedf0..d418ed6974bf80 100644 --- a/doc/guides/writing-and-running-benchmarks.md +++ b/doc/guides/writing-and-running-benchmarks.md @@ -45,7 +45,7 @@ benchmarker to be used should be specified by providing it as an argument: To run the `http2` benchmarks, the `h2load` benchmarker must be used. The `h2load` tool is a component of the `nghttp2` project and may be installed -from [nghttp.org][] or built from source. +from [nghttp2.org][] or built from source. `node benchmark/http2/simple.js benchmarker=autocannon` From be716d00cc7cb06046c4e3807b92f0f1b519a51c Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 15:47:24 -0700 Subject: [PATCH 61/97] doc: include http2.md in all.md Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- doc/api/all.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/all.md b/doc/api/all.md index 24eda32f44d3b5..425513e2568d03 100644 --- a/doc/api/all.md +++ b/doc/api/all.md @@ -19,6 +19,7 @@ @include fs @include globals @include http +@include http2 @include https @include inspector @include intl From 9d752d52820c43163daac080452dc2f83196191a Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 18 Jul 2017 08:45:41 -0700 Subject: [PATCH 62/97] test: fix flakiness in test-http2-client-upload Race condition in the closing of the stream causing failure on some platforms. Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- test/parallel/test-http2-client-upload.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) mode change 100644 => 100755 test/parallel/test-http2-client-upload.js diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js old mode 100644 new mode 100755 index 4ce7da878e1fd2..f65ae09c3a205b --- a/test/parallel/test-http2-client-upload.js +++ b/test/parallel/test-http2-client-upload.js @@ -32,13 +32,21 @@ fs.readFile(loc, common.mustCall((err, data) => { server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + const req = client.request({ ':method': 'POST' }); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { - server.close(); - client.destroy(); - })); - fs.createReadStream(loc).pipe(req); + req.on('end', common.mustCall(maybeClose)); + const str = fs.createReadStream(loc); + str.on('end', common.mustCall(maybeClose)); + str.pipe(req); })); })); From 97f622b99e55cbca2c4e78fc6c6ae3cfb1ec46a1 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 18 Jul 2017 09:31:38 -0700 Subject: [PATCH 63/97] test: fix flaky test-http2-client-unescaped-path on osx Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- test/parallel/test-http2-client-unescaped-path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 test/parallel/test-http2-client-unescaped-path.js diff --git a/test/parallel/test-http2-client-unescaped-path.js b/test/parallel/test-http2-client-unescaped-path.js old mode 100644 new mode 100755 index d92d40492e204c..95b5c04b0fed6a --- a/test/parallel/test-http2-client-unescaped-path.js +++ b/test/parallel/test-http2-client-unescaped-path.js @@ -13,7 +13,7 @@ const count = 32; server.listen(0, common.mustCall(() => { const client = http2.connect(`http://localhost:${server.address().port}`); - let remaining = count; + let remaining = count + 1; function maybeClose() { if (--remaining === 0) { server.close(); From cd0f4c6652ed0bd4740cb85611b71821f79a9ee6 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 18 Jul 2017 17:24:41 -0700 Subject: [PATCH 64/97] http2: fix abort when client.destroy inside end event Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- lib/internal/http2/core.js | 68 ++++++++++--------- src/node_http2.cc | 0 src/node_http2.h | 6 +- src/node_http2_core-inl.h | 7 +- src/node_http2_core.h | 8 +-- ...test-http2-options-max-reserved-streams.js | 17 +++-- .../parallel/test-http2-response-splitting.js | 3 +- .../test-http2-server-socket-destroy.js | 7 +- 8 files changed, 58 insertions(+), 58 deletions(-) mode change 100644 => 100755 lib/internal/http2/core.js mode change 100644 => 100755 src/node_http2.cc mode change 100644 => 100755 src/node_http2.h mode change 100644 => 100755 src/node_http2_core-inl.h mode change 100644 => 100755 src/node_http2_core.h mode change 100644 => 100755 test/parallel/test-http2-options-max-reserved-streams.js mode change 100644 => 100755 test/parallel/test-http2-response-splitting.js mode change 100644 => 100755 test/parallel/test-http2-server-socket-destroy.js diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js old mode 100644 new mode 100755 index 1bdd57926c4e62..5c99dbc897ba6c --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -289,11 +289,9 @@ function onSessionRead(nread, buf, handle) { _unrefActive(this); // Reset the session timeout timer _unrefActive(stream); // Reset the stream timeout timer - if (nread >= 0) { + if (nread >= 0 && !stream.destroyed) { if (!stream.push(buf)) { - assert(this.streamReadStop(id) === undefined, - `HTTP/2 Stream ${id} does not exist. Please report this as ' + - 'a bug in Node.js`); + this.streamReadStop(id); state.reading = false; } } else { @@ -1475,44 +1473,48 @@ class Http2Stream extends Duplex { this.once('ready', this._destroy.bind(this, err, callback)); return; } - debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`); - - // Submit RST-STREAM frame if one hasn't been sent already and the - // stream hasn't closed normally... - if (!this[kState].rst) { - const code = - err instanceof Error ? - NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR; - this[kSession].rstStream(this, code); - } - + process.nextTick(() => { + debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`); + + // Submit RST-STREAM frame if one hasn't been sent already and the + // stream hasn't closed normally... + if (!this[kState].rst && !session.destroyed) { + const code = + err instanceof Error ? + NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR; + this[kSession].rstStream(this, code); + } - // Remove the close handler on the session - session.removeListener('close', this[kState].closeHandler); + // Remove the close handler on the session + session.removeListener('close', this[kState].closeHandler); - // Unenroll the timer - unenroll(this); + // Unenroll the timer + unenroll(this); - setImmediate(finishStreamDestroy.bind(this, handle)); - session[kState].streams.delete(this[kID]); - delete this[kSession]; + setImmediate(finishStreamDestroy.bind(this, handle)); - // All done - const rst = this[kState].rst; - const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR; - if (code !== NGHTTP2_NO_ERROR) { - const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code); - process.nextTick(() => this.emit('error', err)); - } - process.nextTick(emit.bind(this, 'streamClosed', code)); - debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`); - callback(err); + // All done + const rst = this[kState].rst; + const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR; + if (code !== NGHTTP2_NO_ERROR) { + const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code); + process.nextTick(() => this.emit('error', err)); + } + process.nextTick(emit.bind(this, 'streamClosed', code)); + debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`); + callback(err); + }); } } function finishStreamDestroy(handle) { + const id = this[kID]; + const session = this[kSession]; + session[kState].streams.delete(id); + delete this[kSession]; if (handle !== undefined) - handle.destroyStream(this[kID]); + handle.destroyStream(id); + this.emit('destroy'); } function processHeaders(headers) { diff --git a/src/node_http2.cc b/src/node_http2.cc old mode 100644 new mode 100755 diff --git a/src/node_http2.h b/src/node_http2.h old mode 100644 new mode 100755 index f6ccad29846d4a..c2dcd82e35948c --- a/src/node_http2.h +++ b/src/node_http2.h @@ -329,7 +329,6 @@ class Http2Session : public AsyncWrap, padding_strategy_ = opts.GetPaddingStrategy(); Init(env->event_loop(), type, *opts); - stream_buf_.AllocateSufficientStorage(kAllocBufferSize); } ~Http2Session() override { @@ -456,7 +455,7 @@ class Http2Session : public AsyncWrap, } char* stream_alloc() { - return *stream_buf_; + return stream_buf_; } private: @@ -464,7 +463,8 @@ class Http2Session : public AsyncWrap, StreamResource::Callback prev_alloc_cb_; StreamResource::Callback prev_read_cb_; padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; - MaybeStackBuffer stream_buf_; + + char stream_buf_[kAllocBufferSize]; }; class ExternalHeader : diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h old mode 100644 new mode 100755 index 49ec63b59bd581..0659cb65a36940 --- a/src/node_http2_core-inl.h +++ b/src/node_http2_core-inl.h @@ -221,10 +221,7 @@ inline int Nghttp2Session::Free() { Nghttp2Session* session = ContainerOf(&Nghttp2Session::prep_, reinterpret_cast(handle)); - session->OnFreeSession(); - DEBUG_HTTP2("Nghttp2Session %d: session is free\n", - session->session_type_); }; uv_close(reinterpret_cast(&prep_), PrepClose); @@ -302,9 +299,9 @@ inline void Nghttp2Stream::ResetState( inline void Nghttp2Stream::Destroy() { DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_); // Do nothing if this stream instance is already destroyed - if (IsDestroyed() || IsDestroying()) + if (IsDestroyed()) return; - flags_ |= NGHTTP2_STREAM_DESTROYING; + flags_ |= NGHTTP2_STREAM_DESTROYED; Nghttp2Session* session = this->session_; if (session != nullptr) { diff --git a/src/node_http2_core.h b/src/node_http2_core.h old mode 100644 new mode 100755 index 10acd7736b419f..3efeda69b58135 --- a/src/node_http2_core.h +++ b/src/node_http2_core.h @@ -65,9 +65,7 @@ enum nghttp2_stream_flags { // Stream is closed NGHTTP2_STREAM_CLOSED = 0x8, // Stream is destroyed - NGHTTP2_STREAM_DESTROYED = 0x10, - // Stream is being destroyed - NGHTTP2_STREAM_DESTROYING = 0x20 + NGHTTP2_STREAM_DESTROYED = 0x10 }; @@ -290,10 +288,6 @@ class Nghttp2Stream { return (flags_ & NGHTTP2_STREAM_DESTROYED) == NGHTTP2_STREAM_DESTROYED; } - inline bool IsDestroying() const { - return (flags_ & NGHTTP2_STREAM_DESTROYING) == NGHTTP2_STREAM_DESTROYING; - } - // Queue outbound chunks of data to be sent on this stream inline int Write( nghttp2_stream_write_t* req, diff --git a/test/parallel/test-http2-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js old mode 100644 new mode 100755 index 1173b58e287de2..b01ed89de0a384 --- a/test/parallel/test-http2-options-max-reserved-streams.js +++ b/test/parallel/test-http2-options-max-reserved-streams.js @@ -51,6 +51,14 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`, options); + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + server.close(); + client.destroy(); + } + } + const req = client.request({ ':path': '/' }); // Because maxReservedRemoteStream is 1, the stream event @@ -59,15 +67,12 @@ server.on('listening', common.mustCall(() => { client.on('stream', common.mustCall((stream) => { stream.resume(); stream.on('end', common.mustCall()); + stream.on('streamClosed', common.mustCall(maybeClose)); })); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(() => { - server.close(); - client.destroy(); - })); - req.end(); - + req.on('end', common.mustCall()); + req.on('streamClosed', common.mustCall(maybeClose)); })); diff --git a/test/parallel/test-http2-response-splitting.js b/test/parallel/test-http2-response-splitting.js old mode 100644 new mode 100755 index 088c675389f5ba..cd3a5d39d7af01 --- a/test/parallel/test-http2-response-splitting.js +++ b/test/parallel/test-http2-response-splitting.js @@ -65,7 +65,8 @@ server.listen(0, common.mustCall(() => { assert.strictEqual(headers.location, undefined); })); req.resume(); - req.on('end', common.mustCall(maybeClose)); + req.on('end', common.mustCall()); + req.on('streamClosed', common.mustCall(maybeClose)); } doTest(str, 'location', str); diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js old mode 100644 new mode 100755 index c10bbd0ccbe0c5..15b19ca1786f53 --- a/test/parallel/test-http2-server-socket-destroy.js +++ b/test/parallel/test-http2-server-socket-destroy.js @@ -36,7 +36,9 @@ function onStream(stream) { assert.notStrictEqual(stream.session, undefined); socket.destroy(); - assert.strictEqual(stream.session, undefined); + stream.on('destroy', common.mustCall(() => { + assert.strictEqual(stream.session, undefined); + })); } server.listen(0); @@ -49,9 +51,8 @@ server.on('listening', common.mustCall(() => { [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); req.on('aborted', common.mustCall()); + req.resume(); req.on('end', common.mustCall()); - req.on('response', common.mustCall()); - req.on('data', common.mustCall()); client.on('close', common.mustCall()); })); From 2620769e7fd9961c170031fe1296cb780bff5c2a Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 19 Jul 2017 07:26:15 -0700 Subject: [PATCH 65/97] http2: refinement and test for socketError Fixes: https://github.com/nodejs/http2/issues/184 Refines the `'socketError'` event a bit and adds a test for the emission of the `'socketError'` event on the server. Client side is tested separately Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- doc/api/http2.md | 16 +++--- lib/internal/http2/core.js | 31 ++++------- .../parallel/test-http2-server-socketerror.js | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 29 deletions(-) create mode 100755 test/parallel/test-http2-server-socketerror.js diff --git a/doc/api/http2.md b/doc/api/http2.md index 9beb2cac922986..6bc1543c079f8a 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -254,9 +254,9 @@ The `'socketError'` event is emitted when an `'error'` is emitted on the `Socket` instance bound to the `Http2Session`. If this event is not handled, the `'error'` event will be re-emitted on the `Socket`. -Likewise, when an `'error'` event is emitted on the `Http2Session`, a -`'sessionError'` event will be emitted on the `Socket`. If that event is -not handled, the `'error'` event will be re-emitted on the `Http2Session`. +For `ServerHttp2Session` instances, a `'socketError'` event listener is always +registered that will, by default, forward the event on to the owning +`Http2Server` instance if no additional handlers are registered. #### Event: 'timeout' -The `'socketError'` event is emitted when an `'error'` event is emitted by -a `Socket` associated with the server. If no listener is registered for this -event, an `'error'` event is emitted. +The `'socketError'` event is emitted when a `'socketError'` event is emitted by +an `Http2Session` associated with the server. #### Event: 'stream' -The `'socketError'` event is emitted when an `'error'` event is emitted by -a `Socket` associated with the server. If no listener is registered for this -event, an `'error'` event is emitted on the `Socket` instance instead. +The `'socketError'` event is emitted when a `'socketError'` event is emitted by +an `Http2Session` associated with the server. #### Event: 'unknownProtocol' * `fd` {number} A readable file descriptor * `headers` {[Headers Object][]} +* `options` {Object} + * `statCheck` {Function} + * `offset` {number} The offset position at which to begin reading + * `length` {number} The amount of data from the fd to send Initiates a response whose data is read from the given file descriptor. No validation is performed on the given file descriptor. If an error occurs while @@ -1034,6 +1038,16 @@ server.on('stream', (stream) => { server.on('close', () => fs.closeSync(fd)); ``` +The optional `options.statCheck` function may be specified to give user code +an opportunity to set additional content headers based on the `fs.Stat` details +of the given fd. If the `statCheck` function is provided, the +`http2stream.respondWithFD()` method will perform an `fs.fstat()` call to +collect details on the provided file descriptor. + +The `offset` and `length` options may be used to limit the response to a +specific range subset. This can be used, for instance, to support HTTP Range +requests. + #### http2stream.respondWithFile(path[, headers[, options]]) + +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API](compatiblity-api). + #### Event: 'timeout' + +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} + +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API](compatiblity-api). + #### Event: 'timeout' + +A `Http2ServerRequest` object is created by [`http2.Server`][] or +[`http2.SecureServer`][] and passed as the first argument to the [`'request'`][] event. It may be used to access a request status, +headers and data. + +It implements the [Readable Stream][] interface, as well as the +following additional events, methods, and properties. + +#### Event: 'aborted' + + +The `'aborted'` event is emitted whenever a `Http2ServerRequest` instance is +abnormally aborted in mid-communication. + +*Note*: The `'aborted'` event will only be emitted if the +`Http2ServerRequest` writable side has not been ended. + +#### Event: 'close' + + +Indicates that the underlying [`Http2Stream`]() was closed. +Just like `'end'`, this event occurs only once per response. + +#### request.destroy([error]) + + +* `error` {Error} + +Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If `error` +is provided, an `'error'` event is emitted and `error` is passed as an argument +to any listeners on the event. + +It does nothing if the stream was already destroyed. + +#### request.headers + + +* {Object} + +The request/response headers object. + +Key-value pairs of header names and values. Header names are lower-cased. +Example: + +```js +// Prints something like: +// +// { 'user-agent': 'curl/7.22.0', +// host: '127.0.0.1:8000', +// accept: '*/*' } +console.log(request.headers); +``` + +See [Headers Object][]. + +### request.httpVersion + + +* {string} + +In case of server request, the HTTP version sent by the client. In the case of +client response, the HTTP version of the connected-to server. Returns +`'2.0'`. + +Also `message.httpVersionMajor` is the first integer and +`message.httpVersionMinor` is the second. + +#### request.method + + +* {string} + +The request method as a string. Read only. Example: +`'GET'`, `'DELETE'`. + +#### request.rawHeaders + + +* {Array} + +The raw request/response headers list exactly as they were received. + +Note that the keys and values are in the same list. It is *not* a +list of tuples. So, the even-numbered offsets are key values, and the +odd-numbered offsets are the associated values. + +Header names are not lowercased, and duplicates are not merged. + +```js +// Prints something like: +// +// [ 'user-agent', +// 'this is invalid because there can be only one', +// 'User-Agent', +// 'curl/7.22.0', +// 'Host', +// '127.0.0.1:8000', +// 'ACCEPT', +// '*/*' ] +console.log(request.rawHeaders); +``` + +#### request.rawTrailers + + +* {Array} + +The raw request/response trailer keys and values exactly as they were +received. Only populated at the `'end'` event. + +#### request.setTimeout(msecs, callback) + + +* `msecs` {number} +* `callback` {Function} + +Calls `request.connection.setTimeout(msecs, callback)`. + +Returns `request`. + +#### request.socket + + +* {net.Socket} + +The [`net.Socket`][] object associated with the connection. + +With TLS support, use [`request.socket.getPeerCertificate()`][] to obtain the +client's authentication details. + +*Note*: do not use this socket object to send or receive any data. All +data transfers are managed by HTTP/2 and data might be lost. + +#### request.stream + + +* {http2.Http2Stream} + +The [`Http2Stream`][] object backing the request. + +#### request.trailers + + +* {Object} + +The request/response trailers object. Only populated at the `'end'` event. + +#### request.url + + +* {string} + +Request URL string. This contains only the URL that is +present in the actual HTTP request. If the request is: + +```txt +GET /status?name=ryan HTTP/1.1\r\n +Accept: text/plain\r\n +\r\n +``` + +Then `request.url` will be: + + +```js +'/status?name=ryan' +``` + +To parse the url into its parts `require('url').parse(request.url)` +can be used. Example: + +```txt +$ node +> require('url').parse('/status?name=ryan') +Url { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: '?name=ryan', + query: 'name=ryan', + pathname: '/status', + path: '/status?name=ryan', + href: '/status?name=ryan' } +``` + +To extract the parameters from the query string, the +`require('querystring').parse` function can be used, or +`true` can be passed as the second argument to `require('url').parse`. +Example: + +```txt +$ node +> require('url').parse('/status?name=ryan', true) +Url { + protocol: null, + slashes: null, + auth: null, + host: null, + port: null, + hostname: null, + hash: null, + search: '?name=ryan', + query: { name: 'ryan' }, + pathname: '/status', + path: '/status?name=ryan', + href: '/status?name=ryan' } +``` + +### Class: http2.Http2ServerResponse + + +This object is created internally by an HTTP server--not by the user. It is +passed as the second parameter to the [`'request'`][] event. + +The response implements, but does not inherit from, the [Writable Stream][] +interface. This is an [`EventEmitter`][] with the following events: + +### Event: 'close' + + +Indicates that the underlying [`Http2Stream`]() was terminated before +[`response.end()`][] was called or able to flush. + +### Event: 'finish' + + +Emitted when the response has been sent. More specifically, this event is +emitted when the last segment of the response headers and body have been +handed off to the HTTP/2 multiplexing for transmission over the network. It +does not imply that the client has received anything yet. + +After this event, no more events will be emitted on the response object. + +### response.addTrailers(headers) + + +* `headers` {Object} + +This method adds HTTP trailing headers (a header but at the end of the +message) to the response. + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +### response.connection + + +* {net.Socket} + +See [`response.socket`][]. + +### response.end([data][, encoding][, callback]) + + +* `data` {string|Buffer} +* `encoding` {string} +* `callback` {Function} + +This method signals to the server that all of the response headers and body +have been sent; that server should consider this message complete. +The method, `response.end()`, MUST be called on each response. + +If `data` is specified, it is equivalent to calling +[`response.write(data, encoding)`][] followed by `response.end(callback)`. + +If `callback` is specified, it will be called when the response stream +is finished. + +### response.finished + + +* {boolean} + +Boolean value that indicates whether the response has completed. Starts +as `false`. After [`response.end()`][] executes, the value will be `true`. + +### response.getHeader(name) + + +* `name` {string} +* Returns: {string} + +Reads out a header that's already been queued but not sent to the client. +Note that the name is case insensitive. + +Example: + +```js +const contentType = response.getHeader('content-type'); +``` + +### response.getHeaderNames() + + +* Returns: {Array} + +Returns an array containing the unique names of the current outgoing headers. +All header names are lowercase. + +Example: + +```js +response.setHeader('Foo', 'bar'); +response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + +const headerNames = response.getHeaderNames(); +// headerNames === ['foo', 'set-cookie'] +``` + +### response.getHeaders() + + +* Returns: {Object} + +Returns a shallow copy of the current outgoing headers. Since a shallow copy +is used, array values may be mutated without additional calls to various +header-related http module methods. The keys of the returned object are the +header names and the values are the respective header values. All header names +are lowercase. + +*Note*: The object returned by the `response.getHeaders()` method _does not_ +prototypically inherit from the JavaScript `Object`. This means that typical +`Object` methods such as `obj.toString()`, `obj.hasOwnProperty()`, and others +are not defined and *will not work*. + +Example: + +```js +response.setHeader('Foo', 'bar'); +response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); + +const headers = response.getHeaders(); +// headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] } +``` + +### response.hasHeader(name) + + +* `name` {string} +* Returns: {boolean} + +Returns `true` if the header identified by `name` is currently set in the +outgoing headers. Note that the header name matching is case-insensitive. + +Example: + +```js +const hasContentType = response.hasHeader('content-type'); +``` + +### response.headersSent + + +* {boolean} + +Boolean (read-only). True if headers were sent, false otherwise. + +### response.removeHeader(name) + + +* `name` {string} + +Removes a header that's queued for implicit sending. + +Example: + +```js +response.removeHeader('Content-Encoding'); +``` + +### response.sendDate + + +* {boolean} + +When true, the Date header will be automatically generated and sent in +the response if it is not already present in the headers. Defaults to true. + +This should only be disabled for testing; HTTP requires the Date header +in responses. + +### response.setHeader(name, value) + + +* `name` {string} +* `value` {string | string[]} + +Sets a single header value for implicit headers. If this header already exists +in the to-be-sent headers, its value will be replaced. Use an array of strings +here to send multiple headers with the same name. + +Example: + +```js +response.setHeader('Content-Type', 'text/html'); +``` + +or + +```js +response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']); +``` + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +When headers have been set with [`response.setHeader()`][], they will be merged with +any headers passed to [`response.writeHead()`][], with the headers passed to +[`response.writeHead()`][] given precedence. + +```js +// returns content-type = text/plain +const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Foo', 'bar'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + +### response.setTimeout(msecs[, callback]) + + +* `msecs` {number} +* `callback` {Function} + +Sets the [`Http2Stream`]()'s timeout value to `msecs`. If a callback is +provided, then it is added as a listener on the `'timeout'` event on +the response object. + +If no `'timeout'` listener is added to the request, the response, or +the server, then [`Http2Stream`]()s are destroyed when they time out. If a handler is +assigned to the request, the response, or the server's `'timeout'` events, +timed out sockets must be handled explicitly. + +Returns `response`. + +### response.socket + + +* {net.Socket} + +Reference to the underlying socket. Usually users will not want to access +this property. In particular, the socket will not emit `'readable'` events +because of how the protocol parser attaches to the socket. After +`response.end()`, the property is nulled. The `socket` may also be accessed +via `response.connection`. + +Example: + +```js +const http = require('http'); +const server = http.createServer((req, res) => { + const ip = req.socket.remoteAddress; + const port = req.socket.remotePort; + res.end(`Your IP address is ${ip} and your source port is ${port}.`); +}).listen(3000); +``` + +### response.statusCode + + +* {number} + +When using implicit headers (not calling [`response.writeHead()`][] explicitly), +this property controls the status code that will be sent to the client when +the headers get flushed. + +Example: + +```js +response.statusCode = 404; +``` + +After response header was sent to the client, this property indicates the +status code which was sent out. + +### response.statusMessage + + +* {string} + +Status message is not supported by HTTP/2 (RFC7540 8.1.2.4). It returns +an empty string. + +#### response.stream + + +* {http2.Http2Stream} + +The [`Http2Stream`][] object backing the response. + +### response.write(chunk[, encoding][, callback]) + + +* `chunk` {string|Buffer} +* `encoding` {string} +* `callback` {Function} +* Returns: {boolean} + +If this method is called and [`response.writeHead()`][] has not been called, +it will switch to implicit header mode and flush the implicit headers. + +This sends a chunk of the response body. This method may +be called multiple times to provide successive parts of the body. + +Note that in the `http` module, the response body is omitted when the +request is a HEAD request. Similarly, the `204` and `304` responses +_must not_ include a message body. + +`chunk` can be a string or a buffer. If `chunk` is a string, +the second parameter specifies how to encode it into a byte stream. +By default the `encoding` is `'utf8'`. `callback` will be called when this chunk +of data is flushed. + +*Note*: This is the raw HTTP body and has nothing to do with +higher-level multi-part body encodings that may be used. + +The first time [`response.write()`][] is called, it will send the buffered +header information and the first chunk of the body to the client. The second +time [`response.write()`][] is called, Node.js assumes data will be streamed, +and sends the new data separately. That is, the response is buffered up to the +first chunk of the body. + +Returns `true` if the entire data was flushed successfully to the kernel +buffer. Returns `false` if all or part of the data was queued in user memory. +`'drain'` will be emitted when the buffer is free again. + +### response.writeContinue() + + +Does nothing. Added for parity with [HTTP/1](). + +### response.writeHead(statusCode[, statusMessage][, headers]) + + +* `statusCode` {number} +* `statusMessage` {string} +* `headers` {Object} + +Sends a response header to the request. The status code is a 3-digit HTTP +status code, like `404`. The last argument, `headers`, are the response headers. +For compatibility with [HTTP/1](), one can give a human-readable `statusMessage` as the second argument, which will be silenty ignored and emit a warning. + +Example: + +```js +const body = 'hello world'; +response.writeHead(200, { + 'Content-Length': Buffer.byteLength(body), + 'Content-Type': 'text/plain' }); +``` + +This method must only be called once on a message and it must +be called before [`response.end()`][] is called. + +If [`response.write()`][] or [`response.end()`][] are called before calling +this, the implicit/mutable headers will be calculated and call this function. + +When headers have been set with [`response.setHeader()`][], they will be merged with +any headers passed to [`response.writeHead()`][], with the headers passed to +[`response.writeHead()`][] given precedence. + +```js +// returns content-type = text/plain +const server = http2.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('X-Foo', 'bar'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); +}); +``` + +Note that Content-Length is given in bytes not characters. The above example +works because the string `'hello world'` contains only single byte characters. +If the body contains higher coded characters then `Buffer.byteLength()` +should be used to determine the number of bytes in a given encoding. +And Node.js does not check whether Content-Length and the length of the body +which has been transmitted are equal or not. + +Attempting to set a header field name or value that contains invalid characters +will result in a [`TypeError`][] being thrown. + +### response.createPushResponse(headers, callback) + + +Call [`stream.pushStream()`]() with the given headers, and wraps the +given newly created [`Http2Stream`] on `Http2ServerRespose`. +The callback will be called with an error with code `ERR_HTTP2_STREAM_CLOSED` +if the stream is closed. [HTTP/2]: https://tools.ietf.org/html/rfc7540 [HTTP/1]: http.html +[https]: https.html [`net.Socket`]: net.html [`tls.TLSSocket`]: tls.html [`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener [`ClientHttp2Stream`]: #http2_class_clienthttp2stream [Compatibility API]: #http2_compatibility_api +[alpn-negotiation]: #http2_alpn_negotiation [`Duplex`]: stream.html#stream_class_stream_duplex [Headers Object]: #http2_headers_object [`Http2Stream`]: #http2_class_http2stream @@ -1720,3 +2485,7 @@ TBD [Using options.selectPadding]: #http2_using_options_selectpadding [error code]: #error_codes [`'unknownProtocol'`]: #http2_event_unknownprotocol +[`'request'`]: #http2_event_request +[Readable Stream]: stream.html#stream_class_stream_readable +[`ServerRequest`]: #http2_class_server_request +[`stream.pushStream()`]: #http2_stream-pushstream diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index c258079356183d..1d93ae14a0bfa2 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -192,7 +192,7 @@ class Http2ServerRequest extends Readable { if (stream) { stream.resume(); } else { - throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED')); } } @@ -391,6 +391,18 @@ class Http2ServerResponse extends Stream { this[kBeginSend](); } + get statusMessage() { + if (statusMessageWarned === false) { + process.emitWarning( + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)', + 'UnsupportedWarning' + ); + statusMessageWarned = true; + } + + return ''; + } + writeHead(statusCode, statusMessage, headers) { if (typeof statusMessage === 'string' && statusMessageWarned === false) { process.emitWarning( @@ -411,6 +423,7 @@ class Http2ServerResponse extends Stream { } } this.statusCode = statusCode; + // TODO mcollina this should probably call sendInfo } write(chunk, encoding, cb) { @@ -497,7 +510,8 @@ class Http2ServerResponse extends Stream { createPushResponse(headers, callback) { const stream = this[kStream]; if (stream === undefined) { - throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED')); + return; } stream.pushStream(headers, {}, function(stream, headers, options) { const response = new Http2ServerResponse(stream); @@ -529,6 +543,11 @@ class Http2ServerResponse extends Stream { this[kStream] = undefined; this.emit('finish'); } + + // added for parity with HTTP/1 + writeContinue() { + // TODO mcollina this should probably be sendContinue + } } function onServerStream(stream, headers, flags) { diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js index 68e438d62ff96d..9679215abb0772 100644 --- a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js +++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js @@ -22,6 +22,16 @@ const server = h2.createServer((request, response) => { assert.strictEqual(push.stream.id % 2, 0); push.end(pushExpect); response.end(); + + // wait for a tick, so the stream is actually closed + setImmediate(function() { + response.createPushResponse({ + ':path': '/pushed', + ':method': 'GET' + }, common.mustCall((error) => { + assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_CLOSED'); + })); + }); })); }); diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js new file mode 100644 index 00000000000000..50d2970d9ea95f --- /dev/null +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js @@ -0,0 +1,47 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const h2 = require('http2'); + +// Http2ServerResponse.statusMessage should warn + +const unsupportedWarned = common.mustCall(1); +process.on('warning', ({ name, message }) => { + const expectedMessage = + 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)'; + if (name === 'UnsupportedWarning' && message === expectedMessage) + unsupportedWarned(); +}); + +const server = h2.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + server.once('request', common.mustCall(function(request, response) { + response.on('finish', common.mustCall(function() { + assert.strictEqual(response.statusMessage, ''); + server.close(); + })); + response.end(); + })); + + const url = `http://localhost:${port}`; + const client = h2.connect(url, common.mustCall(function() { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'http', + ':authority': `localhost:${port}` + }; + const request = client.request(headers); + request.on('response', common.mustCall(function(headers) { + assert.strictEqual(headers[':status'], 200); + }, 1)); + request.on('end', common.mustCall(function() { + client.destroy(); + })); + request.end(); + request.resume(); + })); +})); From 7824fa0b405cbf3df716bc15f470f4200900769a Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 24 Jul 2017 19:04:53 +0100 Subject: [PATCH 69/97] http2: make writeHead behave like HTTP/1. Backport-PR-URL: https://github.com/nodejs/node/pull/14813 Backport-Reviewed-By: Anna Henningsen Backport-Reviewed-By: Timothy Gu PR-URL: https://github.com/nodejs/node/pull/14239 Reviewed-By: Anna Henningsen Reviewed-By: Colin Ihrig Reviewed-By: Matteo Collina --- doc/api/http2.md | 2 +- lib/internal/http2/compat.js | 43 +++++++------------ .../test-http2-compat-serverresponse-end.js | 1 - ...ttp2-compat-serverresponse-flushheaders.js | 11 ++++- ...t-http2-compat-serverresponse-writehead.js | 6 ++- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/doc/api/http2.md b/doc/api/http2.md index 6f106c69c1b18b..bf2f6a3c526e7b 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -2402,7 +2402,7 @@ buffer. Returns `false` if all or part of the data was queued in user memory. added: REPLACEME --> -Does nothing. Added for parity with [HTTP/1](). +Throws an error as the `'continue'` flow is not current implemented. Added for parity with [HTTP/1](). ### response.writeHead(statusCode[, statusMessage][, headers]) - -The `'fetchTrailers'` event is emitted by the `Http2Stream` immediately after -queuing the last chunk of payload data to be sent. The listener callback is -passed a single object (with a `null` prototype) that the listener may used -to specify the trailing header fields to send to the peer. - -```js -stream.on('fetchTrailers', (trailers) => { - trailers['ABC'] = 'some value to send'; -}); -``` - -*Note*: The HTTP/1 specification forbids trailers from containing HTTP/2 -"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event -will be emitted if the `'fetchTrailers'` event handler attempts to set such -header fields. - #### Event: 'frameError' A `Http2ServerRequest` object is created by [`http2.Server`][] or -[`http2.SecureServer`][] and passed as the first argument to the [`'request'`][] event. It may be used to access a request status, -headers and data. +[`http2.SecureServer`][] and passed as the first argument to the +[`'request'`][] event. It may be used to access a request status, headers and +data. It implements the [Readable Stream][] interface, as well as the following additional events, methods, and properties. @@ -1907,9 +1908,9 @@ added: REPLACEME * `error` {Error} -Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If `error` -is provided, an `'error'` event is emitted and `error` is passed as an argument -to any listeners on the event. +Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If +`error` is provided, an `'error'` event is emitted and `error` is passed as an +argument to any listeners on the event. It does nothing if the stream was already destroyed. @@ -2201,7 +2202,7 @@ added: REPLACEME * `name` {string} * Returns: {string} -Reads out a header that's already been queued but not sent to the client. +Reads out a header that has already been queued but not sent to the client. Note that the name is case insensitive. Example: @@ -2291,7 +2292,7 @@ added: REPLACEME * `name` {string} -Removes a header that's queued for implicit sending. +Removes a header that has been queued for implicit sending. Example: @@ -2339,9 +2340,9 @@ response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']); Attempting to set a header field name or value that contains invalid characters will result in a [`TypeError`][] being thrown. -When headers have been set with [`response.setHeader()`][], they will be merged with -any headers passed to [`response.writeHead()`][], with the headers passed to -[`response.writeHead()`][] given precedence. +When headers have been set with [`response.setHeader()`][], they will be merged +with any headers passed to [`response.writeHead()`][], with the headers passed +to [`response.writeHead()`][] given precedence. ```js // returns content-type = text/plain @@ -2366,9 +2367,9 @@ provided, then it is added as a listener on the `'timeout'` event on the response object. If no `'timeout'` listener is added to the request, the response, or -the server, then [`Http2Stream`]()s are destroyed when they time out. If a handler is -assigned to the request, the response, or the server's `'timeout'` events, -timed out sockets must be handled explicitly. +the server, then [`Http2Stream`]()s are destroyed when they time out. If a +handler is assigned to the request, the response, or the server's `'timeout'` +events, timed out sockets must be handled explicitly. Returns `response`. @@ -2478,7 +2479,8 @@ buffer. Returns `false` if all or part of the data was queued in user memory. added: REPLACEME --> -Throws an error as the `'continue'` flow is not current implemented. Added for parity with [HTTP/1](). +Throws an error as the `'continue'` flow is not current implemented. Added for +parity with [HTTP/1](). ### response.writeHead(statusCode[, statusMessage][, headers]) From 621c03acfe13e6280a1e6e14432f999d96bd27f7 Mon Sep 17 00:00:00 2001 From: Refael Ackermann Date: Wed, 9 Aug 2017 00:16:41 -0400 Subject: [PATCH 94/97] doc: delint PR-URL: https://github.com/nodejs/node/pull/14707 Refs: https://github.com/nodejs/node/pull/12756 Reviewed-By: Luigi Pinca Reviewed-By: Daijiro Wachi Reviewed-By: Sakthipriyan Vairamani --- doc/api/http2.md | 18 +++++++++--------- doc/api/n-api.md | 6 ++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/doc/api/http2.md b/doc/api/http2.md index cd034c5db882c0..78e2fad23a884b 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -1850,8 +1850,8 @@ const server = http2.createServer((req, res) => { }); ``` -In order to create a mixed [HTTPS](https) and HTTP/2 server, refer to the -[ALPN negotiation](alpn-negotiation) section. +In order to create a mixed [HTTPS][] and HTTP/2 server, refer to the +[ALPN negotiation][] section. Upgrading from non-tls HTTP/1 servers is not supported. The HTTP2 compatibility API is composed of [`Http2ServerRequest`]() and @@ -1861,10 +1861,10 @@ the status message for HTTP codes is ignored. ### ALPN negotiation -ALPN negotiation allows to support both [HTTPS](https) and HTTP/2 over +ALPN negotiation allows to support both [HTTPS][] and HTTP/2 over the same socket. The `req` and `res` objects can be either HTTP/1 or HTTP/2, and an application **must** restrict itself to the public API of -[HTTP/1](), and detect if it is possible to use the more advanced +[HTTP/1][], and detect if it is possible to use the more advanced features of HTTP/2. The following example creates a server that supports both protocols: @@ -1925,7 +1925,7 @@ abnormally aborted in mid-communication. added: REPLACEME --> -Indicates that the underlying [`Http2Stream`]() was closed. +Indicates that the underlying [`Http2Stream`][] was closed. Just like `'end'`, this event occurs only once per response. #### request.destroy([error]) @@ -1935,7 +1935,7 @@ added: REPLACEME * `error` {Error} -Calls `destroy()` on the [Http2Stream]() that received the `ServerRequest`. If +Calls `destroy()` on the [`Http2Stream`][] that received the [`ServerRequest`][]. If `error` is provided, an `'error'` event is emitted and `error` is passed as an argument to any listeners on the event. @@ -2570,7 +2570,7 @@ will result in a [`TypeError`][] being thrown. added: REPLACEME --> -Call [`stream.pushStream()`]() with the given headers, and wraps the +Call [`stream.pushStream()`][] with the given headers, and wraps the given newly created [`Http2Stream`] on `Http2ServerRespose`. The callback will be called with an error with code `ERR_HTTP2_STREAM_CLOSED` @@ -2578,13 +2578,13 @@ if the stream is closed. [HTTP/2]: https://tools.ietf.org/html/rfc7540 [HTTP/1]: http.html -[https]: https.html +[HTTPS]: https.html [`net.Socket`]: net.html [`tls.TLSSocket`]: tls.html [`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener [`ClientHttp2Stream`]: #http2_class_clienthttp2stream [Compatibility API]: #http2_compatibility_api -[alpn-negotiation]: #http2_alpn_negotiation +[ALPN negotiation]: #http2_alpn_negotiation [`Duplex`]: stream.html#stream_class_stream_duplex [Headers Object]: #http2_headers_object [`Http2Stream`]: #http2_class_http2stream diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 9e7fdd9301332e..04af7a62723c70 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -342,7 +342,7 @@ to be added to the error object. If the optional parameter is NULL then no code will be associated with the error. If a code is provided, the name associated with the error is also updated to be: -``` +```text originalName [code] ``` @@ -350,7 +350,7 @@ where originalName is the original name associated with the error and code is the code that was provided. For example if the code is 'ERR_ERROR_1' and a TypeError is being created the name will be: -``` +```text TypeError [ERR_ERROR_1] ``` @@ -3342,6 +3342,8 @@ support it: * If the function is not available, provide an alternate implementation that does not use the function. + + [Asynchronous Operations]: #n_api_asynchronous_operations [Basic N-API Data Types]: #n_api_basic_n_api_data_types [ECMAScript Language Specification]: https://tc39.github.io/ecma262/ From 1d408503383858297fb7710472024911910ef343 Mon Sep 17 00:00:00 2001 From: Evan Lucas Date: Thu, 10 Aug 2017 17:00:01 -0500 Subject: [PATCH 95/97] http2: fix [kInspect]() output for Http2Stream This fixes a typo in the util.inspect output of Http2Stream. It previously had writeableSate instead of writableState. PR-URL: https://github.com/nodejs/node/pull/14753 Reviewed-By: Anna Henningsen Reviewed-By: Timothy Gu Reviewed-By: James M Snell Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca --- lib/internal/http2/core.js | 2 +- test/parallel/test-http2-stream-client.js | 29 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-http2-stream-client.js diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 6a701179cee0da..c3681383f87e80 100755 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1315,7 +1315,7 @@ class Http2Stream extends Duplex { id: this[kID], state: this.state, readableState: this._readableState, - writeableSate: this._writableState + writableState: this._writableState }; return `Http2Stream ${util.format(obj)}`; } diff --git a/test/parallel/test-http2-stream-client.js b/test/parallel/test-http2-stream-client.js new file mode 100644 index 00000000000000..658c66ce7af895 --- /dev/null +++ b/test/parallel/test-http2-stream-client.js @@ -0,0 +1,29 @@ +// Flags: --expose-http2 +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const util = require('util'); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + assert.strictEqual(stream.aborted, false); + const insp = util.inspect(stream); + assert.ok(/Http2Stream { id/.test(insp)); + assert.ok(/ state:/.test(insp)); + assert.ok(/ readableState:/.test(insp)); + assert.ok(/ writableState:/.test(insp)); + stream.end('ok'); +})); +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + req.resume(); + req.on('streamClosed', common.mustCall(() => { + client.destroy(); + server.close(); + })); +})); From dd521d0a2801f1c2a482e270eddba44dc0e88ccc Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 13 Aug 2017 15:42:18 +0200 Subject: [PATCH 96/97] deps,tools: add missing nghttp2 license MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `COPYING` from `nghttp2` and register it in `tools/license-builder.sh`. Also run `tools/license-builder.sh` and commit the resulting `LICENSE` file. Ref: https://github.com/nghttp2/nghttp2/blob/master/COPYING PR-URL: https://github.com/nodejs/node/pull/14806 Reviewed-By: Refael Ackermann Reviewed-By: Daniel Bevenius Reviewed-By: Luigi Pinca Reviewed-By: James M Snell Reviewed-By: Timothy Gu Reviewed-By: Tobias Nießen --- LICENSE | 64 +++++++++++++++++++++++++++++++++++----- deps/nghttp2/COPYING | 23 +++++++++++++++ tools/license-builder.sh | 2 ++ 3 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 deps/nghttp2/COPYING diff --git a/LICENSE b/LICENSE index 53ebbc70529ed5..c7ed5897ecd057 100644 --- a/LICENSE +++ b/LICENSE @@ -487,8 +487,32 @@ The externally maintained libraries used by Node.js are: - libuv, located at deps/uv, is licensed as follows: """ - libuv is part of the Node project: http://nodejs.org/ - libuv may be distributed alone under Node's license: + libuv is licensed for use as follows: + + ==== + Copyright (c) 2015-present libuv project contributors. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + ==== + + This license applies to parts of libuv originating from the + https://github.com/joyent/libuv repository: ==== @@ -537,7 +561,7 @@ The externally maintained libraries used by Node.js are: - OpenSSL, located at deps/openssl, is licensed as follows: """ - Copyright (c) 1998-2016 The OpenSSL Project. All rights reserved. + Copyright (c) 1998-2017 The OpenSSL Project. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions @@ -677,9 +701,9 @@ The externally maintained libraries used by Node.js are: - zlib, located at deps/zlib, is licensed as follows: """ zlib.h -- interface of the 'zlib' general purpose compression library - version 1.2.8, April 28th, 2013 + version 1.2.11, January 15th, 2017 - Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages @@ -1017,8 +1041,7 @@ The externally maintained libraries used by Node.js are: - ESLint, located at tools/eslint, is licensed as follows: """ - ESLint - Copyright jQuery Foundation and other contributors, https://jquery.org/ + Copyright JS Foundation and other contributors, https://js.foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -1070,3 +1093,30 @@ The externally maintained libraries used by Node.js are: (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ + +- nghttp2, located at deps/nghttp2, is licensed as follows: + """ + The MIT License + + Copyright (c) 2012, 2014, 2015, 2016 Tatsuhiro Tsujikawa + Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + """ diff --git a/deps/nghttp2/COPYING b/deps/nghttp2/COPYING new file mode 100644 index 00000000000000..80201792ec7234 --- /dev/null +++ b/deps/nghttp2/COPYING @@ -0,0 +1,23 @@ +The MIT License + +Copyright (c) 2012, 2014, 2015, 2016 Tatsuhiro Tsujikawa +Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tools/license-builder.sh b/tools/license-builder.sh index de06fdb2e2a2c5..62eabf0e1f8c0a 100755 --- a/tools/license-builder.sh +++ b/tools/license-builder.sh @@ -80,6 +80,8 @@ addlicense "cpplint.py" "tools/cpplint.py" \ addlicense "ESLint" "tools/eslint" "$(cat ${rootdir}/tools/eslint/LICENSE)" addlicense "gtest" "deps/gtest" "$(cat ${rootdir}/deps/gtest/LICENSE)" +# nghttp2 +addlicense "nghttp2" "deps/nghttp2" "$(cat ${rootdir}/deps/nghttp2/COPYING)" mv $tmplicense $licensefile From 56a0577b2ebf111346779cf790a1f54e0e464078 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sun, 13 Aug 2017 22:33:49 +0200 Subject: [PATCH 97/97] 2017-08-15, Version 8.4.0 (Current) Notable changes * **HTTP2** * Experimental support for the built-in `http2` has been added via the `--expose-http2` flag. [#14239](https://github.com/nodejs/node/pull/14239) * **Inspector** * `require()` is available in the inspector console now. [#8837](https://github.com/nodejs/node/pull/8837) * Multiple contexts, as created by the `vm` module, are supported now. [#14465](https://github.com/nodejs/node/pull/14465) * **N-API** * New APIs for creating number values have been introduced. [#14573](https://github.com/nodejs/node/pull/14573) * **Stream** * For `Duplex` streams, the high water mark option can now be set independently for the readable and the writable side. [#14636](https://github.com/nodejs/node/pull/14636) * **Util** * `util.format` now supports the `%o` and `%O` specifiers for printing objects. [#14558](https://github.com/nodejs/node/pull/14558) PR-URL: https://github.com/nodejs/node/pull/14811 --- doc/api/cli.md | 2 +- doc/api/http2.md | 230 ++++++++++++++++----------------- doc/api/n-api.md | 10 +- doc/api/stream.md | 2 +- doc/api/tls.md | 2 +- doc/api/util.md | 2 +- doc/changelogs/CHANGELOG_V8.md | 130 +++++++++++++++++++ src/node_version.h | 6 +- 8 files changed, 257 insertions(+), 127 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index b5b31aac2b8e9c..4d3df97df530e6 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -172,7 +172,7 @@ Silence all process warnings (including deprecations). ### `--expose-http2` Enable the experimental `'http2'` module. diff --git a/doc/api/http2.md b/doc/api/http2.md index 78e2fad23a884b..eaf88483cd0bd4 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -69,7 +69,7 @@ req.end(); ### Class: Http2Session * Extends: {EventEmitter} @@ -103,14 +103,14 @@ solely on the API of the `Http2Session`. #### Event: 'close' The `'close'` event is emitted once the `Http2Session` has been terminated. #### Event: 'connect' The `'connect'` event is emitted once the `Http2Session` has been successfully @@ -120,7 +120,7 @@ connected to the remote peer and communication may begin. #### Event: 'error' The `'error'` event is emitted when an error occurs during the processing of @@ -128,7 +128,7 @@ an `Http2Session`. #### Event: 'frameError' The `'frameError'` event is emitted when an error occurs while attempting to @@ -150,7 +150,7 @@ immediately following the `'frameError'` event. #### Event: 'goaway' The `'goaway'` event is emitted when a GOAWAY frame is received. When invoked, @@ -167,7 +167,7 @@ the handler function will receive three arguments: #### Event: 'localSettings' The `'localSettings'` event is emitted when an acknowledgement SETTINGS frame @@ -188,7 +188,7 @@ session.on('localSettings', (settings) => { #### Event: 'remoteSettings' The `'remoteSettings'` event is emitted when a new SETTINGS frame is received @@ -203,7 +203,7 @@ session.on('remoteSettings', (settings) => { #### Event: 'stream' The `'stream'` event is emitted when a new `Http2Stream` is created. When @@ -256,7 +256,7 @@ server.listen(80); #### Event: 'socketError' The `'socketError'` event is emitted when an `'error'` is emitted on the @@ -269,7 +269,7 @@ registered that will, by default, forward the event on to the owning #### Event: 'timeout' After the `http2session.setTimeout()` method is used to set the timeout period @@ -283,7 +283,7 @@ session.on('timeout', () => { /** .. **/ }); #### http2session.destroy() * Returns: {undefined} @@ -293,7 +293,7 @@ Immediately terminates the `Http2Session` and the associated `net.Socket` or #### http2session.destroyed * Value: {boolean} @@ -303,7 +303,7 @@ longer be used, otherwise `false`. #### http2session.localSettings * Value: {[Settings Object][]} @@ -313,7 +313,7 @@ A prototype-less object describing the current local settings of this #### http2session.pendingSettingsAck * Value: {boolean} @@ -325,7 +325,7 @@ frames have been acknowledged. #### http2session.remoteSettings * Value: {[Settings Object][]} @@ -335,7 +335,7 @@ A prototype-less object describing the current remote settings of this #### http2session.request(headers[, options]) * `headers` {[Headers Object][]} @@ -392,7 +392,7 @@ fields. #### http2session.rstStream(stream, code) * stream {Http2Stream} @@ -405,7 +405,7 @@ Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing the given #### http2session.setTimeout(msecs, callback) * `msecs` {number} @@ -418,7 +418,7 @@ registered as a listener on the `'timeout'` event. #### http2session.shutdown(options[, callback]) * `options` {Object} @@ -460,7 +460,7 @@ session.shutdown({ #### http2session.socket * Value: {net.Socket|tls.TLSSocket} @@ -474,7 +474,7 @@ details. #### http2session.state * Value: {Object} @@ -492,7 +492,7 @@ An object describing the current status of this `Http2Session`. #### http2session.priority(stream, options) * `stream` {Http2Stream} @@ -514,7 +514,7 @@ Updates the priority for the given `Http2Stream` instance. #### http2session.settings(settings) * `settings` {[Settings Object][]} @@ -534,7 +534,7 @@ pending. #### http2session.type * Value: {number} @@ -546,7 +546,7 @@ client. ### Class: Http2Stream * Extends: {Duplex} @@ -619,7 +619,7 @@ destroyed. #### Event: 'aborted' The `'aborted'` event is emitted whenever a `Http2Stream` instance is @@ -630,7 +630,7 @@ writable side has not been ended. #### Event: 'error' The `'error'` event is emitted when an error occurs during the processing of @@ -638,7 +638,7 @@ an `Http2Stream`. #### Event: 'frameError' The `'frameError'` event is emitted when an error occurs while attempting to @@ -649,7 +649,7 @@ error code. The `Http2Stream` instance will be destroyed immediately after the #### Event: 'streamClosed' The `'streamClosed'` event is emitted when the `Http2Stream` is destroyed. Once @@ -661,7 +661,7 @@ code specified when closing the stream. If the code is any value other than #### Event: 'timeout' The `'timeout'` event is emitted after no activity is received for this @@ -670,7 +670,7 @@ The `'timeout'` event is emitted after no activity is received for this #### Event: 'trailers' The `'trailers'` event is emitted when a block of headers associated with @@ -685,7 +685,7 @@ stream.on('trailers', (headers, flags) => { #### http2stream.aborted * Value: {boolean} @@ -695,7 +695,7 @@ the `'aborted'` event will have been emitted. #### http2stream.destroyed * Value: {boolean} @@ -705,7 +705,7 @@ usable. #### http2stream.priority(options) * `options` {Object} @@ -726,7 +726,7 @@ Updates the priority for this `Http2Stream` instance. #### http2stream.rstCode * Value: {number} @@ -738,7 +738,7 @@ calling `http2stream.rstStream()`, or `http2stream.destroy()`. Will be #### http2stream.rstStream(code) * code {number} Unsigned 32-bit integer identifying the error code. Defaults to @@ -750,7 +750,7 @@ Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing this #### http2stream.rstWithNoError() * Returns: {undefined} @@ -759,7 +759,7 @@ Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error). #### http2stream.rstWithProtocolError() { * Returns: {undefined} @@ -768,7 +768,7 @@ Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error). #### http2stream.rstWithCancel() { * Returns: {undefined} @@ -777,7 +777,7 @@ Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel). #### http2stream.rstWithRefuse() { * Returns: {undefined} @@ -786,7 +786,7 @@ Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream). #### http2stream.rstWithInternalError() { * Returns: {undefined} @@ -795,7 +795,7 @@ Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error). #### http2stream.session * Value: {Http2Sesssion} @@ -805,7 +805,7 @@ value will be `undefined` after the `Http2Stream` instance is destroyed. #### http2stream.setTimeout(msecs, callback) * `msecs` {number} @@ -824,7 +824,7 @@ req.setTimeout(5000, () => req.rstStreamWithCancel()); #### http2stream.state * Value: {Object} @@ -839,7 +839,7 @@ A current state of this `Http2Stream`. ### Class: ClientHttp2Stream * Extends {Http2Stream} @@ -851,7 +851,7 @@ the client. #### Event: 'headers' The `'headers'` event is emitted when an additional block of headers is received @@ -867,7 +867,7 @@ stream.on('headers', (headers, flags) => { #### Event: 'push' The `'push'` event is emitted when response headers for a Server Push stream @@ -882,7 +882,7 @@ stream.on('push', (headers, flags) => { #### Event: 'response' The `'response'` event is emitted when a response `HEADERS` frame has been @@ -903,7 +903,7 @@ req.on('response', (headers, flags) => { ### Class: ServerHttp2Stream * Extends: {Http2Stream} @@ -915,7 +915,7 @@ provide additional methods such as `http2stream.pushStream()` and #### http2stream.additionalHeaders(headers) * `headers` {[Headers Object][]} @@ -925,7 +925,7 @@ Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer. #### http2stream.headersSent * Value: {boolean} @@ -934,7 +934,7 @@ Boolean (read-only). True if headers were sent, false otherwise. #### http2stream.pushAllowed * Value: {boolean} @@ -946,7 +946,7 @@ accepts push streams, `false` otherwise. Settings are the same for every #### http2stream.pushStream(headers[, options], callback) * `headers` {[Headers Object][]} @@ -982,7 +982,7 @@ server.on('stream', (stream) => { #### http2stream.respond([headers[, options]]) * `headers` {[Headers Object][]} @@ -1027,7 +1027,7 @@ fields. #### http2stream.respondWithFD(fd[, headers[, options]]) * `fd` {number} A readable file descriptor @@ -1111,7 +1111,7 @@ fields. #### http2stream.respondWithFile(path[, headers[, options]]) * `path` {string|Buffer|URL} @@ -1203,14 +1203,14 @@ fields. ### Class: Http2Server * Extends: {net.Server} #### Event: 'sessionError' The `'sessionError'` event is emitted when an `'error'` event is emitted by @@ -1219,7 +1219,7 @@ an `Http2Session` object. If no listener is registered for this event, an #### Event: 'socketError' The `'socketError'` event is emitted when a `'socketError'` event is emitted by @@ -1227,7 +1227,7 @@ an `Http2Session` associated with the server. #### Event: 'stream' The `'stream'` event is emitted when a `'stream'` event has been emitted by @@ -1258,7 +1258,7 @@ server.on('stream', (stream, headers, flags) => { #### Event: 'request' * `request` {http2.Http2ServerRequest} @@ -1269,7 +1269,7 @@ per session. See the [Compatibility API](compatiblity-api). #### Event: 'timeout' The `'timeout'` event is emitted when there is no activity on the Server for @@ -1277,14 +1277,14 @@ a given number of milliseconds set using `http2server.setTimeout()`. ### Class: Http2SecureServer * Extends: {tls.Server} #### Event: 'sessionError' The `'sessionError'` event is emitted when an `'error'` event is emitted by @@ -1293,7 +1293,7 @@ an `Http2Session` object. If no listener is registered for this event, an #### Event: 'socketError' The `'socketError'` event is emitted when a `'socketError'` event is emitted by @@ -1301,7 +1301,7 @@ an `Http2Session` associated with the server. #### Event: 'unknownProtocol' The `'unknownProtocol'` event is emitted when a connecting client fails to @@ -1311,7 +1311,7 @@ the connection is terminated. See the #### Event: 'stream' The `'stream'` event is emitted when a `'stream'` event has been emitted by @@ -1344,7 +1344,7 @@ server.on('stream', (stream, headers, flags) => { #### Event: 'request' * `request` {http2.Http2ServerRequest} @@ -1355,12 +1355,12 @@ per session. See the [Compatibility API](compatiblity-api). #### Event: 'timeout' ### http2.createServer(options[, onRequestHandler]) * `options` {Object} @@ -1415,7 +1415,7 @@ server.listen(80); ### http2.createSecureServer(options[, onRequestHandler]) * `options` {Object} @@ -1481,7 +1481,7 @@ server.listen(80); ### http2.connect(authority[, options][, listener]) * `authority` {string|URL} @@ -1532,7 +1532,7 @@ client.destroy(); ### http2.constants #### Error Codes for RST_STREAM and GOAWAY @@ -1560,7 +1560,7 @@ a given number of milliseconds set using `http2server.setTimeout()`. ### http2.getDefaultSettings() * Returns: {[Settings Object][]} @@ -1571,7 +1571,7 @@ so instances returned may be safely modified for use. ### http2.getPackedSettings(settings) * `settings` {[Settings Object][]} @@ -1592,7 +1592,7 @@ console.log(packed.toString('base64')); ### http2.getUnpackedSettings(buf) * `buf` {Buffer|Uint8Array} The packed settings @@ -1898,7 +1898,7 @@ HTTP/2. ### Class: http2.Http2ServerRequest A `Http2ServerRequest` object is created by [`http2.Server`][] or @@ -1911,7 +1911,7 @@ following additional events, methods, and properties. #### Event: 'aborted' The `'aborted'` event is emitted whenever a `Http2ServerRequest` instance is @@ -1922,7 +1922,7 @@ abnormally aborted in mid-communication. #### Event: 'close' Indicates that the underlying [`Http2Stream`][] was closed. @@ -1930,7 +1930,7 @@ Just like `'end'`, this event occurs only once per response. #### request.destroy([error]) * `error` {Error} @@ -1943,7 +1943,7 @@ It does nothing if the stream was already destroyed. #### request.headers * {Object} @@ -1966,7 +1966,7 @@ See [Headers Object][]. ### request.httpVersion * {string} @@ -1980,7 +1980,7 @@ Also `message.httpVersionMajor` is the first integer and #### request.method * {string} @@ -1990,7 +1990,7 @@ The request method as a string. Read only. Example: #### request.rawHeaders * {Array} @@ -2019,7 +2019,7 @@ console.log(request.rawHeaders); #### request.rawTrailers * {Array} @@ -2029,7 +2029,7 @@ received. Only populated at the `'end'` event. #### request.setTimeout(msecs, callback) * `msecs` {number} @@ -2041,7 +2041,7 @@ Returns `request`. #### request.socket * {net.Socket} @@ -2056,7 +2056,7 @@ data transfers are managed by HTTP/2 and data might be lost. #### request.stream * {http2.Http2Stream} @@ -2065,7 +2065,7 @@ The [`Http2Stream`][] object backing the request. #### request.trailers * {Object} @@ -2074,7 +2074,7 @@ The request/response trailers object. Only populated at the `'end'` event. #### request.url * {string} @@ -2141,7 +2141,7 @@ Url { ### Class: http2.Http2ServerResponse This object is created internally by an HTTP server--not by the user. It is @@ -2152,7 +2152,7 @@ interface. This is an [`EventEmitter`][] with the following events: ### Event: 'close' Indicates that the underlying [`Http2Stream`]() was terminated before @@ -2160,7 +2160,7 @@ Indicates that the underlying [`Http2Stream`]() was terminated before ### Event: 'finish' Emitted when the response has been sent. More specifically, this event is @@ -2172,7 +2172,7 @@ After this event, no more events will be emitted on the response object. ### response.addTrailers(headers) * `headers` {Object} @@ -2185,7 +2185,7 @@ will result in a [`TypeError`][] being thrown. ### response.connection * {net.Socket} @@ -2194,7 +2194,7 @@ See [`response.socket`][]. ### response.end([data][, encoding][, callback]) * `data` {string|Buffer} @@ -2213,7 +2213,7 @@ is finished. ### response.finished * {boolean} @@ -2223,7 +2223,7 @@ as `false`. After [`response.end()`][] executes, the value will be `true`. ### response.getHeader(name) * `name` {string} @@ -2240,7 +2240,7 @@ const contentType = response.getHeader('content-type'); ### response.getHeaderNames() * Returns: {Array} @@ -2260,7 +2260,7 @@ const headerNames = response.getHeaderNames(); ### response.getHeaders() * Returns: {Object} @@ -2288,7 +2288,7 @@ const headers = response.getHeaders(); ### response.hasHeader(name) * `name` {string} @@ -2305,7 +2305,7 @@ const hasContentType = response.hasHeader('content-type'); ### response.headersSent * {boolean} @@ -2314,7 +2314,7 @@ Boolean (read-only). True if headers were sent, false otherwise. ### response.removeHeader(name) * `name` {string} @@ -2329,7 +2329,7 @@ response.removeHeader('Content-Encoding'); ### response.sendDate * {boolean} @@ -2342,7 +2342,7 @@ in responses. ### response.setHeader(name, value) * `name` {string} @@ -2383,7 +2383,7 @@ const server = http2.createServer((req, res) => { ### response.setTimeout(msecs[, callback]) * `msecs` {number} @@ -2402,7 +2402,7 @@ Returns `response`. ### response.socket * {net.Socket} @@ -2426,7 +2426,7 @@ const server = http2.createServer((req, res) => { ### response.statusCode * {number} @@ -2446,7 +2446,7 @@ status code which was sent out. ### response.statusMessage * {string} @@ -2456,7 +2456,7 @@ an empty string. #### response.stream * {http2.Http2Stream} @@ -2465,7 +2465,7 @@ The [`Http2Stream`][] object backing the response. ### response.write(chunk[, encoding][, callback]) * `chunk` {string|Buffer} @@ -2503,7 +2503,7 @@ buffer. Returns `false` if all or part of the data was queued in user memory. ### response.writeContinue() Throws an error as the `'continue'` flow is not current implemented. Added for @@ -2511,7 +2511,7 @@ parity with [HTTP/1](). ### response.writeHead(statusCode[, statusMessage][, headers]) * `statusCode` {number} @@ -2567,7 +2567,7 @@ will result in a [`TypeError`][] being thrown. ### response.createPushResponse(headers, callback) Call [`stream.pushStream()`][] with the given headers, and wraps the diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 04af7a62723c70..109674a4bf4b06 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -1374,7 +1374,7 @@ JavaScript DataView Objects are described in ### Functions to convert from C types to N-API #### *napi_create_int32* ```C napi_status napi_create_int32(napi_env env, int32_t value, napi_value* result) @@ -1395,7 +1395,7 @@ of the ECMAScript Language Specification. #### *napi_create_uint32* ```C napi_status napi_create_uint32(napi_env env, uint32_t value, napi_value* result) @@ -1416,7 +1416,7 @@ of the ECMAScript Language Specification. #### *napi_create_int64* ```C napi_status napi_create_int64(napi_env env, int64_t value, napi_value* result) @@ -1443,7 +1443,7 @@ outside the range of #### *napi_create_double* ```C napi_status napi_create_double(napi_env env, double value, napi_value* result) @@ -3287,7 +3287,7 @@ callback invocation, even if it has been successfully cancelled. ### napi_get_node_version ```C diff --git a/doc/api/stream.md b/doc/api/stream.md index 7d42bd2d71ca6a..3663fe2d780830 100644 --- a/doc/api/stream.md +++ b/doc/api/stream.md @@ -1742,7 +1742,7 @@ constructor and implement *both* the `readable._read()` and #### new stream.Duplex(options) Disables TLS renegotiation for this `TLSSocket` instance. Once called, attempts diff --git a/doc/api/util.md b/doc/api/util.md index c853d04716c310..076fbc479dc695 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -151,7 +151,7 @@ property take precedence over `--trace-deprecation` and diff --git a/doc/changelogs/CHANGELOG_V8.md b/doc/changelogs/CHANGELOG_V8.md index 74b2ca1f5c9153..2413d874778933 100644 --- a/doc/changelogs/CHANGELOG_V8.md +++ b/doc/changelogs/CHANGELOG_V8.md @@ -6,6 +6,7 @@ +8.4.0
8.3.0
8.2.1
8.2.0
@@ -29,6 +30,135 @@ * [io.js](CHANGELOG_IOJS.md) * [Archive](CHANGELOG_ARCHIVE.md) + +## 2017-08-15, Version 8.4.0 (Current), @addaleax + +### Notable changes + +* **HTTP2** + * Experimental support for the built-in `http2` has been added via the + `--expose-http2` flag. + [#14239](https://github.com/nodejs/node/pull/14239) + +* **Inspector** + * `require()` is available in the inspector console now. + [#8837](https://github.com/nodejs/node/pull/8837) + * Multiple contexts, as created by the `vm` module, are supported now. + [#14465](https://github.com/nodejs/node/pull/14465) + +* **N-API** + * New APIs for creating number values have been introduced. + [#14573](https://github.com/nodejs/node/pull/14573) + +* **Stream** + * For `Duplex` streams, the high water mark option can now be set + independently for the readable and the writable side. + [#14636](https://github.com/nodejs/node/pull/14636) + +* **Util** + * `util.format` now supports the `%o` and `%O` specifiers for printing + objects. + [#14558](https://github.com/nodejs/node/pull/14558) + +### Commits + +* [[`a6539ece2c`](https://github.com/nodejs/node/commit/a6539ece2c)] - **assert**: optimize code path for deepEqual Maps (Ruben Bridgewater) [#14501](https://github.com/nodejs/node/pull/14501) +* [[`2716b626b0`](https://github.com/nodejs/node/commit/2716b626b0)] - **async_hooks**: CHECK that resource is not empty (Anna Henningsen) [#14694](https://github.com/nodejs/node/pull/14694) +* [[`b3c1c6ff7f`](https://github.com/nodejs/node/commit/b3c1c6ff7f)] - **benchmark**: fix and extend assert benchmarks (Ruben Bridgewater) [#14147](https://github.com/nodejs/node/pull/14147) +* [[`139b08863e`](https://github.com/nodejs/node/commit/139b08863e)] - **benchmark**: Correct constructor for freelist (Gareth Ellis) [#14627](https://github.com/nodejs/node/pull/14627) +* [[`574cc379b9`](https://github.com/nodejs/node/commit/574cc379b9)] - **benchmark**: remove unused parameters (nishijayaraj) [#14640](https://github.com/nodejs/node/pull/14640) +* [[`fef2aa7e27`](https://github.com/nodejs/node/commit/fef2aa7e27)] - **(SEMVER-MINOR)** **deps**: add nghttp2 dependency (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`2d806f4f71`](https://github.com/nodejs/node/commit/2d806f4f71)] - **deps**: cherry-pick f19b889 from V8 upstream (Alexey Kozyatinskiy) [#14465](https://github.com/nodejs/node/pull/14465) +* [[`dd521d0a28`](https://github.com/nodejs/node/commit/dd521d0a28)] - **deps,tools**: add missing nghttp2 license (Anna Henningsen) [#14806](https://github.com/nodejs/node/pull/14806) +* [[`621c03acfe`](https://github.com/nodejs/node/commit/621c03acfe)] - **doc**: delint (Refael Ackermann) [#14707](https://github.com/nodejs/node/pull/14707) +* [[`230cb55574`](https://github.com/nodejs/node/commit/230cb55574)] - **doc**: fix header level typo (Refael Ackermann) [#14707](https://github.com/nodejs/node/pull/14707) +* [[`af85b6e058`](https://github.com/nodejs/node/commit/af85b6e058)] - **doc**: fix http2 sample code for http2.md (Keita Akutsu) [#14667](https://github.com/nodejs/node/pull/14667) +* [[`1e7ddb200f`](https://github.com/nodejs/node/commit/1e7ddb200f)] - **doc**: explain browser support of http/2 without SSL (Gil Tayar) [#14670](https://github.com/nodejs/node/pull/14670) +* [[`be716d00cc`](https://github.com/nodejs/node/commit/be716d00cc)] - **(SEMVER-MINOR)** **doc**: include http2.md in all.md (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`9e51802f53`](https://github.com/nodejs/node/commit/9e51802f53)] - **doc**: add missing `changes:` metadata for util (Anna Henningsen) [#14810](https://github.com/nodejs/node/pull/14810) +* [[`4811fea553`](https://github.com/nodejs/node/commit/4811fea553)] - **doc**: add missing `changes:` metadata for streams (Anna Henningsen) [#14810](https://github.com/nodejs/node/pull/14810) +* [[`20fb69063a`](https://github.com/nodejs/node/commit/20fb69063a)] - **doc**: fix docs style in util.md (Daijiro Wachi) [#14711](https://github.com/nodejs/node/pull/14711) +* [[`0de63e6888`](https://github.com/nodejs/node/commit/0de63e6888)] - **doc**: fix docs style in intl.md (Daijiro Wachi) [#14711](https://github.com/nodejs/node/pull/14711) +* [[`ee2ae0f30b`](https://github.com/nodejs/node/commit/ee2ae0f30b)] - **doc**: expanded description of buffer.slice (Vishal Bisht) [#14720](https://github.com/nodejs/node/pull/14720) +* [[`9888bb1238`](https://github.com/nodejs/node/commit/9888bb1238)] - **doc**: improve fs.read() doc text (Rich Trott) [#14631](https://github.com/nodejs/node/pull/14631) +* [[`d604173a66`](https://github.com/nodejs/node/commit/d604173a66)] - **doc**: clarify the position argument for fs.read (dcharbonnier) [#14631](https://github.com/nodejs/node/pull/14631) +* [[`d3b072276b`](https://github.com/nodejs/node/commit/d3b072276b)] - **doc**: add docs for AssertionError (Mandeep Singh) [#14261](https://github.com/nodejs/node/pull/14261) +* [[`4e15a6b76a`](https://github.com/nodejs/node/commit/4e15a6b76a)] - **doc**: fix order of AtExit callbacks in addons.md (Daniel Bevenius) [#14048](https://github.com/nodejs/node/pull/14048) +* [[`e07dfffad0`](https://github.com/nodejs/node/commit/e07dfffad0)] - **doc**: remove undef NDEBUG from addons.md (Daniel Bevenius) [#14048](https://github.com/nodejs/node/pull/14048) +* [[`c5ee34e39b`](https://github.com/nodejs/node/commit/c5ee34e39b)] - **encoding**: rudimentary TextDecoder support w/o ICU (Timothy Gu) [#14489](https://github.com/nodejs/node/pull/14489) +* [[`e0001dc601`](https://github.com/nodejs/node/commit/e0001dc601)] - **(SEMVER-MINOR)** **http**: move utcDate to internal/http.js (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`1d40850338`](https://github.com/nodejs/node/commit/1d40850338)] - **http2**: fix \[kInspect\]() output for Http2Stream (Evan Lucas) [#14753](https://github.com/nodejs/node/pull/14753) +* [[`c5740f9111`](https://github.com/nodejs/node/commit/c5740f9111)] - **http2**: name padding buffer fields (Anna Henningsen) [#14744](https://github.com/nodejs/node/pull/14744) +* [[`8a0d101adf`](https://github.com/nodejs/node/commit/8a0d101adf)] - **http2**: use per-environment buffers (Anna Henningsen) [#14744](https://github.com/nodejs/node/pull/14744) +* [[`92c37fe5fd`](https://github.com/nodejs/node/commit/92c37fe5fd)] - **http2**: improve perf of passing headers to C++ (Anna Henningsen) [#14723](https://github.com/nodejs/node/pull/14723) +* [[`47bf705f75`](https://github.com/nodejs/node/commit/47bf705f75)] - **http2**: rename some nghttp2 stream flags (Kelvin Jin) [#14637](https://github.com/nodejs/node/pull/14637) +* [[`723d1af5e7`](https://github.com/nodejs/node/commit/723d1af5e7)] - **(SEMVER-MINOR)** **http2**: fix flakiness in timeout (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`6a30448bac`](https://github.com/nodejs/node/commit/6a30448bac)] - **(SEMVER-MINOR)** **http2**: fix linting after rebase (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`efd929e402`](https://github.com/nodejs/node/commit/efd929e402)] - **(SEMVER-MINOR)** **http2**: fix compilation error after V8 update (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`f46c50b3e2`](https://github.com/nodejs/node/commit/f46c50b3e2)] - **(SEMVER-MINOR)** **http2**: add some doc detail for invalid header chars (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`b43caf92c0`](https://github.com/nodejs/node/commit/b43caf92c0)] - **(SEMVER-MINOR)** **http2**: fix documentation errors (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`33b03b2ab2`](https://github.com/nodejs/node/commit/33b03b2ab2)] - **(SEMVER-MINOR)** **http2**: minor cleanup (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`174ab6fda0`](https://github.com/nodejs/node/commit/174ab6fda0)] - **(SEMVER-MINOR)** **http2**: use static allocated arrays (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`9a4be4adc4`](https://github.com/nodejs/node/commit/9a4be4adc4)] - **(SEMVER-MINOR)** **http2**: get trailers working with the compat api (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`3e5b07a8fb`](https://github.com/nodejs/node/commit/3e5b07a8fb)] - **(SEMVER-MINOR)** **http2**: refactor trailers API (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`26e1f8e01c`](https://github.com/nodejs/node/commit/26e1f8e01c)] - **(SEMVER-MINOR)** **http2**: address initial pr feedback (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`7824fa0b40`](https://github.com/nodejs/node/commit/7824fa0b40)] - **(SEMVER-MINOR)** **http2**: make writeHead behave like HTTP/1. (Matteo Collina) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`b778838337`](https://github.com/nodejs/node/commit/b778838337)] - **(SEMVER-MINOR)** **http2**: doc and fixes to the Compatibility API (Matteo Collina) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`8f3bbd9b68`](https://github.com/nodejs/node/commit/8f3bbd9b68)] - **(SEMVER-MINOR)** **http2**: add range support for respondWith{File|FD} (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`61696f1215`](https://github.com/nodejs/node/commit/61696f1215)] - **(SEMVER-MINOR)** **http2**: fix socketOnTimeout and a segfault (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`2620769e7f`](https://github.com/nodejs/node/commit/2620769e7f)] - **(SEMVER-MINOR)** **http2**: refinement and test for socketError (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`cd0f4c6652`](https://github.com/nodejs/node/commit/cd0f4c6652)] - **(SEMVER-MINOR)** **http2**: fix abort when client.destroy inside end event (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`e8cc193bcc`](https://github.com/nodejs/node/commit/e8cc193bcc)] - **(SEMVER-MINOR)** **http2**: fix documentation nits (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`a49146e446`](https://github.com/nodejs/node/commit/a49146e446)] - **(SEMVER-MINOR)** **http2**: remove redundant return in test (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`3eb61b00de`](https://github.com/nodejs/node/commit/3eb61b00de)] - **(SEMVER-MINOR)** **http2**: add tests and benchmarks (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`9623ee0f99`](https://github.com/nodejs/node/commit/9623ee0f99)] - **(SEMVER-MINOR)** **http2**: introducing HTTP/2 (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`029567a460`](https://github.com/nodejs/node/commit/029567a460)] - **inspector**: support extra contexts (Eugene Ostroukhov) [#14465](https://github.com/nodejs/node/pull/14465) +* [[`d89f9f82b0`](https://github.com/nodejs/node/commit/d89f9f82b0)] - **(SEMVER-MINOR)** **inspector**: allow require in Runtime.evaluate (Jan Krems) [#8837](https://github.com/nodejs/node/pull/8837) +* [[`ac1b81ad75`](https://github.com/nodejs/node/commit/ac1b81ad75)] - **lib**: move deprecationWarned var (Daniel Bevenius) [#14769](https://github.com/nodejs/node/pull/14769) +* [[`8433b1ad37`](https://github.com/nodejs/node/commit/8433b1ad37)] - **lib**: use Timer.now() in readline module (Rich Trott) [#14681](https://github.com/nodejs/node/pull/14681) +* [[`917ace283f`](https://github.com/nodejs/node/commit/917ace283f)] - **(SEMVER-MINOR)** **n-api**: add napi_get_node_version (Anna Henningsen) [#14696](https://github.com/nodejs/node/pull/14696) +* [[`5e2cce59ef`](https://github.com/nodejs/node/commit/5e2cce59ef)] - **(SEMVER-MINOR)** **n-api**: optimize number API performance (Jason Ginchereau) [#14573](https://github.com/nodejs/node/pull/14573) +* [[`c94f346b93`](https://github.com/nodejs/node/commit/c94f346b93)] - **net**: use rest parameters instead of arguments (Tobias Nießen) [#13472](https://github.com/nodejs/node/pull/13472) +* [[`1c00875747`](https://github.com/nodejs/node/commit/1c00875747)] - **repl**: include folder extensions in autocomplete (Teddy Katz) [#14727](https://github.com/nodejs/node/pull/14727) +* [[`59d1d56da6`](https://github.com/nodejs/node/commit/59d1d56da6)] - **src**: remove unused http2_socket_buffer from env (Anna Henningsen) [#14740](https://github.com/nodejs/node/pull/14740) +* [[`268a1ff3f1`](https://github.com/nodejs/node/commit/268a1ff3f1)] - **src**: mention that node options are space-separated (Gabriel Schulhof) [#14709](https://github.com/nodejs/node/pull/14709) +* [[`9237ef868e`](https://github.com/nodejs/node/commit/9237ef868e)] - **src**: avoid creating local data variable (Daniel Bevenius) [#14732](https://github.com/nodejs/node/pull/14732) +* [[`f83827d64b`](https://github.com/nodejs/node/commit/f83827d64b)] - **src**: use local isolate instead of args.GetIsolate (Daniel Bevenius) [#14768](https://github.com/nodejs/node/pull/14768) +* [[`d7d22ead2b`](https://github.com/nodejs/node/commit/d7d22ead2b)] - **src**: add comments for cares library init refcount (Anna Henningsen) [#14743](https://github.com/nodejs/node/pull/14743) +* [[`b87fae927d`](https://github.com/nodejs/node/commit/b87fae927d)] - **src**: remove duplicate loop (Anna Henningsen) [#14750](https://github.com/nodejs/node/pull/14750) +* [[`033773c17b`](https://github.com/nodejs/node/commit/033773c17b)] - **src**: add overlooked handle to cleanup (Anna Henningsen) [#14749](https://github.com/nodejs/node/pull/14749) +* [[`dd6444d401`](https://github.com/nodejs/node/commit/dd6444d401)] - **src,http2**: DRY header/trailer handling code up (Anna Henningsen) [#14688](https://github.com/nodejs/node/pull/14688) +* [[`ef8ac7b5ac`](https://github.com/nodejs/node/commit/ef8ac7b5ac)] - **(SEMVER-MINOR)** **stream**: support readable/writableHWM for Duplex (Guy Margalit) [#14636](https://github.com/nodejs/node/pull/14636) +* [[`6d9f94f93f`](https://github.com/nodejs/node/commit/6d9f94f93f)] - **test**: cover all HTTP methods that parser supports (Oky Antoro) [#14773](https://github.com/nodejs/node/pull/14773) +* [[`e4854fccfc`](https://github.com/nodejs/node/commit/e4854fccfc)] - **test**: use regular expressions in throw assertions (Vincent Xue) [#14318](https://github.com/nodejs/node/pull/14318) +* [[`66788fc4d0`](https://github.com/nodejs/node/commit/66788fc4d0)] - **test**: increase http2 coverage (Michael Albert) [#14701](https://github.com/nodejs/node/pull/14701) +* [[`dbb9c370d4`](https://github.com/nodejs/node/commit/dbb9c370d4)] - **test**: add crypto check to http2 tests (Daniel Bevenius) [#14657](https://github.com/nodejs/node/pull/14657) +* [[`97f622b99e`](https://github.com/nodejs/node/commit/97f622b99e)] - **(SEMVER-MINOR)** **test**: fix flaky test-http2-client-unescaped-path on osx (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`9d752d5282`](https://github.com/nodejs/node/commit/9d752d5282)] - **(SEMVER-MINOR)** **test**: fix flakiness in test-http2-client-upload (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`82c63a55ea`](https://github.com/nodejs/node/commit/82c63a55ea)] - **test**: add test-benchmark-arrays (Rich Trott) [#14728](https://github.com/nodejs/node/pull/14728) +* [[`0eab77c86f`](https://github.com/nodejs/node/commit/0eab77c86f)] - **test**: allow inspector to reopen with same port (Gibson Fahnestock) [#14320](https://github.com/nodejs/node/pull/14320) +* [[`9bbbf12827`](https://github.com/nodejs/node/commit/9bbbf12827)] - **test**: remove redundant `using` in cctest (XadillaX) [#14739](https://github.com/nodejs/node/pull/14739) +* [[`7eb9f6f6e4`](https://github.com/nodejs/node/commit/7eb9f6f6e4)] - **test**: make totalLen snake case (Daniel Bevenius) [#14765](https://github.com/nodejs/node/pull/14765) +* [[`977e22857a`](https://github.com/nodejs/node/commit/977e22857a)] - **test**: make test-tls-connect checks more strict (Rich Trott) [#14695](https://github.com/nodejs/node/pull/14695) +* [[`a781bb4508`](https://github.com/nodejs/node/commit/a781bb4508)] - ***Revert*** "**test**: disable MultipleEnvironmentsPerIsolate" (Anna Henningsen) [#14749](https://github.com/nodejs/node/pull/14749) +* [[`8ff2a5c338`](https://github.com/nodejs/node/commit/8ff2a5c338)] - ***Revert*** "**test**: add DISABLED_ prefix to commented out test" (Anna Henningsen) [#14749](https://github.com/nodejs/node/pull/14749) +* [[`0bc3124c80`](https://github.com/nodejs/node/commit/0bc3124c80)] - **test**: properly order freeing resources in cctest (Anna Henningsen) [#14749](https://github.com/nodejs/node/pull/14749) +* [[`3f1bb0a551`](https://github.com/nodejs/node/commit/3f1bb0a551)] - **test**: split out load-sensitive readline tests (Rich Trott) [#14681](https://github.com/nodejs/node/pull/14681) +* [[`5d99d7dff2`](https://github.com/nodejs/node/commit/5d99d7dff2)] - **test**: add block scoping to test-readline-interface (Rich Trott) [#14615](https://github.com/nodejs/node/pull/14615) +* [[`58742729da`](https://github.com/nodejs/node/commit/58742729da)] - **test**: set module loading error for aix (Prakash Palaniappan) [#14511](https://github.com/nodejs/node/pull/14511) +* [[`06ba2dae30`](https://github.com/nodejs/node/commit/06ba2dae30)] - **test**: fix conversion of microseconds in test (Nick Stanish) [#14706](https://github.com/nodejs/node/pull/14706) +* [[`30837b3b90`](https://github.com/nodejs/node/commit/30837b3b90)] - **test**: improve check in test-os (Rich Trott) [#14655](https://github.com/nodejs/node/pull/14655) +* [[`55aba6aee7`](https://github.com/nodejs/node/commit/55aba6aee7)] - **test**: replace indexOf with includes (Miguel Angel Asencio Hurtado) [#14630](https://github.com/nodejs/node/pull/14630) +* [[`935d34bd6b`](https://github.com/nodejs/node/commit/935d34bd6b)] - **test**: fix test-readline-interface (Azard) [#14677](https://github.com/nodejs/node/pull/14677) +* [[`2ee3320f2c`](https://github.com/nodejs/node/commit/2ee3320f2c)] - **test**: improve multiple timers tests (James M Snell) [#14616](https://github.com/nodejs/node/pull/14616) +* [[`71f2e76353`](https://github.com/nodejs/node/commit/71f2e76353)] - **test**: use ciphers supported by shared OpenSSL (Jérémy Lal) [#14566](https://github.com/nodejs/node/pull/14566) +* [[`f73f659186`](https://github.com/nodejs/node/commit/f73f659186)] - **test**: mitigate RegEx exceeding 80 chars (Aditya Anand) [#14607](https://github.com/nodejs/node/pull/14607) +* [[`96147c980c`](https://github.com/nodejs/node/commit/96147c980c)] - **test**: read proper inspector message size (Bartosz Sosnowski) [#14596](https://github.com/nodejs/node/pull/14596) +* [[`e84c9d7176`](https://github.com/nodejs/node/commit/e84c9d7176)] - **(SEMVER-MINOR)** **tls**: add tlsSocket.disableRenegotiation() (James M Snell) [#14239](https://github.com/nodejs/node/pull/14239) +* [[`a0e05e884e`](https://github.com/nodejs/node/commit/a0e05e884e)] - **tools**: fix tools/addon-verify.js (Daniel Bevenius) [#14048](https://github.com/nodejs/node/pull/14048) +* [[`116841056a`](https://github.com/nodejs/node/commit/116841056a)] - **util**: improve util.inspect performance (Ruben Bridgewater) [#14492](https://github.com/nodejs/node/pull/14492) +* [[`7203924fea`](https://github.com/nodejs/node/commit/7203924fea)] - **(SEMVER-MINOR)** **util**: implement %o and %O as formatting specifiers (Greg Alexander) [#14558](https://github.com/nodejs/node/pull/14558) + ## 2017-08-09, Version 8.3.0 (Current), @addaleax diff --git a/src/node_version.h b/src/node_version.h index 7dc100d800f17f..8683a9820af2cf 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -23,10 +23,10 @@ #define SRC_NODE_VERSION_H_ #define NODE_MAJOR_VERSION 8 -#define NODE_MINOR_VERSION 3 -#define NODE_PATCH_VERSION 1 +#define NODE_MINOR_VERSION 4 +#define NODE_PATCH_VERSION 0 -#define NODE_VERSION_IS_RELEASE 0 +#define NODE_VERSION_IS_RELEASE 1 #ifndef NODE_STRINGIFY #define NODE_STRINGIFY(n) NODE_STRINGIFY_HELPER(n)