diff options
33 files changed, 350 insertions, 72 deletions
diff --git a/doc/api/http2.md b/doc/api/http2.md index 6bc1543c07..cc7f3ea3b5 100755 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -998,13 +998,17 @@ server.on('stream', (stream) => { }); ``` -#### http2stream.respondWithFD(fd[, headers]) +#### http2stream.respondWithFD(fd[, headers[, options]]) <!-- YAML added: REPLACEME --> * `fd` {number} A readable file descriptor * `headers` {[Headers Object][]} +* `options` {Object} + * `statCheck` {Function} + * `offset` {number} The offset position at which to begin reading + * `length` {number} The amount of data from the fd to send Initiates a response whose data is read from the given file descriptor. No validation is performed on the given file descriptor. If an error occurs while @@ -1034,6 +1038,16 @@ server.on('stream', (stream) => { server.on('close', () => fs.closeSync(fd)); ``` +The optional `options.statCheck` function may be specified to give user code +an opportunity to set additional content headers based on the `fs.Stat` details +of the given fd. If the `statCheck` function is provided, the +`http2stream.respondWithFD()` method will perform an `fs.fstat()` call to +collect details on the provided file descriptor. + +The `offset` and `length` options may be used to limit the response to a +specific range subset. This can be used, for instance, to support HTTP Range +requests. + #### http2stream.respondWithFile(path[, headers[, options]]) <!-- YAML added: REPLACEME @@ -1043,6 +1057,8 @@ added: REPLACEME * `headers` {[Headers Object][]} * `options` {Object} * `statCheck` {Function} + * `offset` {number} The offset position at which to begin reading + * `length` {number} The amount of data from the fd to send Sends a regular file as the response. The `path` must specify a regular file or an `'error'` event will be emitted on the `Http2Stream` object. @@ -1096,6 +1112,10 @@ server.on('stream', (stream) => { The `content-length` header field will be automatically set. +The `offset` and `length` options may be used to limit the response to a +specific range subset. This can be used, for instance, to support HTTP Range +requests. + ### Class: Http2Server <!-- YAML added: REPLACEME diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index cd9a1fa2b7..c258079356 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -452,7 +452,7 @@ class Http2ServerResponse extends Stream { stream.once('finish', cb); } - this[kBeginSend]({endStream: true}); + this[kBeginSend]({ endStream: true }); if (stream !== undefined) { stream.end(); diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 38cd316029..dc3033f2ed 100755 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1541,7 +1541,7 @@ function processHeaders(headers) { return headers; } -function processRespondWithFD(fd, headers) { +function processRespondWithFD(fd, headers, offset = 0, length = -1) { const session = this[kSession]; const state = this[kState]; state.headersSent = true; @@ -1551,7 +1551,7 @@ function processRespondWithFD(fd, headers) { const handle = session[kHandle]; const ret = - handle.submitFile(this[kID], fd, headers); + handle.submitFile(this[kID], fd, headers, offset, length); let err; switch (ret) { case NGHTTP2_ERR_NOMEM: @@ -1575,26 +1575,71 @@ function doSendFD(session, options, fd, headers, err, stat) { process.nextTick(() => this.emit('error', err)); return; } + + const statOptions = { + offset: options.offset !== undefined ? options.offset : 0, + length: options.length !== undefined ? options.length : -1 + }; + + if (typeof options.statCheck === 'function' && + options.statCheck.call(this, stat, headers, statOptions) === false) { + return; + } + + const headersList = mapToHeaders(headers, + assertValidPseudoHeaderResponse); + if (!Array.isArray(headersList)) { + process.nextTick(() => this.emit('error', headersList)); + } + + processRespondWithFD.call(this, fd, headersList, + statOptions.offset, + statOptions.length); +} + +function doSendFileFD(session, options, fd, headers, err, stat) { + if (this.destroyed || session.destroyed) { + abort(this); + return; + } + if (err) { + process.nextTick(() => this.emit('error', err)); + return; + } if (!stat.isFile()) { err = new errors.Error('ERR_HTTP2_SEND_FILE'); process.nextTick(() => this.emit('error', err)); return; } + const statOptions = { + offset: options.offset !== undefined ? options.offset : 0, + length: options.length !== undefined ? options.length : -1 + }; + // Set the content-length by default - headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size; if (typeof options.statCheck === 'function' && options.statCheck.call(this, stat, headers) === false) { return; } + statOptions.length = + statOptions.length < 0 ? stat.size - (+statOptions.offset) : + Math.min(stat.size - (+statOptions.offset), + statOptions.length); + + if (headers[HTTP2_HEADER_CONTENT_LENGTH] === undefined) + headers[HTTP2_HEADER_CONTENT_LENGTH] = statOptions.length; + const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse); if (!Array.isArray(headersList)) { - throw headersList; + process.nextTick(() => this.emit('error', headersList)); } - processRespondWithFD.call(this, fd, headersList); + processRespondWithFD.call(this, fd, headersList, + options.offset, + options.length); } function afterOpen(session, options, headers, err, fd) { @@ -1609,7 +1654,7 @@ function afterOpen(session, options, headers, err, fd) { } state.fd = fd; - fs.fstat(fd, doSendFD.bind(this, session, options, fd, headers)); + fs.fstat(fd, doSendFileFD.bind(this, session, options, fd, headers)); } @@ -1786,12 +1831,12 @@ class ServerHttp2Stream extends Http2Stream { } // Initiate a response using an open FD. Note that there are fewer - // protections with this approach. For one, the fd is not validated. - // In respondWithFile, the file is checked to make sure it is a + // protections with this approach. For one, the fd is not validated by + // default. In respondWithFile, the file is checked to make sure it is a // regular file, here the fd is passed directly. If the underlying // mechanism is not able to read from the fd, then the stream will be // reset with an error code. - respondWithFD(fd, headers) { + respondWithFD(fd, headers, options) { const session = this[kSession]; if (this.destroyed) throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); @@ -1803,6 +1848,26 @@ class ServerHttp2Stream extends Http2Stream { if (state.headersSent) throw new errors.Error('ERR_HTTP2_HEADERS_SENT'); + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + + if (options.offset !== undefined && typeof options.offset !== 'number') + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'offset', + options.offset); + + if (options.length !== undefined && typeof options.length !== 'number') + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'length', + options.length); + + if (options.statCheck !== undefined && + typeof options.statCheck !== 'function') { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'statCheck', + options.statCheck); + } + if (typeof fd !== 'number') throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'fd', 'number'); @@ -1816,13 +1881,20 @@ class ServerHttp2Stream extends Http2Stream { throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode); } + if (options.statCheck !== undefined) { + fs.fstat(fd, doSendFD.bind(this, session, options, fd, headers)); + return; + } + const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse); if (!Array.isArray(headersList)) { - throw headersList; + process.nextTick(() => this.emit('error', headersList)); } - processRespondWithFD.call(this, fd, headersList); + processRespondWithFD.call(this, fd, headersList, + options.offset, + options.length); } // Initiate a file response on this Http2Stream. The path is passed to @@ -1847,6 +1919,16 @@ class ServerHttp2Stream extends Http2Stream { assertIsObject(options, 'options'); options = Object.assign(Object.create(null), options); + if (options.offset !== undefined && typeof options.offset !== 'number') + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'offset', + options.offset); + + if (options.length !== undefined && typeof options.length !== 'number') + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'length', + options.length); + if (options.statCheck !== undefined && typeof options.statCheck !== 'function') { throw new errors.TypeError('ERR_INVALID_OPT_VALUE', diff --git a/src/node_http2.cc b/src/node_http2.cc index 61a98758b1..ee12072939 100755 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -604,7 +604,9 @@ void Http2Session::SubmitResponse(const FunctionCallbackInfo<Value>& args) { void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) { CHECK(args[0]->IsNumber()); // Stream ID CHECK(args[1]->IsNumber()); // File Descriptor - CHECK(args[2]->IsArray()); // Headers + CHECK(args[2]->IsArray()); // Headers + CHECK(args[3]->IsNumber()); // Offset + CHECK(args[4]->IsNumber()); // Length Http2Session* session; Nghttp2Stream* stream; @@ -618,6 +620,11 @@ void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) { int fd = args[1]->Int32Value(context).ToChecked(); Local<Array> headers = args[2].As<Array>(); + int64_t offset = args[3]->IntegerValue(context).ToChecked(); + int64_t length = args[4]->IntegerValue(context).ToChecked(); + + CHECK_GE(offset, 0); + DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, " "end-stream: %d\n", fd, id, headers->Length()); @@ -627,7 +634,8 @@ void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) { Headers list(isolate, context, headers); - args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length())); + args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length(), + offset, length)); } void Http2Session::SendHeaders(const FunctionCallbackInfo<Value>& args) { diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h index c79d412e63..f20bdc3749 100755 --- a/src/node_http2_core-inl.h +++ b/src/node_http2_core-inl.h @@ -429,7 +429,10 @@ inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva, } // Initiate a response that contains data read from a file descriptor. -inline int Nghttp2Stream::SubmitFile(int fd, nghttp2_nv* nva, size_t len) { +inline int Nghttp2Stream::SubmitFile(int fd, + nghttp2_nv* nva, size_t len, + int64_t offset, + int64_t length) { CHECK_GT(len, 0); CHECK_GT(fd, 0); DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_); @@ -438,6 +441,9 @@ inline int Nghttp2Stream::SubmitFile(int fd, nghttp2_nv* nva, size_t len) { prov.source.fd = fd; prov.read_callback = Nghttp2Session::OnStreamReadFD; + if (offset > 0) fd_offset_ = offset; + if (length > -1) fd_length_ = length; + return nghttp2_submit_response(session_->session(), id_, nva, len, &prov); } diff --git a/src/node_http2_core.cc b/src/node_http2_core.cc index 4d9ab4a4df..3273c8aaa2 100644 --- a/src/node_http2_core.cc +++ b/src/node_http2_core.cc @@ -180,18 +180,25 @@ ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session, int fd = source->fd; int64_t offset = stream->fd_offset_; - ssize_t numchars; + ssize_t numchars = 0; + + if (stream->fd_length_ >= 0 && + stream->fd_length_ < static_cast<int64_t>(length)) + length = stream->fd_length_; uv_buf_t data; data.base = reinterpret_cast<char*>(buf); data.len = length; uv_fs_t read_req; - numchars = uv_fs_read(handle->loop_, - &read_req, - fd, &data, 1, - offset, nullptr); - uv_fs_req_cleanup(&read_req); + + if (length > 0) { + numchars = uv_fs_read(handle->loop_, + &read_req, + fd, &data, 1, + offset, nullptr); + uv_fs_req_cleanup(&read_req); + } // Close the stream with an error if reading fails if (numchars < 0) @@ -199,9 +206,10 @@ ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session, // Update the read offset for the next read stream->fd_offset_ += numchars; + stream->fd_length_ -= numchars; // if numchars < length, assume that we are done. - if (static_cast<size_t>(numchars) < length) { + if (static_cast<size_t>(numchars) < length || length <= 0) { DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n", handle->session_type_, id); *flags |= NGHTTP2_DATA_FLAG_EOF; diff --git a/src/node_http2_core.h b/src/node_http2_core.h index 4c3fc5549c..80dd98e9ff 100755 --- a/src/node_http2_core.h +++ b/src/node_http2_core.h @@ -310,7 +310,10 @@ class Nghttp2Stream { bool emptyPayload = false); // Send data read from a file descriptor as the response on this stream. - inline int SubmitFile(int fd, nghttp2_nv* nva, size_t len); + inline int SubmitFile(int fd, + nghttp2_nv* nva, size_t len, + int64_t offset, + int64_t length); // Submit informational headers for this stream inline int SubmitInfo(nghttp2_nv* nva, size_t len); @@ -420,7 +423,8 @@ class Nghttp2Stream { nghttp2_stream_write_queue* queue_tail_ = nullptr; unsigned int queue_head_index_ = 0; size_t queue_head_offset_ = 0; - size_t fd_offset_ = 0; + int64_t fd_offset_ = 0; + int64_t fd_length_ = -1; // The Current Headers block... As headers are received for this stream, // they are temporarily stored here until the OnFrameReceived is called diff --git a/test/parallel/test-http2-client-settings-before-connect.js b/test/parallel/test-http2-client-settings-before-connect.js index 9391502479..f320aa97db 100644 --- a/test/parallel/test-http2-client-settings-before-connect.js +++ b/test/parallel/test-http2-client-settings-before-connect.js @@ -24,28 +24,28 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - assert.throws(() => client.settings({headerTableSize: -1}), + assert.throws(() => client.settings({ headerTableSize: -1 }), RangeError); - assert.throws(() => client.settings({headerTableSize: 2 ** 32}), + assert.throws(() => client.settings({ headerTableSize: 2 ** 32 }), RangeError); - assert.throws(() => client.settings({initialWindowSize: -1}), + assert.throws(() => client.settings({ initialWindowSize: -1 }), RangeError); - assert.throws(() => client.settings({initialWindowSize: 2 ** 32}), + assert.throws(() => client.settings({ initialWindowSize: 2 ** 32 }), RangeError); - assert.throws(() => client.settings({maxFrameSize: 1}), + assert.throws(() => client.settings({ maxFrameSize: 1 }), RangeError); - assert.throws(() => client.settings({maxFrameSize: 2 ** 24}), + assert.throws(() => client.settings({ maxFrameSize: 2 ** 24 }), RangeError); - assert.throws(() => client.settings({maxConcurrentStreams: -1}), + assert.throws(() => client.settings({ maxConcurrentStreams: -1 }), RangeError); - assert.throws(() => client.settings({maxConcurrentStreams: 2 ** 31}), + assert.throws(() => client.settings({ maxConcurrentStreams: 2 ** 31 }), RangeError); - assert.throws(() => client.settings({maxHeaderListSize: -1}), + assert.throws(() => client.settings({ maxHeaderListSize: -1 }), RangeError); - assert.throws(() => client.settings({maxHeaderListSize: 2 ** 32}), + assert.throws(() => client.settings({ maxHeaderListSize: 2 ** 32 }), RangeError); ['a', 1, 0, null, {}].forEach((i) => { - assert.throws(() => client.settings({enablePush: i}), TypeError); + assert.throws(() => client.settings({ enablePush: i }), TypeError); }); client.settings({ maxFrameSize: 1234567 }); diff --git a/test/parallel/test-http2-client-shutdown-before-connect.js b/test/parallel/test-http2-client-shutdown-before-connect.js index 203963bf57..eabfc6261c 100644 --- a/test/parallel/test-http2-client-shutdown-before-connect.js +++ b/test/parallel/test-http2-client-shutdown-before-connect.js @@ -15,7 +15,7 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - client.shutdown({graceful: true}, common.mustCall(() => { + client.shutdown({ graceful: true }, common.mustCall(() => { server.close(); client.destroy(); })); diff --git a/test/parallel/test-http2-compat-serverresponse-flushheaders.js b/test/parallel/test-http2-compat-serverresponse-flushheaders.js index 4bfe490912..932f79b7c2 100644 --- a/test/parallel/test-http2-compat-serverresponse-flushheaders.js +++ b/test/parallel/test-http2-compat-serverresponse-flushheaders.js @@ -13,7 +13,7 @@ server.listen(0, common.mustCall(function() { server.once('request', common.mustCall(function(request, response) { response.flushHeaders(); response.flushHeaders(); // Idempotent - response.writeHead(400, {'foo-bar': 'abc123'}); // Ignored + response.writeHead(400, { 'foo-bar': 'abc123' }); // Ignored response.on('finish', common.mustCall(function() { server.close(); diff --git a/test/parallel/test-http2-compat-serverresponse-headers.js b/test/parallel/test-http2-compat-serverresponse-headers.js index 28bd36ce2e..8e11babddf 100644 --- a/test/parallel/test-http2-compat-serverresponse-headers.js +++ b/test/parallel/test-http2-compat-serverresponse-headers.js @@ -53,7 +53,7 @@ server.listen(0, common.mustCall(function() { response.setHeader(real, expectedValue); const expectedHeaderNames = [real]; assert.deepStrictEqual(response.getHeaderNames(), expectedHeaderNames); - const expectedHeaders = {[real]: expectedValue}; + const expectedHeaders = { [real]: expectedValue }; assert.deepStrictEqual(response.getHeaders(), expectedHeaders); response.getHeaders()[fake] = fake; diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage.js b/test/parallel/test-http2-compat-serverresponse-statusmessage.js index 08822c9939..c9c5b5a013 100644 --- a/test/parallel/test-http2-compat-serverresponse-statusmessage.js +++ b/test/parallel/test-http2-compat-serverresponse-statusmessage.js @@ -8,7 +8,7 @@ const h2 = require('http2'); // Http2ServerResponse.writeHead should accept an optional status message const unsupportedWarned = common.mustCall(1); -process.on('warning', ({name, message}) => { +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) @@ -21,7 +21,7 @@ server.listen(0, common.mustCall(function() { server.once('request', common.mustCall(function(request, response) { const statusCode = 200; const statusMessage = 'OK'; - const headers = {'foo-bar': 'abc123'}; + const headers = { 'foo-bar': 'abc123' }; response.writeHead(statusCode, statusMessage, headers); response.on('finish', common.mustCall(function() { diff --git a/test/parallel/test-http2-compat-serverresponse-writehead.js b/test/parallel/test-http2-compat-serverresponse-writehead.js index b4c531d339..721b84ca3e 100644 --- a/test/parallel/test-http2-compat-serverresponse-writehead.js +++ b/test/parallel/test-http2-compat-serverresponse-writehead.js @@ -13,7 +13,7 @@ server.listen(0, common.mustCall(function() { server.once('request', common.mustCall(function(request, response) { response.setHeader('foo-bar', 'def456'); response.writeHead(500); - response.writeHead(418, {'foo-bar': 'abc123'}); // Override + response.writeHead(418, { 'foo-bar': 'abc123' }); // Override response.on('finish', common.mustCall(function() { assert.doesNotThrow(() => { response.writeHead(300); }); diff --git a/test/parallel/test-http2-create-client-connect.js b/test/parallel/test-http2-create-client-connect.js index 8173dc3d08..376b0a208d 100644 --- a/test/parallel/test-http2-create-client-connect.js +++ b/test/parallel/test-http2-create-client-connect.js @@ -21,8 +21,8 @@ const URL = url.URL; [`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:'}] + [{ port: port }, { protocol: 'http:' }], + [{ port: port, hostname: '127.0.0.1' }, { protocol: 'http:' }] ]; let count = items.length; @@ -41,7 +41,7 @@ const URL = url.URL; }); // Will fail because protocol does not match the server. - h2.connect({port: port, protocol: 'https:'}) + h2.connect({ port: port, protocol: 'https:' }) .on('socketError', common.mustCall()); })); } @@ -60,14 +60,14 @@ const URL = url.URL; server.on('listening', common.mustCall(function() { const port = this.address().port; - const opts = {rejectUnauthorized: false}; + 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] + [{ port: port, protocol: 'https:' }, opts], + [{ port: port, hostname: '127.0.0.1', protocol: 'https:' }, opts] ]; let count = items.length; diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js index 9b1cf4a0c9..a775ebd71e 100644 --- a/test/parallel/test-http2-create-client-secure-session.js +++ b/test/parallel/test-http2-create-client-secure-session.js @@ -27,7 +27,7 @@ function onStream(stream, headers) { } function verifySecureSession(key, cert, ca, opts) { - const server = h2.createSecureServer({cert, key}); + const server = h2.createSecureServer({ cert, key }); server.on('stream', common.mustCall(onStream)); server.listen(0); server.on('listening', common.mustCall(function() { @@ -35,7 +35,7 @@ function verifySecureSession(key, cert, ca, opts) { if (!opts) { opts = {}; } - opts.secureContext = tls.createSecureContext({ca}); + opts.secureContext = tls.createSecureContext({ ca }); const client = h2.connect(`https://localhost:${this.address().port}`, opts, function() { const req = client.request(headers); @@ -72,4 +72,4 @@ verifySecureSession( loadKey('agent1-key.pem'), loadKey('agent1-cert.pem'), loadKey('ca1-cert.pem'), - {servername: 'agent1'}); + { servername: 'agent1' }); diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js index 0c1a1bccee..0e3744fc27 100644 --- a/test/parallel/test-http2-getpackedsettings.js +++ b/test/parallel/test-http2-getpackedsettings.js @@ -122,7 +122,7 @@ assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false })); const packed = Buffer.from([0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF]); assert.throws(() => { - http2.getUnpackedSettings(packed, {validate: true}); + http2.getUnpackedSettings(packed, { validate: true }); }, common.expectsError({ code: 'ERR_HTTP2_INVALID_SETTING_VALUE', type: RangeError, diff --git a/test/parallel/test-http2-info-headers.js b/test/parallel/test-http2-info-headers.js index c5d93d514f..cb91b560cd 100755 --- a/test/parallel/test-http2-info-headers.js +++ b/test/parallel/test-http2-info-headers.js @@ -56,7 +56,7 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request({ ':path': '/'}); + const req = client.request({ ':path': '/' }); // The additionalHeaders method does not exist on client stream assert.strictEqual(req.additionalHeaders, undefined); diff --git a/test/parallel/test-http2-max-concurrent-streams.js b/test/parallel/test-http2-max-concurrent-streams.js index 6725a7c754..9b582eb68d 100644 --- a/test/parallel/test-http2-max-concurrent-streams.js +++ b/test/parallel/test-http2-max-concurrent-streams.js @@ -13,7 +13,7 @@ const { } = h2.constants; // Only allow one stream to be open at a time -const server = h2.createServer({ settings: { maxConcurrentStreams: 1 }}); +const server = h2.createServer({ settings: { maxConcurrentStreams: 1 } }); // The stream handler must be called only once server.on('stream', common.mustCall((stream) => { diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js index e356169d26..94475519cb 100644 --- a/test/parallel/test-http2-misused-pseudoheaders.js +++ b/test/parallel/test-http2-misused-pseudoheaders.js @@ -18,7 +18,7 @@ function onStream(stream, headers, flags) { ':method', ':scheme' ].forEach((i) => { - assert.throws(() => stream.respond({[i]: '/'}), + assert.throws(() => stream.respond({ [i]: '/' }), common.expectsError({ code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' })); diff --git a/test/parallel/test-http2-multi-content-length.js b/test/parallel/test-http2-multi-content-length.js index 5dcd56990b..9ab1d48c21 100644 --- a/test/parallel/test-http2-multi-content-length.js +++ b/test/parallel/test-http2-multi-content-length.js @@ -47,7 +47,7 @@ server.listen(0, common.mustCall(() => { // 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}); + const req3 = client.request({ 'content-length': 1 }); req3.resume(); req3.on('end', common.mustCall(maybeClose)); req3.on('error', common.expectsError({ diff --git a/test/parallel/test-http2-multiplex.js b/test/parallel/test-http2-multiplex.js index b6b81c73a6..7467b0fa60 100644 --- a/test/parallel/test-http2-multiplex.js +++ b/test/parallel/test-http2-multiplex.js @@ -30,7 +30,7 @@ server.listen(0, common.mustCall(() => { } function doRequest() { - const req = client.request({ ':method': 'POST '}); + const req = client.request({ ':method': 'POST ' }); let data = ''; req.setEncoding('utf8'); diff --git a/test/parallel/test-http2-priority-event.js b/test/parallel/test-http2-priority-event.js index bbb248265e..505f0f1e9b 100644 --- a/test/parallel/test-http2-priority-event.js +++ b/test/parallel/test-http2-priority-event.js @@ -37,7 +37,7 @@ 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': '/'}); + const req = client.request({ ':path': '/' }); client.on('connect', () => { req.priority({ diff --git a/test/parallel/test-http2-respond-file-fd-range.js b/test/parallel/test-http2-respond-file-fd-range.js new file mode 100644 index 0000000000..083dd45615 --- /dev/null +++ b/test/parallel/test-http2-respond-file-fd-range.js @@ -0,0 +1,94 @@ +// Flags: --expose-http2 +'use strict'; + +// Tests the ability to minimally request a byte range with respondWithFD + +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, 'printA.js'); +const data = fs.readFileSync(fname); +const fd = fs.openSync(fname, 'r'); + +// Note: this is not anywhere close to a proper implementation of the range +// header. +function getOffsetLength(range) { + if (range === undefined) + return [0, -1]; + const r = /bytes=(\d+)-(\d+)/.exec(range); + return [+r[1], +r[2] - +r[1]]; +} + +const server = http2.createServer(); +server.on('stream', (stream, headers) => { + + const [ offset, length ] = getOffsetLength(headers.range); + + stream.respondWithFD(fd, { + [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain' + }, { + statCheck: common.mustCall((stat, headers, options) => { + assert.strictEqual(options.length, length); + assert.strictEqual(options.offset, offset); + headers[HTTP2_HEADER_CONTENT_LENGTH] = + Math.min(options.length, stat.size - offset); + }), + offset: offset, + length: length + }); +}); +server.on('close', common.mustCall(() => fs.closeSync(fd))); +server.listen(0, () => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + let remaining = 2; + function maybeClose() { + if (--remaining === 0) { + client.destroy(); + server.close(); + } + } + + { + const req = client.request({ range: 'bytes=8-11' }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], 3); + })); + req.setEncoding('utf8'); + let check = ''; + req.on('data', (chunk) => check += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8', 8, 11)); + })); + req.on('streamClosed', common.mustCall(maybeClose)); + req.end(); + } + + { + const req = client.request({ range: 'bytes=8-28' }); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain'); + assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], 9); + })); + req.setEncoding('utf8'); + let check = ''; + req.on('data', (chunk) => check += chunk); + req.on('end', common.mustCall(() => { + assert.strictEqual(check, data.toString('utf8', 8, 28)); + })); + req.on('streamClosed', common.mustCall(maybeClose)); + req.end(); + } + +}); diff --git a/test/parallel/test-http2-respond-file-range.js b/test/parallel/test-http2-respond-file-range.js new file mode 100644 index 0000000000..40c5526953 --- /dev/null +++ b/test/parallel/test-http2-respond-file-range.js @@ -0,0 +1,52 @@ +// 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, 'printA.js'); +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: common.mustCall((stat, headers) => { + headers[HTTP2_HEADER_LAST_MODIFIED] = stat.mtime.toUTCString(); + }), + offset: 8, + length: 3 + }); +}); +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], 3); + 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', 8, 11)); + client.destroy(); + server.close(); + })); + req.end(); +}); diff --git a/test/parallel/test-http2-serve-file.js b/test/parallel/test-http2-serve-file.js index 1092703274..6c88a42863 100644 --- a/test/parallel/test-http2-serve-file.js +++ b/test/parallel/test-http2-serve-file.js @@ -25,7 +25,7 @@ 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}); +const server = http2.createSecureServer({ key, cert }); server.on('stream', (stream, headers) => { const name = headers[HTTP2_HEADER_PATH].slice(1); @@ -44,7 +44,7 @@ server.on('stream', (stream, headers) => { server.listen(8000, () => { - const secureContext = tls.createSecureContext({ca}); + const secureContext = tls.createSecureContext({ ca }); const client = http2.connect(`https://localhost:${server.address().port}`, { secureContext }); diff --git a/test/parallel/test-http2-server-push-disabled.js b/test/parallel/test-http2-server-push-disabled.js index 1fedf22293..33f8ed7c65 100644 --- a/test/parallel/test-http2-server-push-disabled.js +++ b/test/parallel/test-http2-server-push-disabled.js @@ -36,7 +36,7 @@ server.on('stream', common.mustCall((stream) => { })); server.listen(0, common.mustCall(() => { - const options = {settings: { enablePush: false }}; + const options = { settings: { enablePush: false } }; const client = http2.connect(`http://localhost:${server.address().port}`, options); const req = client.request({ ':path': '/' }); diff --git a/test/parallel/test-http2-server-shutdown-before-respond.js b/test/parallel/test-http2-server-shutdown-before-respond.js index c7ffee7e31..ccdbbf5d01 100644 --- a/test/parallel/test-http2-server-shutdown-before-respond.js +++ b/test/parallel/test-http2-server-shutdown-before-respond.js @@ -11,7 +11,7 @@ server.on('stream', common.mustCall(onStream)); function onStream(stream, headers, flags) { const session = stream.session; - stream.session.shutdown({graceful: true}, common.mustCall(() => { + stream.session.shutdown({ graceful: true }, common.mustCall(() => { session.destroy(); })); stream.respond({}); diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js index bc9877e23e..1d358f2c96 100644 --- a/test/parallel/test-http2-session-settings.js +++ b/test/parallel/test-http2-session-settings.js @@ -90,7 +90,7 @@ server.on('listening', common.mustCall(() => { })); }); [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => { - assert.throws(() => client.settings({enablePush: i}), + assert.throws(() => client.settings({ enablePush: i }), common.expectsError({ code: 'ERR_HTTP2_INVALID_SETTING_VALUE', type: TypeError diff --git a/test/parallel/test-http2-timeouts.js b/test/parallel/test-http2-timeouts.js index 132496e1fc..093a1bc3d1 100644 --- a/test/parallel/test-http2-timeouts.js +++ b/test/parallel/test-http2-timeouts.js @@ -9,7 +9,7 @@ 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.respond({ ':status': 200 }); stream.end('hello world'); })); })); diff --git a/test/parallel/test-http2-trailers.js b/test/parallel/test-http2-trailers.js index 35a5dbf28a..04ad3b0ff9 100644 --- a/test/parallel/test-http2-trailers.js +++ b/test/parallel/test-http2-trailers.js @@ -29,7 +29,7 @@ server.listen(0); server.on('listening', common.mustCall(function() { const client = h2.connect(`http://localhost:${this.address().port}`); - const req = client.request({':path': '/'}); + const req = client.request({ ':path': '/' }); req.on('data', common.mustCall()); req.on('trailers', common.mustCall((headers) => { assert.strictEqual(headers[trailerKey], trailerValue); diff --git a/test/parallel/test-http2-util-headers-list.js b/test/parallel/test-http2-util-headers-list.js index d19c78a2b3..c895a4f7a0 100644 --- a/test/parallel/test-http2-util-headers-list.js +++ b/test/parallel/test-http2-util-headers-list.js @@ -191,7 +191,7 @@ const { common.expectsError({ code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', message: msg - })(mapToHeaders({[name]: [1, 2, 3]})); + })(mapToHeaders({ [name]: [1, 2, 3] })); }); [ @@ -217,7 +217,7 @@ const { HTTP2_HEADER_VIA, HTTP2_HEADER_WWW_AUTHENTICATE ].forEach((name) => { - assert(!(mapToHeaders({[name]: [1, 2, 3]}) instanceof Error), name); + assert(!(mapToHeaders({ [name]: [1, 2, 3] }) instanceof Error), name); }); const regex = @@ -242,7 +242,7 @@ const regex = common.expectsError({ code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS', message: regex - })(mapToHeaders({[name]: 'abc'})); + })(mapToHeaders({ [name]: 'abc' })); }); assert(!(mapToHeaders({ te: 'trailers' }) instanceof Error)); diff --git a/test/parallel/test-http2-write-empty-string.js b/test/parallel/test-http2-write-empty-string.js index 74675fd67d..661777e39f 100644 --- a/test/parallel/test-http2-write-empty-string.js +++ b/test/parallel/test-http2-write-empty-string.js @@ -6,7 +6,7 @@ const assert = require('assert'); const http2 = require('http2'); const server = http2.createServer(function(request, response) { - response.writeHead(200, {'Content-Type': 'text/plain'}); + response.writeHead(200, { 'Content-Type': 'text/plain' }); response.write('1\n'); response.write(''); response.write('2\n'); diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js index 0efa935e43..616b08a7cb 100644 --- a/test/parallel/test-tls-disable-renegotiation.js +++ b/test/parallel/test-tls-disable-renegotiation.js @@ -39,13 +39,17 @@ const server = tls.Server(options, common.mustCall((socket) => { server.listen(0, common.mustCall(() => { const port = server.address().port; + const options = { + rejectUnauthorized: false, + port + }; const client = - tls.connect({rejectUnauthorized: false, port: port}, common.mustCall(() => { + tls.connect(options, common.mustCall(() => { client.write(''); // Negotiation is still permitted for this first // attempt. This should succeed. client.renegotiate( - {rejectUnauthorized: false}, + { rejectUnauthorized: false }, common.mustCall(() => { // Once renegotiation completes, we write some // data to the socket, which triggers the on @@ -58,7 +62,7 @@ server.listen(0, common.mustCall(() => { // server will simply drop the connection after // emitting the error. client.renegotiate( - {rejectUnauthorized: false}, + { rejectUnauthorized: false }, common.mustNotCall()); })); })); |