http2: add range support for respondWith{File|FD}

* respondWithFD now supports optional statCheck
* respondWithFD and respondWithFile both support offset/length for
  range requests
* Fix linting nits following most recent update

PR-URL: https://github.com/nodejs/node/pull/14239
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
James M Snell 2017-07-22 09:20:53 -07:00
parent 953458f645
commit d6a774b1bd
33 changed files with 357 additions and 79 deletions

View File

@ -998,13 +998,17 @@ server.on('stream', (stream) => {
}); });
``` ```
#### http2stream.respondWithFD(fd[, headers]) #### http2stream.respondWithFD(fd[, headers[, options]])
<!-- YAML <!-- YAML
added: REPLACEME added: REPLACEME
--> -->
* `fd` {number} A readable file descriptor * `fd` {number} A readable file descriptor
* `headers` {[Headers Object][]} * `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 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 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)); 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]]) #### http2stream.respondWithFile(path[, headers[, options]])
<!-- YAML <!-- YAML
added: REPLACEME added: REPLACEME
@ -1043,6 +1057,8 @@ added: REPLACEME
* `headers` {[Headers Object][]} * `headers` {[Headers Object][]}
* `options` {Object} * `options` {Object}
* `statCheck` {Function} * `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 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. 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 `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 ### Class: Http2Server
<!-- YAML <!-- YAML
added: REPLACEME added: REPLACEME

View File

@ -452,7 +452,7 @@ class Http2ServerResponse extends Stream {
stream.once('finish', cb); stream.once('finish', cb);
} }
this[kBeginSend]({endStream: true}); this[kBeginSend]({ endStream: true });
if (stream !== undefined) { if (stream !== undefined) {
stream.end(); stream.end();

View File

@ -1541,7 +1541,7 @@ function processHeaders(headers) {
return headers; return headers;
} }
function processRespondWithFD(fd, headers) { function processRespondWithFD(fd, headers, offset = 0, length = -1) {
const session = this[kSession]; const session = this[kSession];
const state = this[kState]; const state = this[kState];
state.headersSent = true; state.headersSent = true;
@ -1551,7 +1551,7 @@ function processRespondWithFD(fd, headers) {
const handle = session[kHandle]; const handle = session[kHandle];
const ret = const ret =
handle.submitFile(this[kID], fd, headers); handle.submitFile(this[kID], fd, headers, offset, length);
let err; let err;
switch (ret) { switch (ret) {
case NGHTTP2_ERR_NOMEM: case NGHTTP2_ERR_NOMEM:
@ -1575,26 +1575,71 @@ function doSendFD(session, options, fd, headers, err, stat) {
process.nextTick(() => this.emit('error', err)); process.nextTick(() => this.emit('error', err));
return; return;
} }
if (!stat.isFile()) {
err = new errors.Error('ERR_HTTP2_SEND_FILE');
process.nextTick(() => this.emit('error', err));
return;
}
// Set the content-length by default const statOptions = {
headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size; offset: options.offset !== undefined ? options.offset : 0,
length: options.length !== undefined ? options.length : -1
};
if (typeof options.statCheck === 'function' && if (typeof options.statCheck === 'function' &&
options.statCheck.call(this, stat, headers) === false) { options.statCheck.call(this, stat, headers, statOptions) === false) {
return; return;
} }
const headersList = mapToHeaders(headers, const headersList = mapToHeaders(headers,
assertValidPseudoHeaderResponse); assertValidPseudoHeaderResponse);
if (!Array.isArray(headersList)) { if (!Array.isArray(headersList)) {
throw headersList; process.nextTick(() => this.emit('error', headersList));
} }
processRespondWithFD.call(this, fd, 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
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)) {
process.nextTick(() => this.emit('error', headersList));
}
processRespondWithFD.call(this, fd, headersList,
options.offset,
options.length);
} }
function afterOpen(session, options, headers, err, fd) { function afterOpen(session, options, headers, err, fd) {
@ -1609,7 +1654,7 @@ function afterOpen(session, options, headers, err, fd) {
} }
state.fd = 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 // Initiate a response using an open FD. Note that there are fewer
// protections with this approach. For one, the fd is not validated. // protections with this approach. For one, the fd is not validated by
// In respondWithFile, the file is checked to make sure it is a // default. In respondWithFile, the file is checked to make sure it is a
// regular file, here the fd is passed directly. If the underlying // 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 // mechanism is not able to read from the fd, then the stream will be
// reset with an error code. // reset with an error code.
respondWithFD(fd, headers) { respondWithFD(fd, headers, options) {
const session = this[kSession]; const session = this[kSession];
if (this.destroyed) if (this.destroyed)
throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
@ -1803,6 +1848,26 @@ class ServerHttp2Stream extends Http2Stream {
if (state.headersSent) if (state.headersSent)
throw new errors.Error('ERR_HTTP2_HEADERS_SENT'); 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') if (typeof fd !== 'number')
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
'fd', 'number'); 'fd', 'number');
@ -1816,13 +1881,20 @@ class ServerHttp2Stream extends Http2Stream {
throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode); 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, const headersList = mapToHeaders(headers,
assertValidPseudoHeaderResponse); assertValidPseudoHeaderResponse);
if (!Array.isArray(headersList)) { 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 // Initiate a file response on this Http2Stream. The path is passed to
@ -1847,6 +1919,16 @@ class ServerHttp2Stream extends Http2Stream {
assertIsObject(options, 'options'); assertIsObject(options, 'options');
options = Object.assign(Object.create(null), 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 && if (options.statCheck !== undefined &&
typeof options.statCheck !== 'function') { typeof options.statCheck !== 'function') {
throw new errors.TypeError('ERR_INVALID_OPT_VALUE', throw new errors.TypeError('ERR_INVALID_OPT_VALUE',

View File

@ -604,7 +604,9 @@ void Http2Session::SubmitResponse(const FunctionCallbackInfo<Value>& args) {
void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) { void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsNumber()); // Stream ID CHECK(args[0]->IsNumber()); // Stream ID
CHECK(args[1]->IsNumber()); // File Descriptor 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; Http2Session* session;
Nghttp2Stream* stream; Nghttp2Stream* stream;
@ -618,6 +620,11 @@ void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) {
int fd = args[1]->Int32Value(context).ToChecked(); int fd = args[1]->Int32Value(context).ToChecked();
Local<Array> headers = args[2].As<Array>(); 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, " DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, "
"end-stream: %d\n", fd, id, headers->Length()); "end-stream: %d\n", fd, id, headers->Length());
@ -627,7 +634,8 @@ void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) {
Headers list(isolate, context, headers); 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) { void Http2Session::SendHeaders(const FunctionCallbackInfo<Value>& args) {

View File

@ -429,7 +429,10 @@ inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva,
} }
// Initiate a response that contains data read from a file descriptor. // 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(len, 0);
CHECK_GT(fd, 0); CHECK_GT(fd, 0);
DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_); 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.source.fd = fd;
prov.read_callback = Nghttp2Session::OnStreamReadFD; prov.read_callback = Nghttp2Session::OnStreamReadFD;
if (offset > 0) fd_offset_ = offset;
if (length > -1) fd_length_ = length;
return nghttp2_submit_response(session_->session(), id_, return nghttp2_submit_response(session_->session(), id_,
nva, len, &prov); nva, len, &prov);
} }

View File

@ -180,18 +180,25 @@ ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session,
int fd = source->fd; int fd = source->fd;
int64_t offset = stream->fd_offset_; 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; uv_buf_t data;
data.base = reinterpret_cast<char*>(buf); data.base = reinterpret_cast<char*>(buf);
data.len = length; data.len = length;
uv_fs_t read_req; uv_fs_t read_req;
numchars = uv_fs_read(handle->loop_,
&read_req, if (length > 0) {
fd, &data, 1, numchars = uv_fs_read(handle->loop_,
offset, nullptr); &read_req,
uv_fs_req_cleanup(&read_req); fd, &data, 1,
offset, nullptr);
uv_fs_req_cleanup(&read_req);
}
// Close the stream with an error if reading fails // Close the stream with an error if reading fails
if (numchars < 0) if (numchars < 0)
@ -199,9 +206,10 @@ ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session,
// Update the read offset for the next read // Update the read offset for the next read
stream->fd_offset_ += numchars; stream->fd_offset_ += numchars;
stream->fd_length_ -= numchars;
// if numchars < length, assume that we are done. // 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", DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n",
handle->session_type_, id); handle->session_type_, id);
*flags |= NGHTTP2_DATA_FLAG_EOF; *flags |= NGHTTP2_DATA_FLAG_EOF;

View File

@ -310,7 +310,10 @@ class Nghttp2Stream {
bool emptyPayload = false); bool emptyPayload = false);
// Send data read from a file descriptor as the response on this stream. // 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 // Submit informational headers for this stream
inline int SubmitInfo(nghttp2_nv* nva, size_t len); inline int SubmitInfo(nghttp2_nv* nva, size_t len);
@ -420,7 +423,8 @@ class Nghttp2Stream {
nghttp2_stream_write_queue* queue_tail_ = nullptr; nghttp2_stream_write_queue* queue_tail_ = nullptr;
unsigned int queue_head_index_ = 0; unsigned int queue_head_index_ = 0;
size_t queue_head_offset_ = 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, // The Current Headers block... As headers are received for this stream,
// they are temporarily stored here until the OnFrameReceived is called // they are temporarily stored here until the OnFrameReceived is called

View File

@ -24,28 +24,28 @@ server.on('listening', common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`); const client = h2.connect(`http://localhost:${server.address().port}`);
assert.throws(() => client.settings({headerTableSize: -1}), assert.throws(() => client.settings({ headerTableSize: -1 }),
RangeError); RangeError);
assert.throws(() => client.settings({headerTableSize: 2 ** 32}), assert.throws(() => client.settings({ headerTableSize: 2 ** 32 }),
RangeError); RangeError);
assert.throws(() => client.settings({initialWindowSize: -1}), assert.throws(() => client.settings({ initialWindowSize: -1 }),
RangeError); RangeError);
assert.throws(() => client.settings({initialWindowSize: 2 ** 32}), assert.throws(() => client.settings({ initialWindowSize: 2 ** 32 }),
RangeError); RangeError);
assert.throws(() => client.settings({maxFrameSize: 1}), assert.throws(() => client.settings({ maxFrameSize: 1 }),
RangeError); RangeError);
assert.throws(() => client.settings({maxFrameSize: 2 ** 24}), assert.throws(() => client.settings({ maxFrameSize: 2 ** 24 }),
RangeError); RangeError);
assert.throws(() => client.settings({maxConcurrentStreams: -1}), assert.throws(() => client.settings({ maxConcurrentStreams: -1 }),
RangeError); RangeError);
assert.throws(() => client.settings({maxConcurrentStreams: 2 ** 31}), assert.throws(() => client.settings({ maxConcurrentStreams: 2 ** 31 }),
RangeError); RangeError);
assert.throws(() => client.settings({maxHeaderListSize: -1}), assert.throws(() => client.settings({ maxHeaderListSize: -1 }),
RangeError); RangeError);
assert.throws(() => client.settings({maxHeaderListSize: 2 ** 32}), assert.throws(() => client.settings({ maxHeaderListSize: 2 ** 32 }),
RangeError); RangeError);
['a', 1, 0, null, {}].forEach((i) => { ['a', 1, 0, null, {}].forEach((i) => {
assert.throws(() => client.settings({enablePush: i}), TypeError); assert.throws(() => client.settings({ enablePush: i }), TypeError);
}); });
client.settings({ maxFrameSize: 1234567 }); client.settings({ maxFrameSize: 1234567 });

