Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bb91834
http: move utcDate to internal/http.js
jasnell Oct 17, 2016
c6472e7
tls: add tlsSocket.disableRenegotiation()
jasnell Nov 4, 2016
a22543c
deps: add nghttp2 dependency
jasnell Jul 17, 2017
ea75860
http2: introducing HTTP/2
jasnell Jul 17, 2017
837dfe4
http2: add tests and benchmarks
jasnell Jul 17, 2017
e4b9149
http2: remove redundant return in test
jasnell Jul 17, 2017
8357bd0
http2: fix documentation nits
jasnell Jul 17, 2017
295e4b1
doc: include http2.md in all.md
jasnell Jul 17, 2017
9cb0611
test: fix flakiness in test-http2-client-upload
jasnell Jul 18, 2017
fefe2cb
test: fix flaky test-http2-client-unescaped-path on osx
jasnell Jul 18, 2017
a93b3b8
http2: fix abort when client.destroy inside end event
jasnell Jul 19, 2017
2289352
http2: refinement and test for socketError
jasnell Jul 19, 2017
d74da3a
http2: fix socketOnTimeout and a segfault
jasnell Jul 19, 2017
07758c8
http2: add range support for respondWith{File|FD}
jasnell Jul 22, 2017
0d8bf4c
http2: doc and fixes to the Compatibility API
mcollina Jul 24, 2017
2db82e0
http2: make writeHead behave like HTTP/1.
mcollina Jul 24, 2017
6bc7cc2
http2: address initial pr feedback
jasnell Jul 31, 2017
0a5fe92
http2: refactor trailers API
jasnell Jul 31, 2017
7c5825b
http2: get trailers working with the compat api
jasnell Jul 31, 2017
0e13eb6
http2: use static allocated arrays
jasnell Aug 1, 2017
a2045e9
http2: minor cleanup
jasnell Aug 1, 2017
c9c9e92
http2: fix documentation errors
jasnell Aug 1, 2017
ea2a35f
http2: add some doc detail for invalid header chars
jasnell Aug 1, 2017
69a0783
http2: fix compilation error after V8 update
jasnell Aug 3, 2017
ba2744e
http2: fix linting after rebase
jasnell Aug 3, 2017
c58115d
http2: fix flakiness in timeout
jasnell Aug 3, 2017
79e14a9
http2: rename some nghttp2 stream flags
kjin Aug 5, 2017
38f55ea
test: add crypto check to http2 tests
danbev Aug 7, 2017
ede1161
doc: fix http2 sample code for http2.md
kakts Aug 7, 2017
ea3c8c7
doc: explain browser support of http/2 without SSL
giltayar Aug 7, 2017
e344b91
src,http2: DRY header/trailer handling code up
addaleax Aug 8, 2017
70f7c54
test: increase http2 coverage
michaalbert Aug 8, 2017
1129943
http2: improve perf of passing headers to C++
addaleax Aug 9, 2017
5db4971
src: remove unused http2_socket_buffer from env
addaleax Aug 10, 2017
a6c1571
http2: use per-environment buffers
addaleax Aug 10, 2017
01bada5
http2: name padding buffer fields
addaleax Aug 10, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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: #14239
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
  • Loading branch information
jasnell committed Aug 13, 2017
commit 07758c8b451113a848b13bda35d872211012f389
22 changes: 21 additions & 1 deletion doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
104 changes: 93 additions & 11 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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:
Expand All @@ -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) {
Expand All @@ -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));
}


Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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
Expand All @@ -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',
Expand Down
12 changes: 10 additions & 2 deletions src/node_http2.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());

Expand All @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion src/node_http2_core-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Expand All @@ -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);
}
Expand Down
22 changes: 15 additions & 7 deletions src/node_http2_core.cc
Original file line number Diff line number Diff line change
Expand Up @@ -180,28 +180,36 @@ 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)
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;

// 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;
Expand Down
8 changes: 6 additions & 2 deletions src/node_http2_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading