From 3eb61b00de97e83414d91af3db0a857baf3a5733 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Jul 2017 10:29:42 -0700 Subject: [PATCH] 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