View File

@ -15,7 +15,7 @@ server.on('listening', common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`); const client = h2.connect(`http://localhost:${server.address().port}`);
client.shutdown({graceful: true}, common.mustCall(() => { client.shutdown({ graceful: true }, common.mustCall(() => {
server.close(); server.close();
client.destroy(); client.destroy();
})); }));

View File

@ -13,7 +13,7 @@ server.listen(0, common.mustCall(function() {
server.once('request', common.mustCall(function(request, response) { server.once('request', common.mustCall(function(request, response) {
response.flushHeaders(); response.flushHeaders();
response.flushHeaders(); // Idempotent response.flushHeaders(); // Idempotent
response.writeHead(400, {'foo-bar': 'abc123'}); // Ignored response.writeHead(400, { 'foo-bar': 'abc123' }); // Ignored
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
server.close(); server.close();

View File

@ -53,7 +53,7 @@ server.listen(0, common.mustCall(function() {
response.setHeader(real, expectedValue); response.setHeader(real, expectedValue);
const expectedHeaderNames = [real]; const expectedHeaderNames = [real];
assert.deepStrictEqual(response.getHeaderNames(), expectedHeaderNames); assert.deepStrictEqual(response.getHeaderNames(), expectedHeaderNames);
const expectedHeaders = {[real]: expectedValue}; const expectedHeaders = { [real]: expectedValue };
assert.deepStrictEqual(response.getHeaders(), expectedHeaders); assert.deepStrictEqual(response.getHeaders(), expectedHeaders);
response.getHeaders()[fake] = fake; response.getHeaders()[fake] = fake;

View File

@ -8,7 +8,7 @@ const h2 = require('http2');
// Http2ServerResponse.writeHead should accept an optional status message // Http2ServerResponse.writeHead should accept an optional status message
const unsupportedWarned = common.mustCall(1); const unsupportedWarned = common.mustCall(1);
process.on('warning', ({name, message}) => { process.on('warning', ({ name, message }) => {
const expectedMessage = const expectedMessage =
'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)'; 'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)';
if (name === 'UnsupportedWarning' && message === expectedMessage) if (name === 'UnsupportedWarning' && message === expectedMessage)
@ -21,7 +21,7 @@ server.listen(0, common.mustCall(function() {
server.once('request', common.mustCall(function(request, response) { server.once('request', common.mustCall(function(request, response) {
const statusCode = 200; const statusCode = 200;
const statusMessage = 'OK'; const statusMessage = 'OK';
const headers = {'foo-bar': 'abc123'}; const headers = { 'foo-bar': 'abc123' };
response.writeHead(statusCode, statusMessage, headers); response.writeHead(statusCode, statusMessage, headers);
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {

View File

@ -13,7 +13,7 @@ server.listen(0, common.mustCall(function() {
server.once('request', common.mustCall(function(request, response) { server.once('request', common.mustCall(function(request, response) {
response.setHeader('foo-bar', 'def456'); response.setHeader('foo-bar', 'def456');
response.writeHead(500); response.writeHead(500);
response.writeHead(418, {'foo-bar': 'abc123'}); // Override response.writeHead(418, { 'foo-bar': 'abc123' }); // Override
response.on('finish', common.mustCall(function() { response.on('finish', common.mustCall(function() {
assert.doesNotThrow(() => { response.writeHead(300); }); assert.doesNotThrow(() => { response.writeHead(300); });

View File

@ -21,8 +21,8 @@ const URL = url.URL;
[`http://localhost:${port}`], [`http://localhost:${port}`],
[new URL(`http://localhost:${port}`)], [new URL(`http://localhost:${port}`)],
[url.parse(`http://localhost:${port}`)], [url.parse(`http://localhost:${port}`)],
[{port: port}, {protocol: 'http:'}], [{ port: port }, { protocol: 'http:' }],
[{port: port, hostname: '127.0.0.1'}, {protocol: 'http:'}] [{ port: port, hostname: '127.0.0.1' }, { protocol: 'http:' }]
]; ];
let count = items.length; let count = items.length;
@ -41,7 +41,7 @@ const URL = url.URL;
}); });
// Will fail because protocol does not match the server. // 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()); .on('socketError', common.mustCall());
})); }));
} }
@ -60,14 +60,14 @@ const URL = url.URL;
server.on('listening', common.mustCall(function() { server.on('listening', common.mustCall(function() {
const port = this.address().port; const port = this.address().port;
const opts = {rejectUnauthorized: false}; const opts = { rejectUnauthorized: false };
const items = [ const items = [
[`https://localhost:${port}`, opts], [`https://localhost:${port}`, opts],
[new URL(`https://localhost:${port}`), opts], [new URL(`https://localhost:${port}`), opts],
[url.parse(`https://localhost:${port}`), opts], [url.parse(`https://localhost:${port}`), opts],
[{port: port, protocol: 'https:'}, opts], [{ port: port, protocol: 'https:' }, opts],
[{port: port, hostname: '127.0.0.1', protocol: 'https:'}, opts] [{ port: port, hostname: '127.0.0.1', protocol: 'https:' }, opts]
]; ];
let count = items.length; let count = items.length;

View File

@ -27,7 +27,7 @@ function onStream(stream, headers) {
} }
function verifySecureSession(key, cert, ca, opts) { 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.on('stream', common.mustCall(onStream));
server.listen(0); server.listen(0);
server.on('listening', common.mustCall(function() { server.on('listening', common.mustCall(function() {
@ -35,7 +35,7 @@ function verifySecureSession(key, cert, ca, opts) {
if (!opts) { if (!opts) {
opts = {}; opts = {};
} }
opts.secureContext = tls.createSecureContext({ca}); opts.secureContext = tls.createSecureContext({ ca });
const client = h2.connect(`https://localhost:${this.address().port}`, opts, function() { const client = h2.connect(`https://localhost:${this.address().port}`, opts, function() {
const req = client.request(headers); const req = client.request(headers);
@ -72,4 +72,4 @@ verifySecureSession(
loadKey('agent1-key.pem'), loadKey('agent1-key.pem'),
loadKey('agent1-cert.pem'), loadKey('agent1-cert.pem'),
loadKey('ca1-cert.pem'), loadKey('ca1-cert.pem'),
{servername: 'agent1'}); { servername: 'agent1' });

View File

@ -122,7 +122,7 @@ assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false }));
const packed = Buffer.from([0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF]); const packed = Buffer.from([0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF]);
assert.throws(() => { assert.throws(() => {
http2.getUnpackedSettings(packed, {validate: true}); http2.getUnpackedSettings(packed, { validate: true });
}, common.expectsError({ }, common.expectsError({
code: 'ERR_HTTP2_INVALID_SETTING_VALUE', code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
type: RangeError, type: RangeError,

View File

@ -56,7 +56,7 @@ server.on('listening', common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`); 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 // The additionalHeaders method does not exist on client stream
assert.strictEqual(req.additionalHeaders, undefined); assert.strictEqual(req.additionalHeaders, undefined);

View File

@ -13,7 +13,7 @@ const {
} = h2.constants; } = h2.constants;
// Only allow one stream to be open at a time // 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 // The stream handler must be called only once
server.on('stream', common.mustCall((stream) => { server.on('stream', common.mustCall((stream) => {

View File

@ -18,7 +18,7 @@ function onStream(stream, headers, flags) {
':method', ':method',
':scheme' ':scheme'
].forEach((i) => { ].forEach((i) => {
assert.throws(() => stream.respond({[i]: '/'}), assert.throws(() => stream.respond({ [i]: '/' }),
common.expectsError({ common.expectsError({
code: 'ERR_HTTP2_INVALID_PSEUDOHEADER' code: 'ERR_HTTP2_INVALID_PSEUDOHEADER'
})); }));

View File

@ -47,7 +47,7 @@ server.listen(0, common.mustCall(() => {
// Request 3 will fail because nghttp2 does not allow the content-length // Request 3 will fail because nghttp2 does not allow the content-length
// header to be set for non-payload bearing requests... // 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.resume();
req3.on('end', common.mustCall(maybeClose)); req3.on('end', common.mustCall(maybeClose));
req3.on('error', common.expectsError({ req3.on('error', common.expectsError({

View File

@ -30,7 +30,7 @@ server.listen(0, common.mustCall(() => {
} }
function doRequest() { function doRequest() {
const req = client.request({ ':method': 'POST '}); const req = client.request({ ':method': 'POST ' });
let data = ''; let data = '';
req.setEncoding('utf8'); req.setEncoding('utf8');

View File

@ -37,7 +37,7 @@ server.on('priority', common.mustCall(onPriority));
server.on('listening', common.mustCall(() => { server.on('listening', common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`); const client = h2.connect(`http://localhost:${server.address().port}`);
const req = client.request({ ':path': '/'}); const req = client.request({ ':path': '/' });
client.on('connect', () => { client.on('connect', () => {
req.priority({ req.priority({

View File

@ -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();
}
});

View File

@ -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();
});

View File

@ -25,7 +25,7 @@ const key = loadKey('agent8-key.pem');
const cert = loadKey('agent8-cert.pem'); const cert = loadKey('agent8-cert.pem');
const ca = loadKey('fake-startcom-root-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) => { server.on('stream', (stream, headers) => {
const name = headers[HTTP2_HEADER_PATH].slice(1); const name = headers[HTTP2_HEADER_PATH].slice(1);
@ -44,7 +44,7 @@ server.on('stream', (stream, headers) => {
server.listen(8000, () => { server.listen(8000, () => {
const secureContext = tls.createSecureContext({ca}); const secureContext = tls.createSecureContext({ ca });
const client = http2.connect(`https://localhost:${server.address().port}`, const client = http2.connect(`https://localhost:${server.address().port}`,
{ secureContext }); { secureContext });

View File

@ -36,7 +36,7 @@ server.on('stream', common.mustCall((stream) => {
})); }));
server.listen(0, common.mustCall(() => { server.listen(0, common.mustCall(() => {
const options = {settings: { enablePush: false }}; const options = { settings: { enablePush: false } };
const client = http2.connect(`http://localhost:${server.address().port}`, const client = http2.connect(`http://localhost:${server.address().port}`,
options); options);
const req = client.request({ ':path': '/' }); const req = client.request({ ':path': '/' });

View File

@ -11,7 +11,7 @@ server.on('stream', common.mustCall(onStream));
function onStream(stream, headers, flags) { function onStream(stream, headers, flags) {
const session = stream.session; const session = stream.session;
stream.session.shutdown({graceful: true}, common.mustCall(() => { stream.session.shutdown({ graceful: true }, common.mustCall(() => {
session.destroy(); session.destroy();
})); }));
stream.respond({}); stream.respond({});

View File

@ -90,7 +90,7 @@ server.on('listening', common.mustCall(() => {
})); }));
}); });
[1, {}, 'test', [], null, Infinity, NaN].forEach((i) => { [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => {
assert.throws(() => client.settings({enablePush: i}), assert.throws(() => client.settings({ enablePush: i }),
common.expectsError({ common.expectsError({
code: 'ERR_HTTP2_INVALID_SETTING_VALUE', code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
type: TypeError type: TypeError

View File

@ -9,7 +9,7 @@ const server = h2.createServer();
// we use the lower-level API here // we use the lower-level API here
server.on('stream', common.mustCall((stream) => { server.on('stream', common.mustCall((stream) => {
stream.setTimeout(1, common.mustCall(() => { stream.setTimeout(1, common.mustCall(() => {
stream.respond({':status': 200}); stream.respond({ ':status': 200 });
stream.end('hello world'); stream.end('hello world');
})); }));
})); }));

View File

@ -29,7 +29,7 @@ server.listen(0);
server.on('listening', common.mustCall(function() { server.on('listening', common.mustCall(function() {
const client = h2.connect(`http://localhost:${this.address().port}`); 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('data', common.mustCall());
req.on('trailers', common.mustCall((headers) => { req.on('trailers', common.mustCall((headers) => {
assert.strictEqual(headers[trailerKey], trailerValue); assert.strictEqual(headers[trailerKey], trailerValue);

View File

@ -191,7 +191,7 @@ const {
common.expectsError({ common.expectsError({
code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', code: 'ERR_HTTP2_HEADER_SINGLE_VALUE',
message: msg message: msg
})(mapToHeaders({[name]: [1, 2, 3]})); })(mapToHeaders({ [name]: [1, 2, 3] }));
}); });
[ [
@ -217,7 +217,7 @@ const {
HTTP2_HEADER_VIA, HTTP2_HEADER_VIA,
HTTP2_HEADER_WWW_AUTHENTICATE HTTP2_HEADER_WWW_AUTHENTICATE
].forEach((name) => { ].forEach((name) => {
assert(!(mapToHeaders({[name]: [1, 2, 3]}) instanceof Error), name); assert(!(mapToHeaders({ [name]: [1, 2, 3] }) instanceof Error), name);
}); });
const regex = const regex =
@ -242,7 +242,7 @@ const regex =
common.expectsError({ common.expectsError({
code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS', code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS',
message: regex message: regex
})(mapToHeaders({[name]: 'abc'})); })(mapToHeaders({ [name]: 'abc' }));
}); });
assert(!(mapToHeaders({ te: 'trailers' }) instanceof Error)); assert(!(mapToHeaders({ te: 'trailers' }) instanceof Error));

View File

@ -6,7 +6,7 @@ const assert = require('assert');
const http2 = require('http2'); const http2 = require('http2');
const server = http2.createServer(function(request, response) { 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('1\n');
response.write(''); response.write('');
response.write('2\n'); response.write('2\n');

View File

@ -39,13 +39,17 @@ const server = tls.Server(options, common.mustCall((socket) => {
server.listen(0, common.mustCall(() => { server.listen(0, common.mustCall(() => {
const port = server.address().port; const port = server.address().port;
const options = {
rejectUnauthorized: false,
port
};
const client = const client =
tls.connect({rejectUnauthorized: false, port: port}, common.mustCall(() => { tls.connect(options, common.mustCall(() => {
client.write(''); client.write('');
// Negotiation is still permitted for this first // Negotiation is still permitted for this first
// attempt. This should succeed. // attempt. This should succeed.
client.renegotiate( client.renegotiate(
{rejectUnauthorized: false}, { rejectUnauthorized: false },
common.mustCall(() => { common.mustCall(() => {
// Once renegotiation completes, we write some // Once renegotiation completes, we write some
// data to the socket, which triggers the on // data to the socket, which triggers the on
@ -58,7 +62,7 @@ server.listen(0, common.mustCall(() => {
// server will simply drop the connection after // server will simply drop the connection after
// emitting the error. // emitting the error.
client.renegotiate( client.renegotiate(
{rejectUnauthorized: false}, { rejectUnauthorized: false },
common.mustNotCall()); common.mustNotCall());
})); }));
})); }));