mirror of https://github.com/lukechilds/node.git
Browse Source
At long last: The initial *experimental* implementation of HTTP/2. This is an accumulation of the work that has been done in the nodejs/http2 repository, squashed down to a couple of commits. The original commit history has been preserved in the nodejs/http2 repository. This PR introduces the nghttp2 C library as a new dependency. This library provides the majority of the HTTP/2 protocol implementation, with the rest of the code here providing the mapping of the library into a usable JS API. Within src, a handful of new node_http2_*.c and node_http2_*.h files are introduced. These provide the internal mechanisms that interface with nghttp and define the `process.binding('http2')` interface. The JS API is defined within `internal/http2/*.js`. There are two APIs provided: Core and Compat. The Core API is HTTP/2 specific and is designed to be as minimal and as efficient as possible. The Compat API is intended to be as close to the existing HTTP/1 API as possible, with some exceptions. Tests, documentation and initial benchmarks are included. The `http2` module is gated by a new `--expose-http2` command line flag. When used, `require('http2')` will be exposed to users. Note that there is an existing `http2` module on npm that would be impacted by the introduction of this module, which is the main reason for gating this behind a flag. When using `require('http2')` the first time, a process warning will be emitted indicating that an experimental feature is being used. To run the benchmarks, the `h2load` tool (part of the nghttp project) is required: `./node benchmarks/http2/simple.js benchmarker=h2load`. Only two benchmarks are currently available. Additional configuration options to enable verbose debugging are provided: ``` $ ./configure --debug-http2 --debug-nghttp2 $ NODE_DEBUG=http2 ./node ``` The `--debug-http2` configuration option enables verbose debug statements from the `src/node_http2_*` files. The `--debug-nghttp2` enables the nghttp library's own verbose debug output. The `NODE_DEBUG=http2` enables JS-level debug output. The following illustrates as simple HTTP/2 server and client interaction: (The HTTP/2 client and server support both plain text and TLS connections) ```jt client = http2.connect('http://localhost:80'); const req = client.request({ ':path': '/some/path' }); req.on('data', (chunk) => { /* do something with the data */ }); req.on('end', () => { client.destroy(); }); // Plain text (non-TLS server) const server = http2.createServer(); server.on('stream', (stream, requestHeaders) => { stream.respond({ ':status': 200 }); stream.write('hello '); stream.end('world'); }); server.listen(80); ``` ```js const http2 = require('http2'); const client = http2.connect('http://localhost'); ``` Author: Anna Henningsen <anna@addaleax.net> Author: Colin Ihrig <cjihrig@gmail.com> Author: Daniel Bevenius <daniel.bevenius@gmail.com> Author: James M Snell <jasnell@gmail.com> Author: Jun Mukai Author: Kelvin Jin Author: Matteo Collina <matteo.collina@gmail.com> Author: Robert Kowalski <rok@kowalski.gd> Author: Santiago Gimeno <santiago.gimeno@gmail.com> Author: Sebastiaan Deckers <sebdeckers83@gmail.com> Author: Yosuke Furukawa <yosuke.furukawa@gmail.com> 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>v6
35 changed files with 9061 additions and 7 deletions
File diff suppressed because it is too large
@ -0,0 +1,27 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
process.emitWarning( |
||||
|
'The http2 module is an experimental API.', |
||||
|
'ExperimentalWarning', undefined, |
||||
|
'See https://github.com/nodejs/http2' |
||||
|
); |
||||
|
|
||||
|
const { |
||||
|
constants, |
||||
|
getDefaultSettings, |
||||
|
getPackedSettings, |
||||
|
getUnpackedSettings, |
||||
|
createServer, |
||||
|
createSecureServer, |
||||
|
connect |
||||
|
} = require('internal/http2/core'); |
||||
|
|
||||
|
module.exports = { |
||||
|
constants, |
||||
|
getDefaultSettings, |
||||
|
getPackedSettings, |
||||
|
getUnpackedSettings, |
||||
|
createServer, |
||||
|
createSecureServer, |
||||
|
connect |
||||
|
}; |
@ -0,0 +1,570 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const Stream = require('stream'); |
||||
|
const Readable = Stream.Readable; |
||||
|
const binding = process.binding('http2'); |
||||
|
const constants = binding.constants; |
||||
|
const errors = require('internal/errors'); |
||||
|
|
||||
|
const kFinish = Symbol('finish'); |
||||
|
const kBeginSend = Symbol('begin-send'); |
||||
|
const kState = Symbol('state'); |
||||
|
const kStream = Symbol('stream'); |
||||
|
const kRequest = Symbol('request'); |
||||
|
const kResponse = Symbol('response'); |
||||
|
const kHeaders = Symbol('headers'); |
||||
|
const kTrailers = Symbol('trailers'); |
||||
|
|
||||
|
let statusMessageWarned = false; |
||||
|
|
||||
|
// Defines and implements an API compatibility layer on top of the core
|
||||
|
// HTTP/2 implementation, intended to provide an interface that is as
|
||||
|
// close as possible to the current require('http') API
|
||||
|
|
||||
|
function assertValidHeader(name, value) { |
||||
|
if (isPseudoHeader(name)) |
||||
|
throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED'); |
||||
|
if (value === undefined || value === null) |
||||
|
throw new errors.TypeError('ERR_HTTP2_INVALID_HEADER_VALUE'); |
||||
|
} |
||||
|
|
||||
|
function isPseudoHeader(name) { |
||||
|
switch (name) { |
||||
|
case ':status': |
||||
|
return true; |
||||
|
case ':method': |
||||
|
return true; |
||||
|
case ':path': |
||||
|
return true; |
||||
|
case ':authority': |
||||
|
return true; |
||||
|
case ':scheme': |
||||
|
return true; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onStreamData(chunk) { |
||||
|
const request = this[kRequest]; |
||||
|
if (!request.push(chunk)) |
||||
|
this.pause(); |
||||
|
} |
||||
|
|
||||
|
function onStreamEnd() { |
||||
|
// Cause the request stream to end as well.
|
||||
|
const request = this[kRequest]; |
||||
|
request.push(null); |
||||
|
} |
||||
|
|
||||
|
function onStreamError(error) { |
||||
|
const request = this[kRequest]; |
||||
|
request.emit('error', error); |
||||
|
} |
||||
|
|
||||
|
function onRequestPause() { |
||||
|
const stream = this[kStream]; |
||||
|
stream.pause(); |
||||
|
} |
||||
|
|
||||
|
function onRequestResume() { |
||||
|
const stream = this[kStream]; |
||||
|
stream.resume(); |
||||
|
} |
||||
|
|
||||
|
function onRequestDrain() { |
||||
|
if (this.isPaused()) |
||||
|
this.resume(); |
||||
|
} |
||||
|
|
||||
|
function onStreamResponseDrain() { |
||||
|
const response = this[kResponse]; |
||||
|
response.emit('drain'); |
||||
|
} |
||||
|
|
||||
|
function onStreamResponseError(error) { |
||||
|
const response = this[kResponse]; |
||||
|
response.emit('error', error); |
||||
|
} |
||||
|
|
||||
|
function onStreamClosedRequest() { |
||||
|
const req = this[kRequest]; |
||||
|
req.push(null); |
||||
|
} |
||||
|
|
||||
|
function onStreamClosedResponse() { |
||||
|
const res = this[kResponse]; |
||||
|
res.writable = false; |
||||
|
res.emit('finish'); |
||||
|
} |
||||
|
|
||||
|
function onAborted(hadError, code) { |
||||
|
if ((this.writable) || |
||||
|
(this._readableState && !this._readableState.ended)) { |
||||
|
this.emit('aborted', hadError, code); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class Http2ServerRequest extends Readable { |
||||
|
constructor(stream, headers, options) { |
||||
|
super(options); |
||||
|
this[kState] = { |
||||
|
statusCode: null, |
||||
|
closed: false, |
||||
|
closedCode: constants.NGHTTP2_NO_ERROR |
||||
|
}; |
||||
|
this[kHeaders] = headers; |
||||
|
this[kStream] = stream; |
||||
|
stream[kRequest] = this; |
||||
|
|
||||
|
// Pause the stream..
|
||||
|
stream.pause(); |
||||
|
stream.on('data', onStreamData); |
||||
|
stream.on('end', onStreamEnd); |
||||
|
stream.on('error', onStreamError); |
||||
|
stream.on('close', onStreamClosedRequest); |
||||
|
stream.on('aborted', onAborted.bind(this)); |
||||
|
const onfinish = this[kFinish].bind(this); |
||||
|
stream.on('streamClosed', onfinish); |
||||
|
stream.on('finish', onfinish); |
||||
|
this.on('pause', onRequestPause); |
||||
|
this.on('resume', onRequestResume); |
||||
|
this.on('drain', onRequestDrain); |
||||
|
} |
||||
|
|
||||
|
get closed() { |
||||
|
const state = this[kState]; |
||||
|
return Boolean(state.closed); |
||||
|
} |
||||
|
|
||||
|
get code() { |
||||
|
const state = this[kState]; |
||||
|
return Number(state.closedCode); |
||||
|
} |
||||
|
|
||||
|
get stream() { |
||||
|
return this[kStream]; |
||||
|
} |
||||
|
|
||||
|
get statusCode() { |
||||
|
return this[kState].statusCode; |
||||
|
} |
||||
|
|
||||
|
get headers() { |
||||
|
return this[kHeaders]; |
||||
|
} |
||||
|
|
||||
|
get rawHeaders() { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return []; |
||||
|
const tuples = Object.entries(headers); |
||||
|
const flattened = Array.prototype.concat.apply([], tuples); |
||||
|
return flattened.map(String); |
||||
|
} |
||||
|
|
||||
|
get trailers() { |
||||
|
return this[kTrailers]; |
||||
|
} |
||||
|
|
||||
|
get httpVersionMajor() { |
||||
|
return 2; |
||||
|
} |
||||
|
|
||||
|
get httpVersionMinor() { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
get httpVersion() { |
||||
|
return '2.0'; |
||||
|
} |
||||
|
|
||||
|
get socket() { |
||||
|
return this.stream.session.socket; |
||||
|
} |
||||
|
|
||||
|
get connection() { |
||||
|
return this.socket; |
||||
|
} |
||||
|
|
||||
|
_read(nread) { |
||||
|
const stream = this[kStream]; |
||||
|
if (stream) { |
||||
|
stream.resume(); |
||||
|
} else { |
||||
|
throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get method() { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return; |
||||
|
return headers[constants.HTTP2_HEADER_METHOD]; |
||||
|
} |
||||
|
|
||||
|
get authority() { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return; |
||||
|
return headers[constants.HTTP2_HEADER_AUTHORITY]; |
||||
|
} |
||||
|
|
||||
|
get scheme() { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return; |
||||
|
return headers[constants.HTTP2_HEADER_SCHEME]; |
||||
|
} |
||||
|
|
||||
|
get url() { |
||||
|
return this.path; |
||||
|
} |
||||
|
|
||||
|
set url(url) { |
||||
|
this.path = url; |
||||
|
} |
||||
|
|
||||
|
get path() { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return; |
||||
|
return headers[constants.HTTP2_HEADER_PATH]; |
||||
|
} |
||||
|
|
||||
|
set path(path) { |
||||
|
let headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
headers = this[kHeaders] = Object.create(null); |
||||
|
headers[constants.HTTP2_HEADER_PATH] = path; |
||||
|
} |
||||
|
|
||||
|
setTimeout(msecs, callback) { |
||||
|
const stream = this[kStream]; |
||||
|
if (stream === undefined) return; |
||||
|
stream.setTimeout(msecs, callback); |
||||
|
} |
||||
|
|
||||
|
[kFinish](code) { |
||||
|
const state = this[kState]; |
||||
|
if (state.closed) |
||||
|
return; |
||||
|
state.closedCode = code; |
||||
|
state.closed = true; |
||||
|
this.push(null); |
||||
|
this[kStream] = undefined; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class Http2ServerResponse extends Stream { |
||||
|
constructor(stream, options) { |
||||
|
super(options); |
||||
|
this[kState] = { |
||||
|
sendDate: true, |
||||
|
statusCode: constants.HTTP_STATUS_OK, |
||||
|
headerCount: 0, |
||||
|
trailerCount: 0, |
||||
|
closed: false, |
||||
|
closedCode: constants.NGHTTP2_NO_ERROR |
||||
|
}; |
||||
|
this[kStream] = stream; |
||||
|
stream[kResponse] = this; |
||||
|
this.writable = true; |
||||
|
stream.on('drain', onStreamResponseDrain); |
||||
|
stream.on('error', onStreamResponseError); |
||||
|
stream.on('close', onStreamClosedResponse); |
||||
|
stream.on('aborted', onAborted.bind(this)); |
||||
|
const onfinish = this[kFinish].bind(this); |
||||
|
stream.on('streamClosed', onfinish); |
||||
|
stream.on('finish', onfinish); |
||||
|
} |
||||
|
|
||||
|
get finished() { |
||||
|
const stream = this[kStream]; |
||||
|
return stream === undefined || stream._writableState.ended; |
||||
|
} |
||||
|
|
||||
|
get closed() { |
||||
|
const state = this[kState]; |
||||
|
return Boolean(state.closed); |
||||
|
} |
||||
|
|
||||
|
get code() { |
||||
|
const state = this[kState]; |
||||
|
return Number(state.closedCode); |
||||
|
} |
||||
|
|
||||
|
get stream() { |
||||
|
return this[kStream]; |
||||
|
} |
||||
|
|
||||
|
get headersSent() { |
||||
|
const stream = this[kStream]; |
||||
|
return stream.headersSent; |
||||
|
} |
||||
|
|
||||
|
get sendDate() { |
||||
|
return Boolean(this[kState].sendDate); |
||||
|
} |
||||
|
|
||||
|
set sendDate(bool) { |
||||
|
this[kState].sendDate = Boolean(bool); |
||||
|
} |
||||
|
|
||||
|
get statusCode() { |
||||
|
return this[kState].statusCode; |
||||
|
} |
||||
|
|
||||
|
set statusCode(code) { |
||||
|
const state = this[kState]; |
||||
|
code |= 0; |
||||
|
if (code >= 100 && code < 200) |
||||
|
throw new errors.RangeError('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED'); |
||||
|
if (code < 200 || code > 599) |
||||
|
throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID', code); |
||||
|
state.statusCode = code; |
||||
|
} |
||||
|
|
||||
|
addTrailers(headers) { |
||||
|
let trailers = this[kTrailers]; |
||||
|
const keys = Object.keys(headers); |
||||
|
let key = ''; |
||||
|
if (keys.length > 0) |
||||
|
return; |
||||
|
if (trailers === undefined) |
||||
|
trailers = this[kTrailers] = Object.create(null); |
||||
|
for (var i = 0; i < keys.length; i++) { |
||||
|
key = String(keys[i]).trim().toLowerCase(); |
||||
|
const value = headers[key]; |
||||
|
assertValidHeader(key, value); |
||||
|
trailers[key] = String(value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
getHeader(name) { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return; |
||||
|
name = String(name).trim().toLowerCase(); |
||||
|
return headers[name]; |
||||
|
} |
||||
|
|
||||
|
getHeaderNames() { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return []; |
||||
|
return Object.keys(headers); |
||||
|
} |
||||
|
|
||||
|
getHeaders() { |
||||
|
const headers = this[kHeaders]; |
||||
|
return Object.assign({}, headers); |
||||
|
} |
||||
|
|
||||
|
hasHeader(name) { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return false; |
||||
|
name = String(name).trim().toLowerCase(); |
||||
|
return Object.prototype.hasOwnProperty.call(headers, name); |
||||
|
} |
||||
|
|
||||
|
removeHeader(name) { |
||||
|
const headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
return; |
||||
|
name = String(name).trim().toLowerCase(); |
||||
|
delete headers[name]; |
||||
|
} |
||||
|
|
||||
|
setHeader(name, value) { |
||||
|
name = String(name).trim().toLowerCase(); |
||||
|
assertValidHeader(name, value); |
||||
|
let headers = this[kHeaders]; |
||||
|
if (headers === undefined) |
||||
|
headers = this[kHeaders] = Object.create(null); |
||||
|
headers[name] = String(value); |
||||
|
} |
||||
|
|
||||
|
flushHeaders() { |
||||
|
if (this[kStream].headersSent === false) |
||||
|
this[kBeginSend](); |
||||
|
} |
||||
|
|
||||
|
writeHead(statusCode, statusMessage, headers) { |
||||
|
if (typeof statusMessage === 'string' && statusMessageWarned === false) { |
||||
|
process.emitWarning( |
||||
|
'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)', |
||||
|
'UnsupportedWarning' |
||||
|
); |
||||
|
statusMessageWarned = true; |
||||
|
} |
||||
|
if (headers === undefined && typeof statusMessage === 'object') { |
||||
|
headers = statusMessage; |
||||
|
} |
||||
|
if (headers) { |
||||
|
const keys = Object.keys(headers); |
||||
|
let key = ''; |
||||
|
for (var i = 0; i < keys.length; i++) { |
||||
|
key = keys[i]; |
||||
|
this.setHeader(key, headers[key]); |
||||
|
} |
||||
|
} |
||||
|
this.statusCode = statusCode; |
||||
|
} |
||||
|
|
||||
|
write(chunk, encoding, cb) { |
||||
|
const stream = this[kStream]; |
||||
|
|
||||
|
if (typeof encoding === 'function') { |
||||
|
cb = encoding; |
||||
|
encoding = 'utf8'; |
||||
|
} |
||||
|
|
||||
|
if (stream === undefined) { |
||||
|
const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); |
||||
|
if (cb) |
||||
|
process.nextTick(cb, err); |
||||
|
else |
||||
|
throw err; |
||||
|
return; |
||||
|
} |
||||
|
this[kBeginSend](); |
||||
|
return stream.write(chunk, encoding, cb); |
||||
|
} |
||||
|
|
||||
|
end(chunk, encoding, cb) { |
||||
|
const stream = this[kStream]; |
||||
|
|
||||
|
if (typeof chunk === 'function') { |
||||
|
cb = chunk; |
||||
|
chunk = null; |
||||
|
encoding = 'utf8'; |
||||
|
} else if (typeof encoding === 'function') { |
||||
|
cb = encoding; |
||||
|
encoding = 'utf8'; |
||||
|
} |
||||
|
if (chunk !== null && chunk !== undefined) { |
||||
|
this.write(chunk, encoding); |
||||
|
} |
||||
|
|
||||
|
if (typeof cb === 'function' && stream !== undefined) { |
||||
|
stream.once('finish', cb); |
||||
|
} |
||||
|
|
||||
|
this[kBeginSend]({endStream: true}); |
||||
|
|
||||
|
if (stream !== undefined) { |
||||
|
stream.end(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
destroy(err) { |
||||
|
const stream = this[kStream]; |
||||
|
if (stream === undefined) { |
||||
|
// nothing to do, already closed
|
||||
|
return; |
||||
|
} |
||||
|
stream.destroy(err); |
||||
|
} |
||||
|
|
||||
|
setTimeout(msecs, callback) { |
||||
|
const stream = this[kStream]; |
||||
|
if (stream === undefined) return; |
||||
|
stream.setTimeout(msecs, callback); |
||||
|
} |
||||
|
|
||||
|
sendContinue(headers) { |
||||
|
this.sendInfo(100, headers); |
||||
|
} |
||||
|
|
||||
|
sendInfo(code, headers) { |
||||
|
const stream = this[kStream]; |
||||
|
if (stream.headersSent === true) { |
||||
|
throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND'); |
||||
|
} |
||||
|
if (headers && typeof headers !== 'object') |
||||
|
throw new errors.TypeError('ERR_HTTP2_HEADERS_OBJECT'); |
||||
|
if (stream === undefined) return; |
||||
|
code |= 0; |
||||
|
if (code < 100 || code >= 200) |
||||
|
throw new errors.RangeError('ERR_HTTP2_INVALID_INFO_STATUS', code); |
||||
|
|
||||
|
headers[constants.HTTP2_HEADER_STATUS] = code; |
||||
|
stream.respond(headers); |
||||
|
} |
||||
|
|
||||
|
createPushResponse(headers, callback) { |
||||
|
const stream = this[kStream]; |
||||
|
if (stream === undefined) { |
||||
|
throw new errors.Error('ERR_HTTP2_STREAM_CLOSED'); |
||||
|
} |
||||
|
stream.pushStream(headers, {}, function(stream, headers, options) { |
||||
|
const response = new Http2ServerResponse(stream); |
||||
|
callback(null, response); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[kBeginSend](options) { |
||||
|
const stream = this[kStream]; |
||||
|
if (stream !== undefined && stream.headersSent === false) { |
||||
|
const state = this[kState]; |
||||
|
const headers = this[kHeaders] || Object.create(null); |
||||
|
headers[constants.HTTP2_HEADER_STATUS] = state.statusCode; |
||||
|
if (stream.finished === true) |
||||
|
options.endStream = true; |
||||
|
if (stream.destroyed === false) { |
||||
|
stream.respond(headers, options); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[kFinish](code) { |
||||
|
const state = this[kState]; |
||||
|
if (state.closed) |
||||
|
return; |
||||
|
state.closedCode = code; |
||||
|
state.closed = true; |
||||
|
this.end(); |
||||
|
this[kStream] = undefined; |
||||
|
this.emit('finish'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onServerStream(stream, headers, flags) { |
||||
|
const server = this; |
||||
|
const request = new Http2ServerRequest(stream, headers); |
||||
|
const response = new Http2ServerResponse(stream); |
||||
|
|
||||
|
// Check for the CONNECT method
|
||||
|
const method = headers[constants.HTTP2_HEADER_METHOD]; |
||||
|
if (method === 'CONNECT') { |
||||
|
if (!server.emit('connect', request, response)) { |
||||
|
response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED; |
||||
|
response.end(); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Check for Expectations
|
||||
|
if (headers.expect !== undefined) { |
||||
|
if (headers.expect === '100-continue') { |
||||
|
if (server.listenerCount('checkContinue')) { |
||||
|
server.emit('checkContinue', request, response); |
||||
|
} else { |
||||
|
response.sendContinue(); |
||||
|
server.emit('request', request, response); |
||||
|
} |
||||
|
} else if (server.listenerCount('checkExpectation')) { |
||||
|
server.emit('checkExpectation', request, response); |
||||
|
} else { |
||||
|
response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED; |
||||
|
response.end(); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
server.emit('request', request, response); |
||||
|
} |
||||
|
|
||||
|
module.exports = { onServerStream }; |
File diff suppressed because it is too large
@ -0,0 +1,513 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
const binding = process.binding('http2'); |
||||
|
const errors = require('internal/errors'); |
||||
|
|
||||
|
const { |
||||
|
HTTP2_HEADER_STATUS, |
||||
|
HTTP2_HEADER_METHOD, |
||||
|
HTTP2_HEADER_AUTHORITY, |
||||
|
HTTP2_HEADER_SCHEME, |
||||
|
HTTP2_HEADER_PATH, |
||||
|
HTTP2_HEADER_AGE, |
||||
|
HTTP2_HEADER_AUTHORIZATION, |
||||
|
HTTP2_HEADER_CONTENT_ENCODING, |
||||
|
HTTP2_HEADER_CONTENT_LANGUAGE, |
||||
|
HTTP2_HEADER_CONTENT_LENGTH, |
||||
|
HTTP2_HEADER_CONTENT_LOCATION, |
||||
|
HTTP2_HEADER_CONTENT_MD5, |
||||
|
HTTP2_HEADER_CONTENT_RANGE, |
||||
|
HTTP2_HEADER_CONTENT_TYPE, |
||||
|
HTTP2_HEADER_COOKIE, |
||||
|
HTTP2_HEADER_DATE, |
||||
|
HTTP2_HEADER_ETAG, |
||||
|
HTTP2_HEADER_EXPIRES, |
||||
|
HTTP2_HEADER_FROM, |
||||
|
HTTP2_HEADER_IF_MATCH, |
||||
|
HTTP2_HEADER_IF_NONE_MATCH, |
||||
|
HTTP2_HEADER_IF_MODIFIED_SINCE, |
||||
|
HTTP2_HEADER_IF_RANGE, |
||||
|
HTTP2_HEADER_IF_UNMODIFIED_SINCE, |
||||
|
HTTP2_HEADER_LAST_MODIFIED, |
||||
|
HTTP2_HEADER_LOCATION, |
||||
|
HTTP2_HEADER_MAX_FORWARDS, |
||||
|
HTTP2_HEADER_PROXY_AUTHORIZATION, |
||||
|
HTTP2_HEADER_RANGE, |
||||
|
HTTP2_HEADER_REFERER, |
||||
|
HTTP2_HEADER_RETRY_AFTER, |
||||
|
HTTP2_HEADER_USER_AGENT, |
||||
|
|
||||
|
HTTP2_HEADER_CONNECTION, |
||||
|
HTTP2_HEADER_UPGRADE, |
||||
|
HTTP2_HEADER_HTTP2_SETTINGS, |
||||
|
HTTP2_HEADER_TE, |
||||
|
HTTP2_HEADER_TRANSFER_ENCODING, |
||||
|
HTTP2_HEADER_HOST, |
||||
|
HTTP2_HEADER_KEEP_ALIVE, |
||||
|
HTTP2_HEADER_PROXY_CONNECTION, |
||||
|
|
||||
|
HTTP2_METHOD_DELETE, |
||||
|
HTTP2_METHOD_GET, |
||||
|
HTTP2_METHOD_HEAD |
||||
|
} = binding.constants; |
||||
|
|
||||
|
// This set is defined strictly by the HTTP/2 specification. Only
|
||||
|
// :-prefixed headers defined by that specification may be added to
|
||||
|
// this set.
|
||||
|
const kValidPseudoHeaders = new Set([ |
||||
|
HTTP2_HEADER_STATUS, |
||||
|
HTTP2_HEADER_METHOD, |
||||
|
HTTP2_HEADER_AUTHORITY, |
||||
|
HTTP2_HEADER_SCHEME, |
||||
|
HTTP2_HEADER_PATH |
||||
|
]); |
||||
|
|
||||
|
// This set contains headers that are permitted to have only a single
|
||||
|
// value. Multiple instances must not be specified.
|
||||
|
const kSingleValueHeaders = new Set([ |
||||
|
HTTP2_HEADER_STATUS, |
||||
|
HTTP2_HEADER_METHOD, |
||||
|
HTTP2_HEADER_AUTHORITY, |
||||
|
HTTP2_HEADER_SCHEME, |
||||
|
HTTP2_HEADER_PATH, |
||||
|
HTTP2_HEADER_AGE, |
||||
|
HTTP2_HEADER_AUTHORIZATION, |
||||
|
HTTP2_HEADER_CONTENT_ENCODING, |
||||
|
HTTP2_HEADER_CONTENT_LANGUAGE, |
||||
|
HTTP2_HEADER_CONTENT_LENGTH, |
||||
|
HTTP2_HEADER_CONTENT_LOCATION, |
||||
|
HTTP2_HEADER_CONTENT_MD5, |
||||
|
HTTP2_HEADER_CONTENT_RANGE, |
||||
|
HTTP2_HEADER_CONTENT_TYPE, |
||||
|
HTTP2_HEADER_DATE, |
||||
|
HTTP2_HEADER_ETAG, |
||||
|
HTTP2_HEADER_EXPIRES, |
||||
|
HTTP2_HEADER_FROM, |
||||
|
HTTP2_HEADER_IF_MATCH, |
||||
|
HTTP2_HEADER_IF_MODIFIED_SINCE, |
||||
|
HTTP2_HEADER_IF_NONE_MATCH, |
||||
|
HTTP2_HEADER_IF_RANGE, |
||||
|
HTTP2_HEADER_IF_UNMODIFIED_SINCE, |
||||
|
HTTP2_HEADER_LAST_MODIFIED, |
||||
|
HTTP2_HEADER_LOCATION, |
||||
|
HTTP2_HEADER_MAX_FORWARDS, |
||||
|
HTTP2_HEADER_PROXY_AUTHORIZATION, |
||||
|
HTTP2_HEADER_RANGE, |
||||
|
HTTP2_HEADER_REFERER, |
||||
|
HTTP2_HEADER_RETRY_AFTER, |
||||
|
HTTP2_HEADER_USER_AGENT |
||||
|
]); |
||||
|
|
||||
|
// The HTTP methods in this set are specifically defined as assigning no
|
||||
|
// meaning to the request payload. By default, unless the user explicitly
|
||||
|
// overrides the endStream option on the request method, the endStream
|
||||
|
// option will be defaulted to true when these methods are used.
|
||||
|
const kNoPayloadMethods = new Set([ |
||||
|
HTTP2_METHOD_DELETE, |
||||
|
HTTP2_METHOD_GET, |
||||
|
HTTP2_METHOD_HEAD |
||||
|
]); |
||||
|
|
||||
|
// The following ArrayBuffer instances are used to share memory more efficiently
|
||||
|
// with the native binding side for a number of methods. These are not intended
|
||||
|
// to be used directly by users in any way. The ArrayBuffers are created on
|
||||
|
// the native side with values that are filled in on demand, the js code then
|
||||
|
// reads those values out. The set of IDX constants that follow identify the
|
||||
|
// relevant data positions within these buffers.
|
||||
|
const settingsBuffer = new Uint32Array(binding.settingsArrayBuffer); |
||||
|
const optionsBuffer = new Uint32Array(binding.optionsArrayBuffer); |
||||
|
|
||||
|
// Note that Float64Array is used here because there is no Int64Array available
|
||||
|
// and these deal with numbers that can be beyond the range of Uint32 and Int32.
|
||||
|
// The values set on the native side will always be integers. This is not a
|
||||
|
// unique example of this, this pattern can be found in use in other parts of
|
||||
|
// Node.js core as a performance optimization.
|
||||
|
const sessionState = new Float64Array(binding.sessionStateArrayBuffer); |
||||
|
const streamState = new Float64Array(binding.streamStateArrayBuffer); |
||||
|
|
||||
|
const IDX_SETTINGS_HEADER_TABLE_SIZE = 0; |
||||
|
const IDX_SETTINGS_ENABLE_PUSH = 1; |
||||
|
const IDX_SETTINGS_INITIAL_WINDOW_SIZE = 2; |
||||
|
const IDX_SETTINGS_MAX_FRAME_SIZE = 3; |
||||
|
const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4; |
||||
|
const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5; |
||||
|
const IDX_SETTINGS_FLAGS = 6; |
||||
|
|
||||
|
const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0; |
||||
|
const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1; |
||||
|
const IDX_SESSION_STATE_NEXT_STREAM_ID = 2; |
||||
|
const IDX_SESSION_STATE_LOCAL_WINDOW_SIZE = 3; |
||||
|
const IDX_SESSION_STATE_LAST_PROC_STREAM_ID = 4; |
||||
|
const IDX_SESSION_STATE_REMOTE_WINDOW_SIZE = 5; |
||||
|
const IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE = 6; |
||||
|
const IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE = 7; |
||||
|
const IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE = 8; |
||||
|
const IDX_STREAM_STATE = 0; |
||||
|
const IDX_STREAM_STATE_WEIGHT = 1; |
||||
|
const IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT = 2; |
||||
|
const IDX_STREAM_STATE_LOCAL_CLOSE = 3; |
||||
|
const IDX_STREAM_STATE_REMOTE_CLOSE = 4; |
||||
|
const IDX_STREAM_STATE_LOCAL_WINDOW_SIZE = 5; |
||||
|
|
||||
|
const IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE = 0; |
||||
|
const IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS = 1; |
||||
|
const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2; |
||||
|
const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3; |
||||
|
const IDX_OPTIONS_PADDING_STRATEGY = 4; |
||||
|
const IDX_OPTIONS_FLAGS = 5; |
||||
|
|
||||
|
function updateOptionsBuffer(options) { |
||||
|
var flags = 0; |
||||
|
if (typeof options.maxDeflateDynamicTableSize === 'number') { |
||||
|
flags |= (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE); |
||||
|
optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE] = |
||||
|
options.maxDeflateDynamicTableSize; |
||||
|
} |
||||
|
if (typeof options.maxReservedRemoteStreams === 'number') { |
||||
|
flags |= (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS); |
||||
|
optionsBuffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS] = |
||||
|
options.maxReservedRemoteStreams; |
||||
|
} |
||||
|
if (typeof options.maxSendHeaderBlockLength === 'number') { |
||||
|
flags |= (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH); |
||||
|
optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] = |
||||
|
options.maxSendHeaderBlockLength; |
||||
|
} |
||||
|
if (typeof options.peerMaxConcurrentStreams === 'number') { |
||||
|
flags |= (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS); |
||||
|
optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS] = |
||||
|
options.peerMaxConcurrentStreams; |
||||
|
} |
||||
|
if (typeof options.paddingStrategy === 'number') { |
||||
|
flags |= (1 << IDX_OPTIONS_PADDING_STRATEGY); |
||||
|
optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY] = |
||||
|
options.paddingStrategy; |
||||
|
} |
||||
|
optionsBuffer[IDX_OPTIONS_FLAGS] = flags; |
||||
|
} |
||||
|
|
||||
|
function getDefaultSettings() { |
||||
|
settingsBuffer[IDX_SETTINGS_FLAGS] = 0; |
||||
|
binding.refreshDefaultSettings(); |
||||
|
const holder = Object.create(null); |
||||
|
|
||||
|
const flags = settingsBuffer[IDX_SETTINGS_FLAGS]; |
||||
|
|
||||
|
if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) === |
||||
|
(1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { |
||||
|
holder.headerTableSize = |
||||
|
settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE]; |
||||
|
} |
||||
|
|
||||
|
if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) === |
||||
|
(1 << IDX_SETTINGS_ENABLE_PUSH)) { |
||||
|
holder.enablePush = |
||||
|
settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] === 1; |
||||
|
} |
||||
|
|
||||
|
if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) === |
||||
|
(1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { |
||||
|
holder.initialWindowSize = |
||||
|
settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]; |
||||
|
} |
||||
|
|
||||
|
if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) === |
||||
|
(1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { |
||||
|
holder.maxFrameSize = |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE]; |
||||
|
} |
||||
|
|
||||
|
if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) === |
||||
|
(1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { |
||||
|
console.log('setting it'); |
||||
|
holder.maxConcurrentStreams = |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]; |
||||
|
} |
||||
|
|
||||
|
if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) === |
||||
|
(1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { |
||||
|
holder.maxHeaderListSize = |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; |
||||
|
} |
||||
|
|
||||
|
return holder; |
||||
|
} |
||||
|
|
||||
|
// remote is a boolean. true to fetch remote settings, false to fetch local.
|
||||
|
// this is only called internally
|
||||
|
function getSettings(session, remote) { |
||||
|
const holder = Object.create(null); |
||||
|
if (remote) |
||||
|
binding.refreshRemoteSettings(session); |
||||
|
else |
||||
|
binding.refreshLocalSettings(session); |
||||
|
|
||||
|
holder.headerTableSize = |
||||
|
settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE]; |
||||
|
holder.enablePush = |
||||
|
!!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH]; |
||||
|
holder.initialWindowSize = |
||||
|
settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]; |
||||
|
holder.maxFrameSize = |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE]; |
||||
|
holder.maxConcurrentStreams = |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]; |
||||
|
holder.maxHeaderListSize = |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; |
||||
|
return holder; |
||||
|
} |
||||
|
|
||||
|
function updateSettingsBuffer(settings) { |
||||
|
var flags = 0; |
||||
|
if (typeof settings.headerTableSize === 'number') { |
||||
|
flags |= (1 << IDX_SETTINGS_HEADER_TABLE_SIZE); |
||||
|
settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = |
||||
|
settings.headerTableSize; |
||||
|
} |
||||
|
if (typeof settings.maxConcurrentStreams === 'number') { |
||||
|
flags |= (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS); |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = |
||||
|
settings.maxConcurrentStreams; |
||||
|
} |
||||
|
if (typeof settings.initialWindowSize === 'number') { |
||||
|
flags |= (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE); |
||||
|
settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = |
||||
|
settings.initialWindowSize; |
||||
|
} |
||||
|
if (typeof settings.maxFrameSize === 'number') { |
||||
|
flags |= (1 << IDX_SETTINGS_MAX_FRAME_SIZE); |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] = |
||||
|
settings.maxFrameSize; |
||||
|
} |
||||
|
if (typeof settings.maxHeaderListSize === 'number') { |
||||
|
flags |= (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE); |
||||
|
settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = |
||||
|
settings.maxHeaderListSize; |
||||
|
} |
||||
|
if (typeof settings.enablePush === 'boolean') { |
||||
|
flags |= (1 << IDX_SETTINGS_ENABLE_PUSH); |
||||
|
settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush); |
||||
|
} |
||||
|
|
||||
|
settingsBuffer[IDX_SETTINGS_FLAGS] = flags; |
||||
|
} |
||||
|
|
||||
|
function getSessionState(session) { |
||||
|
const holder = Object.create(null); |
||||
|
binding.refreshSessionState(session); |
||||
|
holder.effectiveLocalWindowSize = |
||||
|
sessionState[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE]; |
||||
|
holder.effectiveRecvDataLength = |
||||
|
sessionState[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH]; |
||||
|
holder.nextStreamID = |
||||
|
sessionState[IDX_SESSION_STATE_NEXT_STREAM_ID]; |
||||
|
holder.localWindowSize = |
||||
|
sessionState[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE]; |
||||
|
holder.lastProcStreamID = |
||||
|
sessionState[IDX_SESSION_STATE_LAST_PROC_STREAM_ID]; |
||||
|
holder.remoteWindowSize = |
||||
|
sessionState[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE]; |
||||
|
holder.outboundQueueSize = |
||||
|
sessionState[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE]; |
||||
|
holder.deflateDynamicTableSize = |
||||
|
sessionState[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE]; |
||||
|
holder.inflateDynamicTableSize = |
||||
|
sessionState[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE]; |
||||
|
return holder; |
||||
|
} |
||||
|
|
||||
|
function getStreamState(session, stream) { |
||||
|
const holder = Object.create(null); |
||||
|
binding.refreshStreamState(session, stream); |
||||
|
holder.state = |
||||
|
streamState[IDX_STREAM_STATE]; |
||||
|
holder.weight = |
||||
|
streamState[IDX_STREAM_STATE_WEIGHT]; |
||||
|
holder.sumDependencyWeight = |
||||
|
streamState[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT]; |
||||
|
holder.localClose = |
||||
|
streamState[IDX_STREAM_STATE_LOCAL_CLOSE]; |
||||
|
holder.remoteClose = |
||||
|
streamState[IDX_STREAM_STATE_REMOTE_CLOSE]; |
||||
|
holder.localWindowSize = |
||||
|
streamState[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE]; |
||||
|
return holder; |
||||
|
} |
||||
|
|
||||
|
function isIllegalConnectionSpecificHeader(name, value) { |
||||
|
switch (name) { |
||||
|
case HTTP2_HEADER_CONNECTION: |
||||
|
case HTTP2_HEADER_UPGRADE: |
||||
|
case HTTP2_HEADER_HOST: |
||||
|
case HTTP2_HEADER_HTTP2_SETTINGS: |
||||
|
case HTTP2_HEADER_KEEP_ALIVE: |
||||
|
case HTTP2_HEADER_PROXY_CONNECTION: |
||||
|
case HTTP2_HEADER_TRANSFER_ENCODING: |
||||
|
return true; |
||||
|
case HTTP2_HEADER_TE: |
||||
|
const val = Array.isArray(value) ? value.join(', ') : value; |
||||
|
return val !== 'trailers'; |
||||
|
default: |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function assertValidPseudoHeader(key) { |
||||
|
if (!kValidPseudoHeaders.has(key)) { |
||||
|
const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key); |
||||
|
Error.captureStackTrace(err, assertValidPseudoHeader); |
||||
|
return err; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function assertValidPseudoHeaderResponse(key) { |
||||
|
if (key !== ':status') { |
||||
|
const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key); |
||||
|
Error.captureStackTrace(err, assertValidPseudoHeaderResponse); |
||||
|
return err; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function assertValidPseudoHeaderTrailer(key) { |
||||
|
const err = new errors.Error('ERR_HTTP2_INVALID_PSEUDOHEADER', key); |
||||
|
Error.captureStackTrace(err, assertValidPseudoHeaderTrailer); |
||||
|
return err; |
||||
|
} |
||||
|
|
||||
|
function mapToHeaders(map, |
||||
|
assertValuePseudoHeader = assertValidPseudoHeader) { |
||||
|
const ret = []; |
||||
|
const keys = Object.keys(map); |
||||
|
const singles = new Set(); |
||||
|
for (var i = 0; i < keys.length; i++) { |
||||
|
let key = keys[i]; |
||||
|
let value = map[key]; |
||||
|
let val; |
||||
|
if (typeof key === 'symbol' || value === undefined || !key) |
||||
|
continue; |
||||
|
key = String(key).toLowerCase(); |
||||
|
const isArray = Array.isArray(value); |
||||
|
if (isArray) { |
||||
|
switch (value.length) { |
||||
|
case 0: |
||||
|
continue; |
||||
|
case 1: |
||||
|
value = String(value[0]); |
||||
|
break; |
||||
|
default: |
||||
|
if (kSingleValueHeaders.has(key)) |
||||
|
return new errors.Error('ERR_HTTP2_HEADER_SINGLE_VALUE', key); |
||||
|
} |
||||
|
} |
||||
|
if (key[0] === ':') { |
||||
|
const err = assertValuePseudoHeader(key); |
||||
|
if (err !== undefined) |
||||
|
return err; |
||||
|
ret.unshift([key, String(value)]); |
||||
|
} else { |
||||
|
if (kSingleValueHeaders.has(key)) { |
||||
|
if (singles.has(key)) |
||||
|
return new errors.Error('ERR_HTTP2_HEADER_SINGLE_VALUE', key); |
||||
|
singles.add(key); |
||||
|
} |
||||
|
if (isIllegalConnectionSpecificHeader(key, value)) { |
||||
|
return new errors.Error('ERR_HTTP2_INVALID_CONNECTION_HEADERS'); |
||||
|
} |
||||
|
if (isArray) { |
||||
|
for (var k = 0; k < value.length; k++) { |
||||
|
val = String(value[k]); |
||||
|
ret.push([key, val]); |
||||
|
} |
||||
|
} else { |
||||
|
val = String(value); |
||||
|
ret.push([key, val]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ret; |
||||
|
} |
||||
|
|
||||
|
class NghttpError extends Error { |
||||
|
constructor(ret) { |
||||
|
super(binding.nghttp2ErrorString(ret)); |
||||
|
this.code = 'ERR_HTTP2_ERROR'; |
||||
|
this.name = 'Error [ERR_HTTP2_ERROR]'; |
||||
|
this.errno = ret; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function assertIsObject(value, name, types) { |
||||
|
if (value !== undefined && |
||||
|
(value === null || |
||||
|
typeof value !== 'object' || |
||||
|
Array.isArray(value))) { |
||||
|
const err = new errors.TypeError('ERR_INVALID_ARG_TYPE', |
||||
|
name, types || 'object'); |
||||
|
Error.captureStackTrace(err, assertIsObject); |
||||
|
throw err; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function assertWithinRange(name, value, min = 0, max = Infinity) { |
||||
|
if (value !== undefined && |
||||
|
(typeof value !== 'number' || value < min || value > max)) { |
||||
|
const err = new errors.RangeError('ERR_HTTP2_INVALID_SETTING_VALUE', |
||||
|
name, value); |
||||
|
err.min = min; |
||||
|
err.max = max; |
||||
|
err.actual = value; |
||||
|
Error.captureStackTrace(err, assertWithinRange); |
||||
|
throw err; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toHeaderObject(headers) { |
||||
|
const obj = Object.create(null); |
||||
|
for (var n = 0; n < headers.length; n = n + 2) { |
||||
|
var name = headers[n]; |
||||
|
var value = headers[n + 1]; |
||||
|
if (name === HTTP2_HEADER_STATUS) |
||||
|
value |= 0; |
||||
|
var existing = obj[name]; |
||||
|
if (existing === undefined) { |
||||
|
obj[name] = value; |
||||
|
} else if (!kSingleValueHeaders.has(name)) { |
||||
|
if (name === HTTP2_HEADER_COOKIE) { |
||||
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
|
||||
|
// "...If there are multiple Cookie header fields after decompression,
|
||||
|
// these MUST be concatenated into a single octet string using the
|
||||
|
// two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ") before
|
||||
|
// being passed into a non-HTTP/2 context."
|
||||
|
obj[name] = `${existing}; ${value}`; |
||||
|
} else { |
||||
|
if (Array.isArray(existing)) |
||||
|
existing.push(value); |
||||
|
else |
||||
|
obj[name] = [existing, value]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return obj; |
||||
|
} |
||||
|
|
||||
|
function isPayloadMeaningless(method) { |
||||
|
return kNoPayloadMethods.has(method); |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
assertIsObject, |
||||
|
assertValidPseudoHeaderResponse, |
||||
|
assertValidPseudoHeaderTrailer, |
||||
|
assertWithinRange, |
||||
|
getDefaultSettings, |
||||
|
getSessionState, |
||||
|
getSettings, |
||||
|
getStreamState, |
||||
|
isPayloadMeaningless, |
||||
|
mapToHeaders, |
||||
|
NghttpError, |
||||
|
toHeaderObject, |
||||
|
updateOptionsBuffer, |
||||
|
updateSettingsBuffer |
||||
|
}; |
@ -0,0 +1,92 @@ |
|||||
|
#ifndef SRC_FREELIST_H_ |
||||
|
#define SRC_FREELIST_H_ |
||||
|
|
||||
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
||||
|
|
||||
|
#include "util.h" |
||||
|
|
||||
|
namespace node { |
||||
|
|
||||
|
struct DefaultFreelistTraits; |
||||
|
|
||||
|
template <typename T, |
||||
|
size_t kMaximumLength, |
||||
|
typename FreelistTraits = DefaultFreelistTraits> |
||||
|
class Freelist { |
||||
|
public: |
||||
|
typedef struct list_item { |
||||
|
T* item = nullptr; |
||||
|
list_item* next = nullptr; |
||||
|
} list_item; |
||||
|
|
||||
|
Freelist() {} |
||||
|
~Freelist() { |
||||
|
while (head_ != nullptr) { |
||||
|
list_item* item = head_; |
||||
|
head_ = item->next; |
||||
|
FreelistTraits::Free(item->item); |
||||
|
free(item); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void push(T* item) { |
||||
|
if (size_ > kMaximumLength) { |
||||
|
FreelistTraits::Free(item); |
||||
|
} else { |
||||
|
size_++; |
||||
|
FreelistTraits::Reset(item); |
||||
|
list_item* li = Calloc<list_item>(1); |
||||
|
li->item = item; |
||||
|
if (head_ == nullptr) { |
||||
|
head_ = li; |
||||
|
tail_ = li; |
||||
|
} else { |
||||
|
tail_->next = li; |
||||
|
tail_ = li; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
T* pop() { |
||||
|
if (head_ != nullptr) { |
||||
|
size_--; |
||||
|
list_item* cur = head_; |
||||
|
T* item = cur->item; |
||||
|
head_ = cur->next; |
||||
|
free(cur); |
||||
|
return item; |
||||
|
} else { |
||||
|
return FreelistTraits::template Alloc<T>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
size_t size_ = 0; |
||||
|
list_item* head_ = nullptr; |
||||
|
list_item* tail_ = nullptr; |
||||
|
}; |
||||
|
|
||||
|
struct DefaultFreelistTraits { |
||||
|
template <typename T> |
||||
|
static T* Alloc() { |
||||
|
return ::new (Malloc<T>(1)) T(); |
||||
|
} |
||||
|
|
||||
|
template <typename T> |
||||
|
static void Free(T* item) { |
||||
|
item->~T(); |
||||
|
free(item); |
||||
|
} |
||||
|
|
||||
|
template <typename T> |
||||
|
static void Reset(T* item) { |
||||
|
item->~T(); |
||||
|
::new (item) T(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
} // namespace node
|
||||
|
|
||||
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
|
||||
|
#endif // SRC_FREELIST_H_
|
File diff suppressed because it is too large
@ -0,0 +1,572 @@ |
|||||
|
#ifndef SRC_NODE_HTTP2_H_ |
||||
|
#define SRC_NODE_HTTP2_H_ |
||||
|
|
||||
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
||||
|
|
||||
|
#include "node_http2_core-inl.h" |
||||
|
#include "stream_base-inl.h" |
||||
|
#include "string_bytes.h" |
||||
|
|
||||
|
namespace node { |
||||
|
namespace http2 { |
||||
|
|
||||
|
using v8::Array; |
||||
|
using v8::Context; |
||||
|
using v8::EscapableHandleScope; |
||||
|
using v8::Isolate; |
||||
|
using v8::MaybeLocal; |
||||
|
|
||||
|
#define HTTP_KNOWN_METHODS(V) \ |
||||
|
V(ACL, "ACL") \ |
||||
|
V(BASELINE_CONTROL, "BASELINE-CONTROL") \ |
||||
|
V(BIND, "BIND") \ |
||||
|
V(CHECKIN, "CHECKIN") \ |
||||
|
V(CHECKOUT, "CHECKOUT") \ |
||||
|
V(CONNECT, "CONNECT") \ |
||||
|
V(COPY, "COPY") \ |
||||
|
V(DELETE, "DELETE") \ |
||||
|
V(GET, "GET") \ |
||||
|
V(HEAD, "HEAD") \ |
||||
|
V(LABEL, "LABEL") \ |
||||
|
V(LINK, "LINK") \ |
||||
|
V(LOCK, "LOCK") \ |
||||
|
V(MERGE, "MERGE") \ |
||||
|
V(MKACTIVITY, "MKACTIVITY") \ |
||||
|
V(MKCALENDAR, "MKCALENDAR") \ |
||||
|
V(MKCOL, "MKCOL") \ |
||||
|
V(MKREDIRECTREF, "MKREDIRECTREF") \ |
||||
|
V(MKWORKSPACE, "MKWORKSPACE") \ |
||||
|
V(MOVE, "MOVE") \ |
||||
|
V(OPTIONS, "OPTIONS") \ |
||||
|
V(ORDERPATCH, "ORDERPATCH") \ |
||||
|
V(PATCH, "PATCH") \ |
||||
|
V(POST, "POST") \ |
||||
|
V(PRI, "PRI") \ |
||||
|
V(PROPFIND, "PROPFIND") \ |
||||
|
V(PROPPATCH, "PROPPATCH") \ |
||||
|
V(PUT, "PUT") \ |
||||
|
V(REBIND, "REBIND") \ |
||||
|
V(REPORT, "REPORT") \ |
||||
|
V(SEARCH, "SEARCH") \ |
||||
|
V(TRACE, "TRACE") \ |
||||
|
V(UNBIND, "UNBIND") \ |
||||
|
V(UNCHECKOUT, "UNCHECKOUT") \ |
||||
|
V(UNLINK, "UNLINK") \ |
||||
|
V(UNLOCK, "UNLOCK") \ |
||||
|
V(UPDATE, "UPDATE") \ |
||||
|
V(UPDATEREDIRECTREF, "UPDATEREDIRECTREF") \ |
||||
|
V(VERSION_CONTROL, "VERSION-CONTROL") |
||||
|
|
||||
|
#define HTTP_KNOWN_HEADERS(V) \ |
||||
|
V(STATUS, ":status") \ |
||||
|
V(METHOD, ":method") \ |
||||
|
V(AUTHORITY, ":authority") \ |
||||
|
V(SCHEME, ":scheme") \ |
||||
|
V(PATH, ":path") \ |
||||
|
V(ACCEPT_CHARSET, "accept-charset") \ |
||||
|
V(ACCEPT_ENCODING, "accept-encoding") \ |
||||
|
V(ACCEPT_LANGUAGE, "accept-language") \ |
||||
|
V(ACCEPT_RANGES, "accept-ranges") \ |
||||
|
V(ACCEPT, "accept") \ |
||||
|
V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \ |
||||
|
V(AGE, "age") \ |
||||
|
V(ALLOW, "allow") \ |
||||
|
V(AUTHORIZATION, "authorization") \ |
||||
|
V(CACHE_CONTROL, "cache-control") \ |
||||
|
V(CONNECTION, "connection") \ |
||||
|
V(CONTENT_DISPOSITION, "content-disposition") \ |
||||
|
V(CONTENT_ENCODING, "content-encoding") \ |
||||
|
V(CONTENT_LANGUAGE, "content-language") \ |
||||
|
V(CONTENT_LENGTH, "content-length") \ |
||||
|
V(CONTENT_LOCATION, "content-location") \ |
||||
|
V(CONTENT_MD5, "content-md5") \ |
||||
|
V(CONTENT_RANGE, "content-range") \ |
||||
|
V(CONTENT_TYPE, "content-type") \ |
||||
|
V(COOKIE, "cookie") \ |
||||
|
V(DATE, "date") \ |
||||
|
V(ETAG, "etag") \ |
||||
|
V(EXPECT, "expect") \ |
||||
|
V(EXPIRES, "expires") \ |
||||
|
V(FROM, "from") \ |
||||
|
V(HOST, "host") \ |
||||
|
V(IF_MATCH, "if-match") \ |
||||
|
V(IF_MODIFIED_SINCE, "if-modified-since") \ |
||||
|
V(IF_NONE_MATCH, "if-none-match") \ |
||||
|
V(IF_RANGE, "if-range") \ |
||||
|
V(IF_UNMODIFIED_SINCE, "if-unmodified-since") \ |
||||
|
V(LAST_MODIFIED, "last-modified") \ |
||||
|
V(LINK, "link") \ |
||||
|
V(LOCATION, "location") \ |
||||
|
V(MAX_FORWARDS, "max-forwards") \ |
||||
|
V(PREFER, "prefer") \ |
||||
|
V(PROXY_AUTHENTICATE, "proxy-authenticate") \ |
||||
|
V(PROXY_AUTHORIZATION, "proxy-authorization") \ |
||||
|
V(RANGE, "range") \ |
||||
|
V(REFERER, "referer") \ |
||||
|
V(REFRESH, "refresh") \ |
||||
|
V(RETRY_AFTER, "retry-after") \ |
||||
|
V(SERVER, "server") \ |
||||
|
V(SET_COOKIE, "set-cookie") \ |
||||
|
V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \ |
||||
|
V(TRANSFER_ENCODING, "transfer-encoding") \ |
||||
|
V(TE, "te") \ |
||||
|
V(UPGRADE, "upgrade") \ |
||||
|
V(USER_AGENT, "user-agent") \ |
||||
|
V(VARY, "vary") \ |
||||
|
V(VIA, "via") \ |
||||
|
V(WWW_AUTHENTICATE, "www-authenticate") \ |
||||
|
V(HTTP2_SETTINGS, "http2-settings") \ |
||||
|
V(KEEP_ALIVE, "keep-alive") \ |
||||
|
V(PROXY_CONNECTION, "proxy-connection") |
||||
|
|
||||
|
enum http_known_headers { |
||||
|
HTTP_KNOWN_HEADER_MIN, |
||||
|
#define V(name, value) HTTP_HEADER_##name, |
||||
|
HTTP_KNOWN_HEADERS(V) |
||||
|
#undef V |
||||
|
HTTP_KNOWN_HEADER_MAX |
||||
|
}; |
||||
|
|
||||
|
#define HTTP_STATUS_CODES(V) \ |
||||
|
V(CONTINUE, 100) \ |
||||
|
V(SWITCHING_PROTOCOLS, 101) \ |
||||
|
V(PROCESSING, 102) \ |
||||
|
V(OK, 200) \ |
||||
|
V(CREATED, 201) \ |
||||
|
V(ACCEPTED, 202) \ |
||||
|
V(NON_AUTHORITATIVE_INFORMATION, 203) \ |
||||
|
V(NO_CONTENT, 204) \ |
||||
|
V(RESET_CONTENT, 205) \ |
||||
|
V(PARTIAL_CONTENT, 206) \ |
||||
|
V(MULTI_STATUS, 207) \ |
||||
|
V(ALREADY_REPORTED, 208) \ |
||||
|
V(IM_USED, 226) \ |
||||
|
V(MULTIPLE_CHOICES, 300) \ |
||||
|
V(MOVED_PERMANENTLY, 301) \ |
||||
|
V(FOUND, 302) \ |
||||
|
V(SEE_OTHER, 303) \ |
||||
|
V(NOT_MODIFIED, 304) \ |
||||
|
V(USE_PROXY, 305) \ |
||||
|
V(TEMPORARY_REDIRECT, 307) \ |
||||
|
V(PERMANENT_REDIRECT, 308) \ |
||||
|
V(BAD_REQUEST, 400) \ |
||||
|
V(UNAUTHORIZED, 401) \ |
||||
|
V(PAYMENT_REQUIRED, 402) \ |
||||
|
V(FORBIDDEN, 403) \ |
||||
|
V(NOT_FOUND, 404) \ |
||||
|
V(METHOD_NOT_ALLOWED, 405) \ |
||||
|
V(NOT_ACCEPTABLE, 406) \ |
||||
|
V(PROXY_AUTHENTICATION_REQUIRED, 407) \ |
||||
|
V(REQUEST_TIMEOUT, 408) \ |
||||
|
V(CONFLICT, 409) \ |
||||
|
V(GONE, 410) \ |
||||
|
V(LENGTH_REQUIRED, 411) \ |
||||
|
V(PRECONDITION_FAILED, 412) \ |
||||
|
V(PAYLOAD_TOO_LARGE, 413) \ |
||||
|
V(URI_TOO_LONG, 414) \ |
||||
|
V(UNSUPPORTED_MEDIA_TYPE, 415) \ |
||||
|
V(RANGE_NOT_SATISFIABLE, 416) \ |
||||
|
V(EXPECTATION_FAILED, 417) \ |
||||
|
V(TEAPOT, 418) \ |
||||
|
V(MISDIRECTED_REQUEST, 421) \ |
||||
|
V(UNPROCESSABLE_ENTITY, 422) \ |
||||
|
V(LOCKED, 423) \ |
||||
|
V(FAILED_DEPENDENCY, 424) \ |
||||
|
V(UNORDERED_COLLECTION, 425) \ |
||||
|
V(UPGRADE_REQUIRED, 426) \ |
||||
|
V(PRECONDITION_REQUIRED, 428) \ |
||||
|
V(TOO_MANY_REQUESTS, 429) \ |
||||
|
V(REQUEST_HEADER_FIELDS_TOO_LARGE, 431) \ |
||||
|
V(UNAVAILABLE_FOR_LEGAL_REASONS, 451) \ |
||||
|
V(INTERNAL_SERVER_ERROR, 500) \ |
||||
|
V(NOT_IMPLEMENTED, 501) \ |
||||
|
V(BAD_GATEWAY, 502) \ |
||||
|
V(SERVICE_UNAVAILABLE, 503) \ |
||||
|
V(GATEWAY_TIMEOUT, 504) \ |
||||
|
V(HTTP_VERSION_NOT_SUPPORTED, 505) \ |
||||
|
V(VARIANT_ALSO_NEGOTIATES, 506) \ |
||||
|
V(INSUFFICIENT_STORAGE, 507) \ |
||||
|
V(LOOP_DETECTED, 508) \ |
||||
|
V(BANDWIDTH_LIMIT_EXCEEDED, 509) \ |
||||
|
V(NOT_EXTENDED, 510) \ |
||||
|
V(NETWORK_AUTHENTICATION_REQUIRED, 511) |
||||
|
|
||||
|
enum http_status_codes { |
||||
|
#define V(name, code) HTTP_STATUS_##name = code, |
||||
|
HTTP_STATUS_CODES(V) |
||||
|
#undef V |
||||
|
}; |
||||
|
|
||||
|
enum padding_strategy_type { |
||||
|
// No padding strategy
|
||||
|
PADDING_STRATEGY_NONE, |
||||
|
// Padding will ensure all data frames are maxFrameSize
|
||||
|
PADDING_STRATEGY_MAX, |
||||
|
// Padding will be determined via JS callback
|
||||
|
PADDING_STRATEGY_CALLBACK |
||||
|
}; |
||||
|
|
||||
|
#define NGHTTP2_ERROR_CODES(V) \ |
||||
|
V(NGHTTP2_ERR_INVALID_ARGUMENT) \ |
||||
|
V(NGHTTP2_ERR_BUFFER_ERROR) \ |
||||
|
V(NGHTTP2_ERR_UNSUPPORTED_VERSION) \ |
||||
|
V(NGHTTP2_ERR_WOULDBLOCK) \ |
||||
|
V(NGHTTP2_ERR_PROTO) \ |
||||
|
V(NGHTTP2_ERR_INVALID_FRAME) \ |
||||
|
V(NGHTTP2_ERR_EOF) \ |
||||
|
V(NGHTTP2_ERR_DEFERRED) \ |
||||
|
V(NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE) \ |
||||
|
V(NGHTTP2_ERR_STREAM_CLOSED) \ |
||||
|
V(NGHTTP2_ERR_STREAM_CLOSING) \ |
||||
|
V(NGHTTP2_ERR_STREAM_SHUT_WR) \ |
||||
|
V(NGHTTP2_ERR_INVALID_STREAM_ID) \ |
||||
|
V(NGHTTP2_ERR_INVALID_STREAM_STATE) \ |
||||
|
V(NGHTTP2_ERR_DEFERRED_DATA_EXIST) \ |
||||
|
V(NGHTTP2_ERR_START_STREAM_NOT_ALLOWED) \ |
||||
|
V(NGHTTP2_ERR_GOAWAY_ALREADY_SENT) \ |
||||
|
V(NGHTTP2_ERR_INVALID_HEADER_BLOCK) \ |
||||
|
V(NGHTTP2_ERR_INVALID_STATE) \ |
||||
|
V(NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) \ |
||||
|
V(NGHTTP2_ERR_FRAME_SIZE_ERROR) \ |
||||
|
V(NGHTTP2_ERR_HEADER_COMP) \ |
||||
|
V(NGHTTP2_ERR_FLOW_CONTROL) \ |
||||
|
V(NGHTTP2_ERR_INSUFF_BUFSIZE) \ |
||||
|
V(NGHTTP2_ERR_PAUSE) \ |
||||
|
V(NGHTTP2_ERR_TOO_MANY_INFLIGHT_SETTINGS) \ |
||||
|
V(NGHTTP2_ERR_PUSH_DISABLED) \ |
||||
|
V(NGHTTP2_ERR_DATA_EXIST) \ |
||||
|
V(NGHTTP2_ERR_SESSION_CLOSING) \ |
||||
|
V(NGHTTP2_ERR_HTTP_HEADER) \ |
||||
|
V(NGHTTP2_ERR_HTTP_MESSAGING) \ |
||||
|
V(NGHTTP2_ERR_REFUSED_STREAM) \ |
||||
|
V(NGHTTP2_ERR_INTERNAL) \ |
||||
|
V(NGHTTP2_ERR_CANCEL) \ |
||||
|
V(NGHTTP2_ERR_FATAL) \ |
||||
|
V(NGHTTP2_ERR_NOMEM) \ |
||||
|
V(NGHTTP2_ERR_CALLBACK_FAILURE) \ |
||||
|
V(NGHTTP2_ERR_BAD_CLIENT_MAGIC) \ |
||||
|
V(NGHTTP2_ERR_FLOODED) |
||||
|
|
||||
|
const char* nghttp2_errname(int rv) { |
||||
|
switch (rv) { |
||||
|
#define V(code) case code: return #code; |
||||
|
NGHTTP2_ERROR_CODES(V) |
||||
|
#undef V |
||||
|
default: |
||||
|
return "NGHTTP2_UNKNOWN_ERROR"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096 |
||||
|
#define DEFAULT_SETTINGS_ENABLE_PUSH 1 |
||||
|
#define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535 |
||||
|
#define DEFAULT_SETTINGS_MAX_FRAME_SIZE 16384 |
||||
|
#define MAX_MAX_FRAME_SIZE 16777215 |
||||
|
#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE |
||||
|
#define MAX_INITIAL_WINDOW_SIZE 2147483647 |
||||
|
|
||||
|
class Http2Options { |
||||
|
public: |
||||
|
explicit Http2Options(Environment* env); |
||||
|
|
||||
|
~Http2Options() { |
||||
|
nghttp2_option_del(options_); |
||||
|
} |
||||
|
|
||||
|
nghttp2_option* operator*() { |
||||
|
return options_; |
||||
|
} |
||||
|
|
||||
|
void SetPaddingStrategy(uint32_t val) { |
||||
|
CHECK_LE(val, PADDING_STRATEGY_CALLBACK); |
||||
|
padding_strategy_ = static_cast<padding_strategy_type>(val); |
||||
|
} |
||||
|
|
||||
|
void SetMaxDeflateDynamicTableSize(size_t val) { |
||||
|
nghttp2_option_set_max_deflate_dynamic_table_size(options_, val); |
||||
|
} |
||||
|
|
||||
|
void SetMaxReservedRemoteStreams(uint32_t val) { |
||||
|
nghttp2_option_set_max_reserved_remote_streams(options_, val); |
||||
|
} |
||||
|
|
||||
|
void SetMaxSendHeaderBlockLength(size_t val) { |
||||
|
nghttp2_option_set_max_send_header_block_length(options_, val); |
||||
|
} |
||||
|
|
||||
|
void SetPeerMaxConcurrentStreams(uint32_t val) { |
||||
|
nghttp2_option_set_peer_max_concurrent_streams(options_, val); |
||||
|
} |
||||
|
|
||||
|
padding_strategy_type GetPaddingStrategy() { |
||||
|
return padding_strategy_; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
nghttp2_option* options_; |
||||
|
padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; |
||||
|
}; |
||||
|
|
||||
|
static const size_t kAllocBufferSize = 64 * 1024; |
||||
|
|
||||
|
////
|
||||
|
typedef uint32_t(*get_setting)(nghttp2_session* session, |
||||
|
nghttp2_settings_id id); |
||||
|
|
||||
|
class Http2Session : public AsyncWrap, |
||||
|
public StreamBase, |
||||
|
public Nghttp2Session { |
||||
|
public: |
||||
|
Http2Session(Environment* env, |
||||
|
Local<Object> wrap, |
||||
|
nghttp2_session_type type) : |
||||
|
AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTP2SESSION), |
||||
|
StreamBase(env) { |
||||
|
Wrap(object(), this); |
||||
|
|
||||
|
Http2Options opts(env); |
||||
|
|
||||
|
padding_strategy_ = opts.GetPaddingStrategy(); |
||||
|
|
||||
|
Init(env->event_loop(), type, *opts); |
||||
|
stream_buf_.AllocateSufficientStorage(kAllocBufferSize); |
||||
|
} |
||||
|
|
||||
|
~Http2Session() override { |
||||
|
CHECK_EQ(false, persistent().IsEmpty()); |
||||
|
ClearWrap(object()); |
||||
|
persistent().Reset(); |
||||
|
CHECK_EQ(true, persistent().IsEmpty()); |
||||
|
} |
||||
|
|
||||
|
static void OnStreamAllocImpl(size_t suggested_size, |
||||
|
uv_buf_t* buf, |
||||
|
void* ctx); |
||||
|
static void OnStreamReadImpl(ssize_t nread, |
||||
|
const uv_buf_t* bufs, |
||||
|
uv_handle_type pending, |
||||
|
void* ctx); |
||||
|
protected: |
||||
|
void OnFreeSession() override; |
||||
|
|
||||
|
ssize_t OnMaxFrameSizePadding(size_t frameLength, |
||||
|
size_t maxPayloadLen); |
||||
|
|
||||
|
ssize_t OnCallbackPadding(size_t frame, |
||||
|
size_t maxPayloadLen); |
||||
|
|
||||
|
bool HasGetPaddingCallback() override { |
||||
|
return padding_strategy_ == PADDING_STRATEGY_MAX || |
||||
|
padding_strategy_ == PADDING_STRATEGY_CALLBACK; |
||||
|
} |
||||
|
|
||||
|
ssize_t GetPadding(size_t frameLength, size_t maxPayloadLen) override { |
||||
|
if (padding_strategy_ == PADDING_STRATEGY_MAX) { |
||||
|
return OnMaxFrameSizePadding(frameLength, maxPayloadLen); |
||||
|
} |
||||
|
|
||||
|
CHECK_EQ(padding_strategy_, PADDING_STRATEGY_CALLBACK); |
||||
|
|
||||
|
return OnCallbackPadding(frameLength, maxPayloadLen); |
||||
|
} |
||||
|
|
||||
|
void OnHeaders(Nghttp2Stream* stream, |
||||
|
nghttp2_header_list* headers, |
||||
|
nghttp2_headers_category cat, |
||||
|
uint8_t flags) override; |
||||
|
void OnStreamClose(int32_t id, uint32_t code) override; |
||||
|
void Send(uv_buf_t* bufs, size_t total) override; |
||||
|
void OnDataChunk(Nghttp2Stream* stream, nghttp2_data_chunk_t* chunk) override; |
||||
|
void OnSettings(bool ack) override; |
||||
|
void OnPriority(int32_t stream, |
||||
|
int32_t parent, |
||||
|
int32_t weight, |
||||
|
int8_t exclusive) override; |
||||
|
void OnGoAway(int32_t lastStreamID, |
||||
|
uint32_t errorCode, |
||||
|
uint8_t* data, |
||||
|
size_t length) override; |
||||
|
void OnFrameError(int32_t id, uint8_t type, int error_code) override; |
||||
|
void OnTrailers(Nghttp2Stream* stream, |
||||
|
MaybeStackBuffer<nghttp2_nv>* trailers) override; |
||||
|
void AllocateSend(size_t recommended, uv_buf_t* buf) override; |
||||
|
|
||||
|
int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count, |
||||
|
uv_stream_t* send_handle) override; |
||||
|
|
||||
|
AsyncWrap* GetAsyncWrap() override { |
||||
|
return static_cast<AsyncWrap*>(this); |
||||
|
} |
||||
|
|
||||
|
void* Cast() override { |
||||
|
return reinterpret_cast<void*>(this); |
||||
|
} |
||||
|
|
||||
|
// Required for StreamBase
|
||||
|
bool IsAlive() override { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Required for StreamBase
|
||||
|
bool IsClosing() override { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Required for StreamBase
|
||||
|
int ReadStart() override { return 0; } |
||||
|
|
||||
|
// Required for StreamBase
|
||||
|
int ReadStop() override { return 0; } |
||||
|
|
||||
|
// Required for StreamBase
|
||||
|
int DoShutdown(ShutdownWrap* req_wrap) override { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
public: |
||||
|
void Consume(Local<External> external); |
||||
|
void Unconsume(); |
||||
|
|
||||
|
static void New(const FunctionCallbackInfo<Value>& args); |
||||
|
static void Consume(const FunctionCallbackInfo<Value>& args); |
||||
|
static void Unconsume(const FunctionCallbackInfo<Value>& args); |
||||
|
static void Destroy(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitSettings(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitRstStream(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitResponse(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitFile(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitRequest(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitPushPromise(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitPriority(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SendHeaders(const FunctionCallbackInfo<Value>& args); |
||||
|
static void ShutdownStream(const FunctionCallbackInfo<Value>& args); |
||||
|
static void StreamWrite(const FunctionCallbackInfo<Value>& args); |
||||
|
static void StreamReadStart(const FunctionCallbackInfo<Value>& args); |
||||
|
static void StreamReadStop(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SetNextStreamID(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SendShutdownNotice(const FunctionCallbackInfo<Value>& args); |
||||
|
static void SubmitGoaway(const FunctionCallbackInfo<Value>& args); |
||||
|
static void DestroyStream(const FunctionCallbackInfo<Value>& args); |
||||
|
|
||||
|
template <get_setting fn> |
||||
|
static void GetSettings(const FunctionCallbackInfo<Value>& args); |
||||
|
|
||||
|
size_t self_size() const override { |
||||
|
return sizeof(*this); |
||||
|
} |
||||
|
|
||||
|
char* stream_alloc() { |
||||
|
return *stream_buf_; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
StreamBase* stream_; |
||||
|
StreamResource::Callback<StreamResource::AllocCb> prev_alloc_cb_; |
||||
|
StreamResource::Callback<StreamResource::ReadCb> prev_read_cb_; |
||||
|
padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; |
||||
|
MaybeStackBuffer<char, kAllocBufferSize> stream_buf_; |
||||
|
}; |
||||
|
|
||||
|
class ExternalHeader : |
||||
|
public String::ExternalOneByteStringResource { |
||||
|
public: |
||||
|
explicit ExternalHeader(nghttp2_rcbuf* buf) |
||||
|
: buf_(buf), vec_(nghttp2_rcbuf_get_buf(buf)) { |
||||
|
} |
||||
|
|
||||
|
~ExternalHeader() override { |
||||
|
nghttp2_rcbuf_decref(buf_); |
||||
|
buf_ = nullptr; |
||||
|
} |
||||
|
|
||||
|
const char* data() const override { |
||||
|
return const_cast<const char*>(reinterpret_cast<char*>(vec_.base)); |
||||
|
} |
||||
|
|
||||
|
size_t length() const override { |
||||
|
return vec_.len; |
||||
|
} |
||||
|
|
||||
|
static Local<String> New(Isolate* isolate, nghttp2_rcbuf* buf) { |
||||
|
EscapableHandleScope scope(isolate); |
||||
|
nghttp2_vec vec = nghttp2_rcbuf_get_buf(buf); |
||||
|
if (vec.len == 0) { |
||||
|
nghttp2_rcbuf_decref(buf); |
||||
|
return scope.Escape(String::Empty(isolate)); |
||||
|
} |
||||
|
|
||||
|
ExternalHeader* h_str = new ExternalHeader(buf); |
||||
|
MaybeLocal<String> str = String::NewExternalOneByte(isolate, h_str); |
||||
|
isolate->AdjustAmountOfExternalAllocatedMemory(vec.len); |
||||
|
|
||||
|
if (str.IsEmpty()) { |
||||
|
delete h_str; |
||||
|
return scope.Escape(String::Empty(isolate)); |
||||
|
} |
||||
|
|
||||
|
return scope.Escape(str.ToLocalChecked()); |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
nghttp2_rcbuf* buf_; |
||||
|
nghttp2_vec vec_; |
||||
|
}; |
||||
|
|
||||
|
class Headers { |
||||
|
public: |
||||
|
Headers(Isolate* isolate, Local<Context> context, Local<Array> headers) { |
||||
|
headers_.AllocateSufficientStorage(headers->Length()); |
||||
|
Local<Value> item; |
||||
|
Local<Array> header; |
||||
|
|
||||
|
for (size_t n = 0; n < headers->Length(); n++) { |
||||
|
item = headers->Get(context, n).ToLocalChecked(); |
||||
|
CHECK(item->IsArray()); |
||||
|
header = item.As<Array>(); |
||||
|
Local<Value> key = header->Get(context, 0).ToLocalChecked(); |
||||
|
Local<Value> value = header->Get(context, 1).ToLocalChecked(); |
||||
|
CHECK(key->IsString()); |
||||
|
CHECK(value->IsString()); |
||||
|
size_t keylen = StringBytes::StorageSize(isolate, key, ASCII); |
||||
|
size_t valuelen = StringBytes::StorageSize(isolate, value, ASCII); |
||||
|
headers_[n].flags = NGHTTP2_NV_FLAG_NONE; |
||||
|
Local<Value> flag = header->Get(context, 2).ToLocalChecked(); |
||||
|
if (flag->BooleanValue(context).ToChecked()) |
||||
|
headers_[n].flags |= NGHTTP2_NV_FLAG_NO_INDEX; |
||||
|
uint8_t* buf = Malloc<uint8_t>(keylen + valuelen); |
||||
|
headers_[n].name = buf; |
||||
|
headers_[n].value = buf + keylen; |
||||
|
headers_[n].namelen = |
||||
|
StringBytes::Write(isolate, |
||||
|
reinterpret_cast<char*>(headers_[n].name), |
||||
|
keylen, key, ASCII); |
||||
|
headers_[n].valuelen = |
||||
|
StringBytes::Write(isolate, |
||||
|
reinterpret_cast<char*>(headers_[n].value), |
||||
|
valuelen, value, ASCII); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
~Headers() { |
||||
|
for (size_t n = 0; n < headers_.length(); n++) |
||||
|
free(headers_[n].name); |
||||
|
} |
||||
|
|
||||
|
nghttp2_nv* operator*() { |
||||
|
return *headers_; |
||||
|
} |
||||
|
|
||||
|
size_t length() const { |
||||
|
return headers_.length(); |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
MaybeStackBuffer<nghttp2_nv> headers_; |
||||
|
}; |
||||
|
|
||||
|
} // namespace http2
|
||||
|
} // namespace node
|
||||
|
|
||||
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
|
||||
|
#endif // SRC_NODE_HTTP2_H_
|
@ -0,0 +1,590 @@ |
|||||
|
#ifndef SRC_NODE_HTTP2_CORE_INL_H_ |
||||
|
#define SRC_NODE_HTTP2_CORE_INL_H_ |
||||
|
|
||||
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
||||
|
|
||||
|
#include "node_http2_core.h" |
||||
|
#include "node_internals.h" // arraysize |
||||
|
#include "freelist.h" |
||||
|
|
||||
|
namespace node { |
||||
|
namespace http2 { |
||||
|
|
||||
|
#define FREELIST_MAX 1024 |
||||
|
|
||||
|
#define LINKED_LIST_ADD(list, item) \ |
||||
|
do { \ |
||||
|
if (list ## _tail_ == nullptr) { \ |
||||
|
list ## _head_ = item; \ |
||||
|
list ## _tail_ = item; \ |
||||
|
} else { \ |
||||
|
list ## _tail_->next = item; \ |
||||
|
list ## _tail_ = item; \ |
||||
|
} \ |
||||
|
} while (0); |
||||
|
|
||||
|
extern Freelist<nghttp2_data_chunk_t, FREELIST_MAX> |
||||
|
data_chunk_free_list; |
||||
|
|
||||
|
extern Freelist<Nghttp2Stream, FREELIST_MAX> stream_free_list; |
||||
|
|
||||
|
extern Freelist<nghttp2_header_list, FREELIST_MAX> header_free_list; |
||||
|
|
||||
|
extern Freelist<nghttp2_data_chunks_t, FREELIST_MAX> |
||||
|
data_chunks_free_list; |
||||
|
|
||||
|
// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html
|
||||
|
inline void Nghttp2Session::SubmitShutdownNotice() { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: submitting shutdown notice\n", session_type_); |
||||
|
nghttp2_submit_shutdown_notice(session_); |
||||
|
} |
||||
|
|
||||
|
// Sends a SETTINGS frame on the current session
|
||||
|
// Note that this *should* send a SETTINGS frame even if niv == 0 and there
|
||||
|
// are no settings entries to send.
|
||||
|
inline int Nghttp2Session::SubmitSettings(const nghttp2_settings_entry iv[], |
||||
|
size_t niv) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: submitting settings, count: %d\n", |
||||
|
session_type_, niv); |
||||
|
return nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv); |
||||
|
} |
||||
|
|
||||
|
// Returns the Nghttp2Stream associated with the given id, or nullptr if none
|
||||
|
inline Nghttp2Stream* Nghttp2Session::FindStream(int32_t id) { |
||||
|
auto s = streams_.find(id); |
||||
|
if (s != streams_.end()) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: stream %d found\n", session_type_, id); |
||||
|
return s->second; |
||||
|
} else { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: stream %d not found\n", session_type_, id); |
||||
|
return nullptr; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Flushes any received queued chunks of data out to the JS layer
|
||||
|
inline void Nghttp2Stream::FlushDataChunks(bool done) { |
||||
|
while (data_chunks_head_ != nullptr) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: emitting data chunk\n", id_); |
||||
|
nghttp2_data_chunk_t* item = data_chunks_head_; |
||||
|
data_chunks_head_ = item->next; |
||||
|
// item will be passed to the Buffer instance and freed on gc
|
||||
|
session_->OnDataChunk(this, item); |
||||
|
} |
||||
|
data_chunks_tail_ = nullptr; |
||||
|
if (done) |
||||
|
session_->OnDataChunk(this, nullptr); |
||||
|
} |
||||
|
|
||||
|
// Passes all of the the chunks for a data frame out to the JS layer
|
||||
|
// The chunks are collected as the frame is being processed and sent out
|
||||
|
// to the JS side only when the frame is fully processed.
|
||||
|
inline void Nghttp2Session::HandleDataFrame(const nghttp2_frame* frame) { |
||||
|
int32_t id = frame->hd.stream_id; |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: handling data frame for stream %d\n", |
||||
|
session_type_, id); |
||||
|
Nghttp2Stream* stream = this->FindStream(id); |
||||
|
// If the stream does not exist, something really bad happened
|
||||
|
CHECK_NE(stream, nullptr); |
||||
|
bool done = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == |
||||
|
NGHTTP2_FLAG_END_STREAM; |
||||
|
stream->FlushDataChunks(done); |
||||
|
} |
||||
|
|
||||
|
// Passes all of the collected headers for a HEADERS frame out to the JS layer.
|
||||
|
// The headers are collected as the frame is being processed and sent out
|
||||
|
// to the JS side only when the frame is fully processed.
|
||||
|
inline void Nghttp2Session::HandleHeadersFrame(const nghttp2_frame* frame) { |
||||
|
int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? |
||||
|
frame->push_promise.promised_stream_id : frame->hd.stream_id; |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: handling headers frame for stream %d\n", |
||||
|
session_type_, id); |
||||
|
Nghttp2Stream* stream = FindStream(id); |
||||
|
// If the stream does not exist, something really bad happened
|
||||
|
CHECK_NE(stream, nullptr); |
||||
|
OnHeaders(stream, |
||||
|
stream->headers(), |
||||
|
stream->headers_category(), |
||||
|
frame->hd.flags); |
||||
|
stream->FreeHeaders(); |
||||
|
} |
||||
|
|
||||
|
// Notifies the JS layer that a PRIORITY frame has been received
|
||||
|
inline void Nghttp2Session::HandlePriorityFrame(const nghttp2_frame* frame) { |
||||
|
nghttp2_priority priority_frame = frame->priority; |
||||
|
int32_t id = frame->hd.stream_id; |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: handling priority frame for stream %d\n", |
||||
|
session_type_, id); |
||||
|
// Ignore the priority frame if stream ID is <= 0
|
||||
|
// This actually should never happen because nghttp2 should treat this as
|
||||
|
// an error condition that terminates the session.
|
||||
|
if (id > 0) { |
||||
|
nghttp2_priority_spec spec = priority_frame.pri_spec; |
||||
|
OnPriority(id, spec.stream_id, spec.weight, spec.exclusive); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Notifies the JS layer that a GOAWAY frame has been received
|
||||
|
inline void Nghttp2Session::HandleGoawayFrame(const nghttp2_frame* frame) { |
||||
|
nghttp2_goaway goaway_frame = frame->goaway; |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: handling goaway frame\n", session_type_); |
||||
|
|
||||
|
OnGoAway(goaway_frame.last_stream_id, |
||||
|
goaway_frame.error_code, |
||||
|
goaway_frame.opaque_data, |
||||
|
goaway_frame.opaque_data_len); |
||||
|
} |
||||
|
|
||||
|
// Prompts nghttp2 to flush the queue of pending data frames
|
||||
|
inline void Nghttp2Session::SendPendingData() { |
||||
|
const uint8_t* data; |
||||
|
ssize_t len = 0; |
||||
|
size_t ncopy = 0; |
||||
|
uv_buf_t buf; |
||||
|
AllocateSend(SEND_BUFFER_RECOMMENDED_SIZE, &buf); |
||||
|
while (nghttp2_session_want_write(session_)) { |
||||
|
len = nghttp2_session_mem_send(session_, &data); |
||||
|
CHECK_GE(len, 0); // If this is less than zero, we're out of memory
|
||||
|
// While len is greater than 0, send a chunk
|
||||
|
while (len > 0) { |
||||
|
ncopy = len; |
||||
|
if (ncopy > buf.len) |
||||
|
ncopy = buf.len; |
||||
|
memcpy(buf.base, data, ncopy); |
||||
|
Send(&buf, ncopy); |
||||
|
len -= ncopy; |
||||
|
CHECK_GE(len, 0); // This should never be less than zero
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Initialize the Nghttp2Session handle by creating and
|
||||
|
// assigning the Nghttp2Session instance and associated
|
||||
|
// uv_loop_t.
|
||||
|
inline int Nghttp2Session::Init(uv_loop_t* loop, |
||||
|
const nghttp2_session_type type, |
||||
|
nghttp2_option* options, |
||||
|
nghttp2_mem* mem) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: initializing session\n", type); |
||||
|
loop_ = loop; |
||||
|
session_type_ = type; |
||||
|
int ret = 0; |
||||
|
|
||||
|
nghttp2_session_callbacks* callbacks |
||||
|
= callback_struct_saved[HasGetPaddingCallback() ? 1 : 0].callbacks; |
||||
|
|
||||
|
nghttp2_option* opts; |
||||
|
if (options != nullptr) { |
||||
|
opts = options; |
||||
|
} else { |
||||
|
nghttp2_option_new(&opts); |
||||
|
} |
||||
|
|
||||
|
switch (type) { |
||||
|
case NGHTTP2_SESSION_SERVER: |
||||
|
ret = nghttp2_session_server_new3(&session_, |
||||
|
callbacks, |
||||
|
this, |
||||
|
opts, |
||||
|
mem); |
||||
|
break; |
||||
|
case NGHTTP2_SESSION_CLIENT: |
||||
|
ret = nghttp2_session_client_new3(&session_, |
||||
|
callbacks, |
||||
|
this, |
||||
|
opts, |
||||
|
mem); |
||||
|
break; |
||||
|
} |
||||
|
if (opts != options) { |
||||
|
nghttp2_option_del(opts); |
||||
|
} |
||||
|
|
||||
|
// For every node::Http2Session instance, there is a uv_prep_t handle
|
||||
|
// whose callback is triggered on every tick of the event loop. When
|
||||
|
// run, nghttp2 is prompted to send any queued data it may have stored.
|
||||
|
uv_prepare_init(loop_, &prep_); |
||||
|
uv_prepare_start(&prep_, [](uv_prepare_t* t) { |
||||
|
Nghttp2Session* session = ContainerOf(&Nghttp2Session::prep_, t); |
||||
|
session->SendPendingData(); |
||||
|
}); |
||||
|
// uv_unref(reinterpret_cast<uv_handle_t*>(&prep_));
|
||||
|
return ret; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
inline int Nghttp2Session::Free() { |
||||
|
assert(session_ != nullptr); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: freeing session\n", session_type_); |
||||
|
// Stop the loop
|
||||
|
uv_prepare_stop(&prep_); |
||||
|
auto PrepClose = [](uv_handle_t* handle) { |
||||
|
Nghttp2Session* session = |
||||
|
ContainerOf(&Nghttp2Session::prep_, |
||||
|
reinterpret_cast<uv_prepare_t*>(handle)); |
||||
|
|
||||
|
session->OnFreeSession(); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: session is free\n", |
||||
|
session->session_type_); |
||||
|
}; |
||||
|
uv_close(reinterpret_cast<uv_handle_t*>(&prep_), PrepClose); |
||||
|
|
||||
|
nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); |
||||
|
nghttp2_session_del(session_); |
||||
|
session_ = nullptr; |
||||
|
loop_ = nullptr; |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
// Write data received from the socket to the underlying nghttp2_session.
|
||||
|
inline ssize_t Nghttp2Session::Write(const uv_buf_t* bufs, unsigned int nbufs) { |
||||
|
size_t total = 0; |
||||
|
for (unsigned int n = 0; n < nbufs; n++) { |
||||
|
ssize_t ret = |
||||
|
nghttp2_session_mem_recv(session_, |
||||
|
reinterpret_cast<uint8_t*>(bufs[n].base), |
||||
|
bufs[n].len); |
||||
|
if (ret < 0) { |
||||
|
return ret; |
||||
|
} else { |
||||
|
total += ret; |
||||
|
} |
||||
|
} |
||||
|
SendPendingData(); |
||||
|
return total; |
||||
|
} |
||||
|
|
||||
|
inline void Nghttp2Session::AddStream(Nghttp2Stream* stream) { |
||||
|
streams_[stream->id()] = stream; |
||||
|
} |
||||
|
|
||||
|
// Removes a stream instance from this session
|
||||
|
inline void Nghttp2Session::RemoveStream(int32_t id) { |
||||
|
streams_.erase(id); |
||||
|
} |
||||
|
|
||||
|
// Implementation for Nghttp2Stream functions
|
||||
|
|
||||
|
inline Nghttp2Stream* Nghttp2Stream::Init( |
||||
|
int32_t id, |
||||
|
Nghttp2Session* session, |
||||
|
nghttp2_headers_category category) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: initializing stream\n", id); |
||||
|
Nghttp2Stream* stream = stream_free_list.pop(); |
||||
|
stream->ResetState(id, session, category); |
||||
|
session->AddStream(stream); |
||||
|
return stream; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// Resets the state of the stream instance to defaults
|
||||
|
inline void Nghttp2Stream::ResetState( |
||||
|
int32_t id, |
||||
|
Nghttp2Session* session, |
||||
|
nghttp2_headers_category category) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: resetting stream state\n", id); |
||||
|
session_ = session; |
||||
|
queue_head_ = nullptr; |
||||
|
queue_tail_ = nullptr; |
||||
|
data_chunks_head_ = nullptr; |
||||
|
data_chunks_tail_ = nullptr; |
||||
|
current_headers_head_ = nullptr; |
||||
|
current_headers_tail_ = nullptr; |
||||
|
current_headers_category_ = category; |
||||
|
flags_ = NGHTTP2_STREAM_FLAG_NONE; |
||||
|
id_ = id; |
||||
|
code_ = NGHTTP2_NO_ERROR; |
||||
|
prev_local_window_size_ = 65535; |
||||
|
queue_head_index_ = 0; |
||||
|
queue_head_offset_ = 0; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
inline void Nghttp2Stream::Destroy() { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_); |
||||
|
// Do nothing if this stream instance is already destroyed
|
||||
|
if (IsDestroyed() || IsDestroying()) |
||||
|
return; |
||||
|
flags_ |= NGHTTP2_STREAM_DESTROYING; |
||||
|
Nghttp2Session* session = this->session_; |
||||
|
|
||||
|
if (session != nullptr) { |
||||
|
// Remove this stream from the associated session
|
||||
|
session_->RemoveStream(this->id()); |
||||
|
session_ = nullptr; |
||||
|
} |
||||
|
|
||||
|
// Free any remaining incoming data chunks.
|
||||
|
while (data_chunks_head_ != nullptr) { |
||||
|
nghttp2_data_chunk_t* chunk = data_chunks_head_; |
||||
|
data_chunks_head_ = chunk->next; |
||||
|
delete[] chunk->buf.base; |
||||
|
data_chunk_free_list.push(chunk); |
||||
|
} |
||||
|
data_chunks_tail_ = nullptr; |
||||
|
|
||||
|
// Free any remaining outgoing data chunks.
|
||||
|
while (queue_head_ != nullptr) { |
||||
|
nghttp2_stream_write_queue* head = queue_head_; |
||||
|
queue_head_ = head->next; |
||||
|
head->cb(head->req, UV_ECANCELED); |
||||
|
delete head; |
||||
|
} |
||||
|
queue_tail_ = nullptr; |
||||
|
|
||||
|
// Free any remaining headers
|
||||
|
FreeHeaders(); |
||||
|
|
||||
|
// Return this stream instance to the freelist
|
||||
|
stream_free_list.push(this); |
||||
|
} |
||||
|
|
||||
|
inline void Nghttp2Stream::FreeHeaders() { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: freeing headers\n", id_); |
||||
|
while (current_headers_head_ != nullptr) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: freeing header item\n", id_); |
||||
|
nghttp2_header_list* item = current_headers_head_; |
||||
|
current_headers_head_ = item->next; |
||||
|
header_free_list.push(item); |
||||
|
} |
||||
|
current_headers_tail_ = nullptr; |
||||
|
} |
||||
|
|
||||
|
// Submit informational headers for a stream.
|
||||
|
inline int Nghttp2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: sending informational headers, count: %d\n", |
||||
|
id_, len); |
||||
|
CHECK_GT(len, 0); |
||||
|
return nghttp2_submit_headers(session_->session(), |
||||
|
NGHTTP2_FLAG_NONE, |
||||
|
id_, nullptr, |
||||
|
nva, len, nullptr); |
||||
|
} |
||||
|
|
||||
|
inline int Nghttp2Stream::SubmitPriority(nghttp2_priority_spec* prispec, |
||||
|
bool silent) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: sending priority spec\n", id_); |
||||
|
return silent ? |
||||
|
nghttp2_session_change_stream_priority(session_->session(), |
||||
|
id_, prispec) : |
||||
|
nghttp2_submit_priority(session_->session(), |
||||
|
NGHTTP2_FLAG_NONE, |
||||
|
id_, prispec); |
||||
|
} |
||||
|
|
||||
|
// Submit an RST_STREAM frame
|
||||
|
inline int Nghttp2Stream::SubmitRstStream(const uint32_t code) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: sending rst-stream, code: %d\n", id_, code); |
||||
|
session_->SendPendingData(); |
||||
|
return nghttp2_submit_rst_stream(session_->session(), |
||||
|
NGHTTP2_FLAG_NONE, |
||||
|
id_, |
||||
|
code); |
||||
|
} |
||||
|
|
||||
|
// Submit a push promise.
|
||||
|
inline int32_t Nghttp2Stream::SubmitPushPromise( |
||||
|
nghttp2_nv* nva, |
||||
|
size_t len, |
||||
|
Nghttp2Stream** assigned, |
||||
|
bool emptyPayload) { |
||||
|
CHECK_GT(len, 0); |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: sending push promise\n", id_); |
||||
|
int32_t ret = nghttp2_submit_push_promise(session_->session(), |
||||
|
NGHTTP2_FLAG_NONE, |
||||
|
id_, nva, len, |
||||
|
nullptr); |
||||
|
if (ret > 0) { |
||||
|
auto stream = Nghttp2Stream::Init(ret, session_); |
||||
|
if (emptyPayload) stream->Shutdown(); |
||||
|
if (assigned != nullptr) *assigned = stream; |
||||
|
} |
||||
|
return ret; |
||||
|
} |
||||
|
|
||||
|
// Initiate a response. If the nghttp2_stream is still writable by
|
||||
|
// the time this is called, then an nghttp2_data_provider will be
|
||||
|
// initialized, causing at least one (possibly empty) data frame to
|
||||
|
// be sent.
|
||||
|
inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva, |
||||
|
size_t len, |
||||
|
bool emptyPayload) { |
||||
|
CHECK_GT(len, 0); |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: submitting response\n", id_); |
||||
|
nghttp2_data_provider* provider = nullptr; |
||||
|
nghttp2_data_provider prov; |
||||
|
prov.source.ptr = this; |
||||
|
prov.read_callback = Nghttp2Session::OnStreamRead; |
||||
|
if (!emptyPayload && IsWritable()) |
||||
|
provider = &prov; |
||||
|
|
||||
|
return nghttp2_submit_response(session_->session(), id_, |
||||
|
nva, len, provider); |
||||
|
} |
||||
|
|
||||
|
// Initiate a response that contains data read from a file descriptor.
|
||||
|
inline int Nghttp2Stream::SubmitFile(int fd, nghttp2_nv* nva, size_t len) { |
||||
|
CHECK_GT(len, 0); |
||||
|
CHECK_GT(fd, 0); |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_); |
||||
|
nghttp2_data_provider prov; |
||||
|
prov.source.ptr = this; |
||||
|
prov.source.fd = fd; |
||||
|
prov.read_callback = Nghttp2Session::OnStreamReadFD; |
||||
|
|
||||
|
return nghttp2_submit_response(session_->session(), id_, |
||||
|
nva, len, &prov); |
||||
|
} |
||||
|
|
||||
|
// Initiate a request. If writable is true (the default), then
|
||||
|
// an nghttp2_data_provider will be initialized, causing at
|
||||
|
// least one (possibly empty) data frame to to be sent.
|
||||
|
inline int32_t Nghttp2Session::SubmitRequest( |
||||
|
nghttp2_priority_spec* prispec, |
||||
|
nghttp2_nv* nva, |
||||
|
size_t len, |
||||
|
Nghttp2Stream** assigned, |
||||
|
bool emptyPayload) { |
||||
|
CHECK_GT(len, 0); |
||||
|
DEBUG_HTTP2("Nghttp2Session: submitting request\n"); |
||||
|
nghttp2_data_provider* provider = nullptr; |
||||
|
nghttp2_data_provider prov; |
||||
|
prov.source.ptr = this; |
||||
|
prov.read_callback = OnStreamRead; |
||||
|
if (!emptyPayload) |
||||
|
provider = &prov; |
||||
|
int32_t ret = nghttp2_submit_request(session_, |
||||
|
prispec, nva, len, |
||||
|
provider, nullptr); |
||||
|
// Assign the Nghttp2Stream handle
|
||||
|
if (ret > 0) { |
||||
|
Nghttp2Stream* stream = Nghttp2Stream::Init(ret, this); |
||||
|
if (emptyPayload) stream->Shutdown(); |
||||
|
if (assigned != nullptr) *assigned = stream; |
||||
|
} |
||||
|
return ret; |
||||
|
} |
||||
|
|
||||
|
// Queue the given set of uv_but_t handles for writing to an
|
||||
|
// nghttp2_stream. The callback will be invoked once the chunks
|
||||
|
// of data have been flushed to the underlying nghttp2_session.
|
||||
|
// Note that this does *not* mean that the data has been flushed
|
||||
|
// to the socket yet.
|
||||
|
inline int Nghttp2Stream::Write(nghttp2_stream_write_t* req, |
||||
|
const uv_buf_t bufs[], |
||||
|
unsigned int nbufs, |
||||
|
nghttp2_stream_write_cb cb) { |
||||
|
if (!IsWritable()) { |
||||
|
if (cb != nullptr) |
||||
|
cb(req, UV_EOF); |
||||
|
return 0; |
||||
|
} |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: queuing buffers to send, count: %d\n", |
||||
|
id_, nbufs); |
||||
|
nghttp2_stream_write_queue* item = new nghttp2_stream_write_queue; |
||||
|
item->cb = cb; |
||||
|
item->req = req; |
||||
|
item->nbufs = nbufs; |
||||
|
item->bufs.AllocateSufficientStorage(nbufs); |
||||
|
req->handle = this; |
||||
|
req->item = item; |
||||
|
memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs)); |
||||
|
|
||||
|
if (queue_head_ == nullptr) { |
||||
|
queue_head_ = item; |
||||
|
queue_tail_ = item; |
||||
|
} else { |
||||
|
queue_tail_->next = item; |
||||
|
queue_tail_ = item; |
||||
|
} |
||||
|
nghttp2_session_resume_data(session_->session(), id_); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
inline void Nghttp2Stream::ReadStart() { |
||||
|
// Has no effect if IsReading() is true.
|
||||
|
if (IsReading()) |
||||
|
return; |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: start reading\n", id_); |
||||
|
if (IsPaused()) { |
||||
|
// If handle->reading is less than zero, read_start had never previously
|
||||
|
// been called. If handle->reading is zero, reading had started and read
|
||||
|
// stop had been previously called, meaning that the flow control window
|
||||
|
// has been explicitly set to zero. Reset the flow control window now to
|
||||
|
// restart the flow of data.
|
||||
|
nghttp2_session_set_local_window_size(session_->session(), |
||||
|
NGHTTP2_FLAG_NONE, |
||||
|
id_, |
||||
|
prev_local_window_size_); |
||||
|
} |
||||
|
flags_ |= NGHTTP2_STREAM_READ_START; |
||||
|
flags_ &= ~NGHTTP2_STREAM_READ_PAUSED; |
||||
|
|
||||
|
// Flush any queued data chunks immediately out to the JS layer
|
||||
|
FlushDataChunks(); |
||||
|
} |
||||
|
|
||||
|
inline void Nghttp2Stream::ReadStop() { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: stop reading\n", id_); |
||||
|
// Has no effect if IsReading() is false, which will happen if we either
|
||||
|
// have not started reading yet at all (NGHTTP2_STREAM_READ_START is not
|
||||
|
// set) or if we're already paused (NGHTTP2_STREAM_READ_PAUSED is set.
|
||||
|
if (!IsReading()) |
||||
|
return; |
||||
|
flags_ |= NGHTTP2_STREAM_READ_PAUSED; |
||||
|
|
||||
|
// When not reading, explicitly set the local window size to 0 so that
|
||||
|
// the peer does not keep sending data that has to be buffered
|
||||
|
int32_t ret = |
||||
|
nghttp2_session_get_stream_local_window_size(session_->session(), id_); |
||||
|
if (ret >= 0) |
||||
|
prev_local_window_size_ = ret; |
||||
|
nghttp2_session_set_local_window_size(session_->session(), |
||||
|
NGHTTP2_FLAG_NONE, |
||||
|
id_, 0); |
||||
|
} |
||||
|
|
||||
|
nghttp2_data_chunks_t::~nghttp2_data_chunks_t() { |
||||
|
for (unsigned int n = 0; n < nbufs; n++) { |
||||
|
free(buf[n].base); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Nghttp2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) { |
||||
|
nghttp2_session_callbacks_new(&callbacks); |
||||
|
nghttp2_session_callbacks_set_on_begin_headers_callback( |
||||
|
callbacks, OnBeginHeadersCallback); |
||||
|
nghttp2_session_callbacks_set_on_header_callback2( |
||||
|
callbacks, OnHeaderCallback); |
||||
|
nghttp2_session_callbacks_set_on_frame_recv_callback( |
||||
|
callbacks, OnFrameReceive); |
||||
|
nghttp2_session_callbacks_set_on_stream_close_callback( |
||||
|
callbacks, OnStreamClose); |
||||
|
nghttp2_session_callbacks_set_on_data_chunk_recv_callback( |
||||
|
callbacks, OnDataChunkReceived); |
||||
|
nghttp2_session_callbacks_set_on_frame_not_send_callback( |
||||
|
callbacks, OnFrameNotSent); |
||||
|
|
||||
|
// nghttp2_session_callbacks_set_on_invalid_frame_recv(
|
||||
|
// callbacks, OnInvalidFrameReceived);
|
||||
|
|
||||
|
#ifdef NODE_DEBUG_HTTP2 |
||||
|
nghttp2_session_callbacks_set_error_callback( |
||||
|
callbacks, OnNghttpError); |
||||
|
#endif |
||||
|
|
||||
|
if (kHasGetPaddingCallback) { |
||||
|
nghttp2_session_callbacks_set_select_padding_callback( |
||||
|
callbacks, OnSelectPadding); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Nghttp2Session::Callbacks::~Callbacks() { |
||||
|
nghttp2_session_callbacks_del(callbacks); |
||||
|
} |
||||
|
|
||||
|
} // namespace http2
|
||||
|
} // namespace node
|
||||
|
|
||||
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
|
||||
|
#endif // SRC_NODE_HTTP2_CORE_INL_H_
|
@ -0,0 +1,326 @@ |
|||||
|
#include "node_http2_core-inl.h" |
||||
|
|
||||
|
namespace node { |
||||
|
namespace http2 { |
||||
|
|
||||
|
#ifdef NODE_DEBUG_HTTP2 |
||||
|
int Nghttp2Session::OnNghttpError(nghttp2_session* session, |
||||
|
const char* message, |
||||
|
size_t len, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: Error '%.*s'\n", |
||||
|
handle->session_type_, len, message); |
||||
|
return 0; |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
// nghttp2 calls this at the beginning a new HEADERS or PUSH_PROMISE frame.
|
||||
|
// We use it to ensure that an Nghttp2Stream instance is allocated to store
|
||||
|
// the state.
|
||||
|
int Nghttp2Session::OnBeginHeadersCallback(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? |
||||
|
frame->push_promise.promised_stream_id : |
||||
|
frame->hd.stream_id; |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: beginning headers for stream %d\n", |
||||
|
handle->session_type_, id); |
||||
|
|
||||
|
Nghttp2Stream* stream = handle->FindStream(id); |
||||
|
if (stream == nullptr) { |
||||
|
Nghttp2Stream::Init(id, handle, frame->headers.cat); |
||||
|
} else { |
||||
|
stream->StartHeaders(frame->headers.cat); |
||||
|
} |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// nghttp2 calls this once for every header name-value pair in a HEADERS
|
||||
|
// or PUSH_PROMISE block. CONTINUATION frames are handled automatically
|
||||
|
// and transparently so we do not need to worry about those at all.
|
||||
|
int Nghttp2Session::OnHeaderCallback(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
nghttp2_rcbuf *name, |
||||
|
nghttp2_rcbuf *value, |
||||
|
uint8_t flags, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
int32_t id = (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? |
||||
|
frame->push_promise.promised_stream_id : |
||||
|
frame->hd.stream_id; |
||||
|
Nghttp2Stream* stream = handle->FindStream(id); |
||||
|
nghttp2_header_list* header = header_free_list.pop(); |
||||
|
header->name = name; |
||||
|
header->value = value; |
||||
|
nghttp2_rcbuf_incref(name); |
||||
|
nghttp2_rcbuf_incref(value); |
||||
|
LINKED_LIST_ADD(stream->current_headers, header); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// When nghttp2 has completely processed a frame, it calls OnFrameReceive.
|
||||
|
// It is our responsibility to delegate out from there. We can ignore most
|
||||
|
// control frames since nghttp2 will handle those for us.
|
||||
|
int Nghttp2Session::OnFrameReceive(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: complete frame received: type: %d\n", |
||||
|
handle->session_type_, frame->hd.type); |
||||
|
bool ack; |
||||
|
switch (frame->hd.type) { |
||||
|
case NGHTTP2_DATA: |
||||
|
handle->HandleDataFrame(frame); |
||||
|
break; |
||||
|
case NGHTTP2_PUSH_PROMISE: |
||||
|
case NGHTTP2_HEADERS: |
||||
|
handle->HandleHeadersFrame(frame); |
||||
|
break; |
||||
|
case NGHTTP2_SETTINGS: |
||||
|
ack = (frame->hd.flags & NGHTTP2_FLAG_ACK) == NGHTTP2_FLAG_ACK; |
||||
|
handle->OnSettings(ack); |
||||
|
break; |
||||
|
case NGHTTP2_PRIORITY: |
||||
|
handle->HandlePriorityFrame(frame); |
||||
|
break; |
||||
|
case NGHTTP2_GOAWAY: |
||||
|
handle->HandleGoawayFrame(frame); |
||||
|
break; |
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
int Nghttp2Session::OnFrameNotSent(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
int error_code, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: frame type %d was not sent, code: %d\n", |
||||
|
handle->session_type_, frame->hd.type, error_code); |
||||
|
// Do not report if the frame was not sent due to the session closing
|
||||
|
if (error_code != NGHTTP2_ERR_SESSION_CLOSING && |
||||
|
error_code != NGHTTP2_ERR_STREAM_CLOSED && |
||||
|
error_code != NGHTTP2_ERR_STREAM_CLOSING) |
||||
|
handle->OnFrameError(frame->hd.stream_id, frame->hd.type, error_code); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// Called when nghttp2 closes a stream, either in response to an RST_STREAM
|
||||
|
// frame or the stream closing naturally on it's own
|
||||
|
int Nghttp2Session::OnStreamClose(nghttp2_session *session, |
||||
|
int32_t id, |
||||
|
uint32_t code, |
||||
|
void *user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: stream %d closed, code: %d\n", |
||||
|
handle->session_type_, id, code); |
||||
|
Nghttp2Stream* stream = handle->FindStream(id); |
||||
|
// Intentionally ignore the callback if the stream does not exist
|
||||
|
if (stream != nullptr) |
||||
|
stream->Close(code); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// Called by nghttp2 multiple times while processing a DATA frame
|
||||
|
int Nghttp2Session::OnDataChunkReceived(nghttp2_session *session, |
||||
|
uint8_t flags, |
||||
|
int32_t id, |
||||
|
const uint8_t *data, |
||||
|
size_t len, |
||||
|
void *user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: buffering data chunk for stream %d, size: " |
||||
|
"%d, flags: %d\n", handle->session_type_, id, len, flags); |
||||
|
Nghttp2Stream* stream = handle->FindStream(id); |
||||
|
nghttp2_data_chunk_t* chunk = data_chunk_free_list.pop(); |
||||
|
chunk->buf = uv_buf_init(new char[len], len); |
||||
|
memcpy(chunk->buf.base, data, len); |
||||
|
if (stream->data_chunks_tail_ == nullptr) { |
||||
|
stream->data_chunks_head_ = |
||||
|
stream->data_chunks_tail_ = chunk; |
||||
|
} else { |
||||
|
stream->data_chunks_tail_->next = chunk; |
||||
|
stream->data_chunks_tail_ = chunk; |
||||
|
} |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// Called by nghttp2 when it needs to determine how much padding to apply
|
||||
|
// to a DATA or HEADERS frame
|
||||
|
ssize_t Nghttp2Session::OnSelectPadding(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
size_t maxPayloadLen, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
assert(handle->HasGetPaddingCallback()); |
||||
|
ssize_t padding = handle->GetPadding(frame->hd.length, maxPayloadLen); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: using padding, size: %d\n", |
||||
|
handle->session_type_, padding); |
||||
|
return padding; |
||||
|
} |
||||
|
|
||||
|
// Called by nghttp2 to collect the data while a file response is sent.
|
||||
|
// The buf is the DATA frame buffer that needs to be filled with at most
|
||||
|
// length bytes. flags is used to control what nghttp2 does next.
|
||||
|
ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session, |
||||
|
int32_t id, |
||||
|
uint8_t* buf, |
||||
|
size_t length, |
||||
|
uint32_t* flags, |
||||
|
nghttp2_data_source* source, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: reading outbound file data for stream %d\n", |
||||
|
handle->session_type_, id); |
||||
|
Nghttp2Stream* stream = handle->FindStream(id); |
||||
|
|
||||
|
int fd = source->fd; |
||||
|
int64_t offset = stream->fd_offset_; |
||||
|
ssize_t numchars; |
||||
|
|
||||
|
uv_buf_t data; |
||||
|
data.base = reinterpret_cast<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); |
||||
|
|
||||
|
// Close the stream with an error if reading fails
|
||||
|
if (numchars < 0) |
||||
|
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; |
||||
|
|
||||
|
// Update the read offset for the next read
|
||||
|
stream->fd_offset_ += numchars; |
||||
|
|
||||
|
// if numchars < length, assume that we are done.
|
||||
|
if (static_cast<size_t>(numchars) < length) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n", |
||||
|
handle->session_type_, id); |
||||
|
*flags |= NGHTTP2_DATA_FLAG_EOF; |
||||
|
// Sending trailers is not permitted with this provider.
|
||||
|
} |
||||
|
|
||||
|
return numchars; |
||||
|
} |
||||
|
|
||||
|
// Called by nghttp2 to collect the data to pack within a DATA frame.
|
||||
|
// The buf is the DATA frame buffer that needs to be filled with at most
|
||||
|
// length bytes. flags is used to control what nghttp2 does next.
|
||||
|
ssize_t Nghttp2Session::OnStreamRead(nghttp2_session* session, |
||||
|
int32_t id, |
||||
|
uint8_t* buf, |
||||
|
size_t length, |
||||
|
uint32_t* flags, |
||||
|
nghttp2_data_source* source, |
||||
|
void* user_data) { |
||||
|
Nghttp2Session* handle = static_cast<Nghttp2Session*>(user_data); |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: reading outbound data for stream %d\n", |
||||
|
handle->session_type_, id); |
||||
|
Nghttp2Stream* stream = handle->FindStream(id); |
||||
|
size_t remaining = length; |
||||
|
size_t offset = 0; |
||||
|
|
||||
|
// While there is data in the queue, copy data into buf until it is full.
|
||||
|
// There may be data left over, which will be sent the next time nghttp
|
||||
|
// calls this callback.
|
||||
|
while (stream->queue_head_ != nullptr) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: processing outbound data chunk\n", |
||||
|
handle->session_type_); |
||||
|
nghttp2_stream_write_queue* head = stream->queue_head_; |
||||
|
while (stream->queue_head_index_ < head->nbufs) { |
||||
|
if (remaining == 0) { |
||||
|
goto end; |
||||
|
} |
||||
|
|
||||
|
unsigned int n = stream->queue_head_index_; |
||||
|
// len is the number of bytes in head->bufs[n] that are yet to be written
|
||||
|
size_t len = head->bufs[n].len - stream->queue_head_offset_; |
||||
|
size_t bytes_to_write = len < remaining ? len : remaining; |
||||
|
memcpy(buf + offset, |
||||
|
head->bufs[n].base + stream->queue_head_offset_, |
||||
|
bytes_to_write); |
||||
|
offset += bytes_to_write; |
||||
|
remaining -= bytes_to_write; |
||||
|
if (bytes_to_write < len) { |
||||
|
stream->queue_head_offset_ += bytes_to_write; |
||||
|
} else { |
||||
|
stream->queue_head_index_++; |
||||
|
stream->queue_head_offset_ = 0; |
||||
|
} |
||||
|
} |
||||
|
stream->queue_head_offset_ = 0; |
||||
|
stream->queue_head_index_ = 0; |
||||
|
stream->queue_head_ = head->next; |
||||
|
head->cb(head->req, 0); |
||||
|
delete head; |
||||
|
} |
||||
|
stream->queue_tail_ = nullptr; |
||||
|
|
||||
|
end: |
||||
|
// If we are no longer writable and there is no more data in the queue,
|
||||
|
// then we need to set the NGHTTP2_DATA_FLAG_EOF flag.
|
||||
|
// If we are still writable but there is not yet any data to send, set the
|
||||
|
// NGHTTP2_ERR_DEFERRED flag. This will put the stream into a pending state
|
||||
|
// that will wait for data to become available.
|
||||
|
// If neither of these flags are set, then nghttp2 will call this callback
|
||||
|
// again to get the data for the next DATA frame.
|
||||
|
int writable = stream->queue_head_ != nullptr || stream->IsWritable(); |
||||
|
if (offset == 0 && writable && stream->queue_head_ == nullptr) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: deferring stream %d\n", |
||||
|
handle->session_type_, id); |
||||
|
return NGHTTP2_ERR_DEFERRED; |
||||
|
} |
||||
|
if (!writable) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: no more data for stream %d\n", |
||||
|
handle->session_type_, id); |
||||
|
*flags |= NGHTTP2_DATA_FLAG_EOF; |
||||
|
|
||||
|
// Only when we are done sending the last chunk of data do we check for
|
||||
|
// any trailing headers that are to be sent. This is the only opportunity
|
||||
|
// we have to make this check. If there are trailers, then the
|
||||
|
// NGHTTP2_DATA_FLAG_NO_END_STREAM flag must be set.
|
||||
|
MaybeStackBuffer<nghttp2_nv> trailers; |
||||
|
handle->OnTrailers(stream, &trailers); |
||||
|
if (trailers.length() > 0) { |
||||
|
DEBUG_HTTP2("Nghttp2Session %d: sending trailers for stream %d, " |
||||
|
"count: %d\n", handle->session_type_, id, trailers.length()); |
||||
|
*flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; |
||||
|
nghttp2_submit_trailer(session, |
||||
|
stream->id(), |
||||
|
*trailers, |
||||
|
trailers.length()); |
||||
|
} |
||||
|
for (size_t n = 0; n < trailers.length(); n++) { |
||||
|
free(trailers[n].name); |
||||
|
free(trailers[n].value); |
||||
|
} |
||||
|
} |
||||
|
assert(offset <= length); |
||||
|
return offset; |
||||
|
} |
||||
|
|
||||
|
Freelist<nghttp2_data_chunk_t, FREELIST_MAX> |
||||
|
data_chunk_free_list; |
||||
|
|
||||
|
Freelist<Nghttp2Stream, FREELIST_MAX> stream_free_list; |
||||
|
|
||||
|
Freelist<nghttp2_header_list, FREELIST_MAX> header_free_list; |
||||
|
|
||||
|
Freelist<nghttp2_data_chunks_t, FREELIST_MAX> |
||||
|
data_chunks_free_list; |
||||
|
|
||||
|
Nghttp2Session::Callbacks Nghttp2Session::callback_struct_saved[2] = { |
||||
|
Callbacks(false), |
||||
|
Callbacks(true) |
||||
|
}; |
||||
|
|
||||
|
} // namespace http2
|
||||
|
} // namespace node
|
@ -0,0 +1,465 @@ |
|||||
|
#ifndef SRC_NODE_HTTP2_CORE_H_ |
||||
|
#define SRC_NODE_HTTP2_CORE_H_ |
||||
|
|
||||
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
||||
|
|
||||
|
#include "util.h" |
||||
|
#include "util-inl.h" |
||||
|
#include "uv.h" |
||||
|
#include "nghttp2/nghttp2.h" |
||||
|
|
||||
|
#include <stdio.h> |
||||
|
#include <unordered_map> |
||||
|
|
||||
|
namespace node { |
||||
|
namespace http2 { |
||||
|
|
||||
|
#ifdef NODE_DEBUG_HTTP2 |
||||
|
|
||||
|
// Adapted from nghttp2 own debug printer
|
||||
|
static inline void _debug_vfprintf(const char *fmt, va_list args) { |
||||
|
vfprintf(stderr, fmt, args); |
||||
|
} |
||||
|
|
||||
|
void inline debug_vfprintf(const char *format, ...) { |
||||
|
va_list args; |
||||
|
va_start(args, format); |
||||
|
_debug_vfprintf(format, args); |
||||
|
va_end(args); |
||||
|
} |
||||
|
|
||||
|
#define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__); |
||||
|
#else |
||||
|
#define DEBUG_HTTP2(...) \ |
||||
|
do { \ |
||||
|
} while (0) |
||||
|
#endif |
||||
|
|
||||
|
class Nghttp2Session; |
||||
|
class Nghttp2Stream; |
||||
|
|
||||
|
struct nghttp2_stream_write_t; |
||||
|
struct nghttp2_data_chunk_t; |
||||
|
struct nghttp2_data_chunks_t; |
||||
|
|
||||
|
#define MAX_BUFFER_COUNT 10 |
||||
|
#define SEND_BUFFER_RECOMMENDED_SIZE 4096 |
||||
|
|
||||
|
enum nghttp2_session_type { |
||||
|
NGHTTP2_SESSION_SERVER, |
||||
|
NGHTTP2_SESSION_CLIENT |
||||
|
}; |
||||
|
|
||||
|
enum nghttp2_shutdown_flags { |
||||
|
NGHTTP2_SHUTDOWN_FLAG_GRACEFUL |
||||
|
}; |
||||
|
|
||||
|
enum nghttp2_stream_flags { |
||||
|
NGHTTP2_STREAM_FLAG_NONE = 0x0, |
||||
|
// Writable side has ended
|
||||
|
NGHTTP2_STREAM_FLAG_SHUT = 0x1, |
||||
|
// Reading has started
|
||||
|
NGHTTP2_STREAM_READ_START = 0x2, |
||||
|
// Reading is paused
|
||||
|
NGHTTP2_STREAM_READ_PAUSED = 0x4, |
||||
|
// Stream is closed
|
||||
|
NGHTTP2_STREAM_CLOSED = 0x8, |
||||
|
// Stream is destroyed
|
||||
|
NGHTTP2_STREAM_DESTROYED = 0x10, |
||||
|
// Stream is being destroyed
|
||||
|
NGHTTP2_STREAM_DESTROYING = 0x20 |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
// Callbacks
|
||||
|
typedef void (*nghttp2_stream_write_cb)( |
||||
|
nghttp2_stream_write_t* req, |
||||
|
int status); |
||||
|
|
||||
|
struct nghttp2_stream_write_queue { |
||||
|
unsigned int nbufs = 0; |
||||
|
nghttp2_stream_write_t* req = nullptr; |
||||
|
nghttp2_stream_write_cb cb = nullptr; |
||||
|
nghttp2_stream_write_queue* next = nullptr; |
||||
|
MaybeStackBuffer<uv_buf_t, MAX_BUFFER_COUNT> bufs; |
||||
|
}; |
||||
|
|
||||
|
struct nghttp2_header_list { |
||||
|
nghttp2_rcbuf* name = nullptr; |
||||
|
nghttp2_rcbuf* value = nullptr; |
||||
|
nghttp2_header_list* next = nullptr; |
||||
|
}; |
||||
|
|
||||
|
// Handle Types
|
||||
|
class Nghttp2Session { |
||||
|
public: |
||||
|
// Initializes the session instance
|
||||
|
inline int Init( |
||||
|
uv_loop_t*, |
||||
|
const nghttp2_session_type type = NGHTTP2_SESSION_SERVER, |
||||
|
nghttp2_option* options = nullptr, |
||||
|
nghttp2_mem* mem = nullptr); |
||||
|
|
||||
|
// Frees this session instance
|
||||
|
inline int Free(); |
||||
|
|
||||
|
// Returns the pointer to the identified stream, or nullptr if
|
||||
|
// the stream does not exist
|
||||
|
inline Nghttp2Stream* FindStream(int32_t id); |
||||
|
|
||||
|
// Submits a new request. If the request is a success, assigned
|
||||
|
// will be a pointer to the Nghttp2Stream instance assigned.
|
||||
|
// This only works if the session is a client session.
|
||||
|
inline int32_t SubmitRequest( |
||||
|
nghttp2_priority_spec* prispec, |
||||
|
nghttp2_nv* nva, |
||||
|
size_t len, |
||||
|
Nghttp2Stream** assigned = nullptr, |
||||
|
bool emptyPayload = true); |
||||
|
|
||||
|
// Submits a notice to the connected peer that the session is in the
|
||||
|
// process of shutting down.
|
||||
|
inline void SubmitShutdownNotice(); |
||||
|
|
||||
|
// Submits a SETTINGS frame to the connected peer.
|
||||
|
inline int SubmitSettings(const nghttp2_settings_entry iv[], size_t niv); |
||||
|
|
||||
|
// Write data to the session
|
||||
|
inline ssize_t Write(const uv_buf_t* bufs, unsigned int nbufs); |
||||
|
|
||||
|
// Returns the nghttp2 library session
|
||||
|
inline nghttp2_session* session() { return session_; } |
||||
|
|
||||
|
protected: |
||||
|
// Adds a stream instance to this session
|
||||
|
inline void AddStream(Nghttp2Stream* stream); |
||||
|
|
||||
|
// Removes a stream instance from this session
|
||||
|
inline void RemoveStream(int32_t id); |
||||
|
|
||||
|
virtual void Send(uv_buf_t* buf, |
||||
|
size_t length) {} |
||||
|
virtual void OnHeaders(Nghttp2Stream* stream, |
||||
|
nghttp2_header_list* headers, |
||||
|
nghttp2_headers_category cat, |
||||
|
uint8_t flags) {} |
||||
|
virtual void OnStreamClose(int32_t id, uint32_t code) {} |
||||
|
virtual void OnDataChunk(Nghttp2Stream* stream, |
||||
|
nghttp2_data_chunk_t* chunk) {} |
||||
|
virtual void OnSettings(bool ack) {} |
||||
|
virtual void OnPriority(int32_t id, |
||||
|
int32_t parent, |
||||
|
int32_t weight, |
||||
|
int8_t exclusive) {} |
||||
|
virtual void OnGoAway(int32_t lastStreamID, |
||||
|
uint32_t errorCode, |
||||
|
uint8_t* data, |
||||
|
size_t length) {} |
||||
|
virtual void OnFrameError(int32_t id, |
||||
|
uint8_t type, |
||||
|
int error_code) {} |
||||
|
virtual ssize_t GetPadding(size_t frameLength, |
||||
|
size_t maxFrameLength) { return 0; } |
||||
|
virtual void OnTrailers(Nghttp2Stream* stream, |
||||
|
MaybeStackBuffer<nghttp2_nv>* nva) {} |
||||
|
virtual void OnFreeSession() {} |
||||
|
virtual void AllocateSend(size_t suggested_size, uv_buf_t* buf) = 0; |
||||
|
|
||||
|
virtual bool HasGetPaddingCallback() { return false; } |
||||
|
|
||||
|
private: |
||||
|
inline void SendPendingData(); |
||||
|
inline void HandleHeadersFrame(const nghttp2_frame* frame); |
||||
|
inline void HandlePriorityFrame(const nghttp2_frame* frame); |
||||
|
inline void HandleDataFrame(const nghttp2_frame* frame); |
||||
|
inline void HandleGoawayFrame(const nghttp2_frame* frame); |
||||
|
|
||||
|
/* callbacks for nghttp2 */ |
||||
|
#ifdef NODE_DEBUG_HTTP2 |
||||
|
static int OnNghttpError(nghttp2_session* session, |
||||
|
const char* message, |
||||
|
size_t len, |
||||
|
void* user_data); |
||||
|
#endif |
||||
|
|
||||
|
static int OnBeginHeadersCallback(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
void* user_data); |
||||
|
static int OnHeaderCallback(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
nghttp2_rcbuf* name, |
||||
|
nghttp2_rcbuf* value, |
||||
|
uint8_t flags, |
||||
|
void* user_data); |
||||
|
static int OnFrameReceive(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
void* user_data); |
||||
|
static int OnFrameNotSent(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
int error_code, |
||||
|
void* user_data); |
||||
|
static int OnStreamClose(nghttp2_session* session, |
||||
|
int32_t id, |
||||
|
uint32_t code, |
||||
|
void* user_data); |
||||
|
static int OnDataChunkReceived(nghttp2_session* session, |
||||
|
uint8_t flags, |
||||
|
int32_t id, |
||||
|
const uint8_t *data, |
||||
|
size_t len, |
||||
|
void* user_data); |
||||
|
static ssize_t OnStreamReadFD(nghttp2_session* session, |
||||
|
int32_t id, |
||||
|
uint8_t* buf, |
||||
|
size_t length, |
||||
|
uint32_t* flags, |
||||
|
nghttp2_data_source* source, |
||||
|
void* user_data); |
||||
|
static ssize_t OnStreamRead(nghttp2_session* session, |
||||
|
int32_t id, |
||||
|
uint8_t* buf, |
||||
|
size_t length, |
||||
|
uint32_t* flags, |
||||
|
nghttp2_data_source* source, |
||||
|
void* user_data); |
||||
|
static ssize_t OnSelectPadding(nghttp2_session* session, |
||||
|
const nghttp2_frame* frame, |
||||
|
size_t maxPayloadLen, |
||||
|
void* user_data); |
||||
|
|
||||
|
struct Callbacks { |
||||
|
inline explicit Callbacks(bool kHasGetPaddingCallback); |
||||
|
inline ~Callbacks(); |
||||
|
|
||||
|
nghttp2_session_callbacks* callbacks; |
||||
|
}; |
||||
|
|
||||
|
/* Use callback_struct_saved[kHasGetPaddingCallback ? 1 : 0] */ |
||||
|
static Callbacks callback_struct_saved[2]; |
||||
|
|
||||
|
nghttp2_session* session_; |
||||
|
uv_loop_t* loop_; |
||||
|
uv_prepare_t prep_; |
||||
|
nghttp2_session_type session_type_; |
||||
|
std::unordered_map<int32_t, Nghttp2Stream*> streams_; |
||||
|
|
||||
|
friend class Nghttp2Stream; |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
|
||||
|
class Nghttp2Stream { |
||||
|
public: |
||||
|
static inline Nghttp2Stream* Init( |
||||
|
int32_t id, |
||||
|
Nghttp2Session* session, |
||||
|
nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS); |
||||
|
|
||||
|
inline ~Nghttp2Stream() { |
||||
|
CHECK_EQ(session_, nullptr); |
||||
|
CHECK_EQ(queue_head_, nullptr); |
||||
|
CHECK_EQ(queue_tail_, nullptr); |
||||
|
CHECK_EQ(data_chunks_head_, nullptr); |
||||
|
CHECK_EQ(data_chunks_tail_, nullptr); |
||||
|
CHECK_EQ(current_headers_head_, nullptr); |
||||
|
CHECK_EQ(current_headers_tail_, nullptr); |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: freed\n", id_); |
||||
|
} |
||||
|
|
||||
|
inline void FlushDataChunks(bool done = false); |
||||
|
|
||||
|
// Resets the state of the stream instance to defaults
|
||||
|
inline void ResetState( |
||||
|
int32_t id, |
||||
|
Nghttp2Session* session, |
||||
|
nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS); |
||||
|
|
||||
|
// Destroy this stream instance and free all held memory.
|
||||
|
// Note that this will free queued outbound and inbound
|
||||
|
// data chunks and inbound headers, so it's important not
|
||||
|
// to call this until those are fully consumed.
|
||||
|
//
|
||||
|
// Also note: this does not actually destroy the instance.
|
||||
|
// instead, it frees the held memory, removes the stream
|
||||
|
// from the parent session, and returns the instance to
|
||||
|
// the FreeList so that it can be reused.
|
||||
|
inline void Destroy(); |
||||
|
|
||||
|
// Returns true if this stream has been destroyed
|
||||
|
inline bool IsDestroyed() const { |
||||
|
return (flags_ & NGHTTP2_STREAM_DESTROYED) == NGHTTP2_STREAM_DESTROYED; |
||||
|
} |
||||
|
|
||||
|
inline bool IsDestroying() const { |
||||
|
return (flags_ & NGHTTP2_STREAM_DESTROYING) == NGHTTP2_STREAM_DESTROYING; |
||||
|
} |
||||
|
|
||||
|
// Queue outbound chunks of data to be sent on this stream
|
||||
|
inline int Write( |
||||
|
nghttp2_stream_write_t* req, |
||||
|
const uv_buf_t bufs[], |
||||
|
unsigned int nbufs, |
||||
|
nghttp2_stream_write_cb cb); |
||||
|
|
||||
|
// Initiate a response on this stream.
|
||||
|
inline int SubmitResponse(nghttp2_nv* nva, |
||||
|
size_t len, |
||||
|
bool emptyPayload = false); |
||||
|
|
||||
|
// Send data read from a file descriptor as the response on this stream.
|
||||
|
inline int SubmitFile(int fd, nghttp2_nv* nva, size_t len); |
||||
|
|
||||
|
// Submit informational headers for this stream
|
||||
|
inline int SubmitInfo(nghttp2_nv* nva, size_t len); |
||||
|
|
||||
|
// Submit a PRIORITY frame for this stream
|
||||
|
inline int SubmitPriority(nghttp2_priority_spec* prispec, |
||||
|
bool silent = false); |
||||
|
|
||||
|
// Submits an RST_STREAM frame using the given code
|
||||
|
inline int SubmitRstStream(const uint32_t code); |
||||
|
|
||||
|
// Submits a PUSH_PROMISE frame with this stream as the parent.
|
||||
|
inline int SubmitPushPromise( |
||||
|
nghttp2_nv* nva, |
||||
|
size_t len, |
||||
|
Nghttp2Stream** assigned = nullptr, |
||||
|
bool writable = true); |
||||
|
|
||||
|
// Marks the Writable side of the stream as being shutdown
|
||||
|
inline void Shutdown() { |
||||
|
flags_ |= NGHTTP2_STREAM_FLAG_SHUT; |
||||
|
nghttp2_session_resume_data(session_->session(), id_); |
||||
|
} |
||||
|
|
||||
|
// Returns true if this stream is writable.
|
||||
|
inline bool IsWritable() const { |
||||
|
return (flags_ & NGHTTP2_STREAM_FLAG_SHUT) == 0; |
||||
|
} |
||||
|
|
||||
|
// Start Reading. If there are queued data chunks, they are pushed into
|
||||
|
// the session to be emitted at the JS side
|
||||
|
inline void ReadStart(); |
||||
|
|
||||
|
// Stop/Pause Reading.
|
||||
|
inline void ReadStop(); |
||||
|
|
||||
|
// Returns true if reading is paused
|
||||
|
inline bool IsPaused() const { |
||||
|
return (flags_ & NGHTTP2_STREAM_READ_PAUSED) == NGHTTP2_STREAM_READ_PAUSED; |
||||
|
} |
||||
|
|
||||
|
// Returns true if this stream is in the reading state, which occurs when
|
||||
|
// the NGHTTP2_STREAM_READ_START flag has been set and the
|
||||
|
// NGHTTP2_STREAM_READ_PAUSED flag is *not* set.
|
||||
|
inline bool IsReading() const { |
||||
|
return ((flags_ & NGHTTP2_STREAM_READ_START) == NGHTTP2_STREAM_READ_START) |
||||
|
&& ((flags_ & NGHTTP2_STREAM_READ_PAUSED) == 0); |
||||
|
} |
||||
|
|
||||
|
inline void Close(int32_t code) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: closing with code %d\n", id_, code); |
||||
|
flags_ |= NGHTTP2_STREAM_CLOSED; |
||||
|
code_ = code; |
||||
|
session_->OnStreamClose(id_, code); |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: closed\n", id_); |
||||
|
} |
||||
|
|
||||
|
// Returns true if this stream has been closed either by receiving or
|
||||
|
// sending an RST_STREAM frame.
|
||||
|
inline bool IsClosed() const { |
||||
|
return (flags_ & NGHTTP2_STREAM_CLOSED) == NGHTTP2_STREAM_CLOSED; |
||||
|
} |
||||
|
|
||||
|
// Returns the RST_STREAM code used to close this stream
|
||||
|
inline int32_t code() const { |
||||
|
return code_; |
||||
|
} |
||||
|
|
||||
|
// Returns the stream identifier for this stream
|
||||
|
inline int32_t id() const { |
||||
|
return id_; |
||||
|
} |
||||
|
|
||||
|
inline nghttp2_header_list* headers() const { |
||||
|
return current_headers_head_; |
||||
|
} |
||||
|
|
||||
|
inline nghttp2_headers_category headers_category() const { |
||||
|
return current_headers_category_; |
||||
|
} |
||||
|
|
||||
|
inline void FreeHeaders(); |
||||
|
|
||||
|
void StartHeaders(nghttp2_headers_category category) { |
||||
|
DEBUG_HTTP2("Nghttp2Stream %d: starting headers, category: %d\n", |
||||
|
id_, category); |
||||
|
// We shouldn't be in the middle of a headers block already.
|
||||
|
// Something bad happened if this fails
|
||||
|
CHECK_EQ(current_headers_head_, nullptr); |
||||
|
CHECK_EQ(current_headers_tail_, nullptr); |
||||
|
current_headers_category_ = category; |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
// The Parent HTTP/2 Session
|
||||
|
Nghttp2Session* session_ = nullptr; |
||||
|
|
||||
|
// The Stream Identifier
|
||||
|
int32_t id_ = 0; |
||||
|
|
||||
|
// Internal state flags
|
||||
|
int flags_ = 0; |
||||
|
|
||||
|
// Outbound Data... This is the data written by the JS layer that is
|
||||
|
// waiting to be written out to the socket.
|
||||
|
nghttp2_stream_write_queue* queue_head_ = nullptr; |
||||
|
nghttp2_stream_write_queue* queue_tail_ = nullptr; |
||||
|
unsigned int queue_head_index_ = 0; |
||||
|
size_t queue_head_offset_ = 0; |
||||
|
size_t fd_offset_ = 0; |
||||
|
|
||||
|
// The Current Headers block... As headers are received for this stream,
|
||||
|
// they are temporarily stored here until the OnFrameReceived is called
|
||||
|
// signalling the end of the HEADERS frame
|
||||
|
nghttp2_header_list* current_headers_head_ = nullptr; |
||||
|
nghttp2_header_list* current_headers_tail_ = nullptr; |
||||
|
nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS; |
||||
|
|
||||
|
// Inbound Data... This is the data received via DATA frames for this stream.
|
||||
|
nghttp2_data_chunk_t* data_chunks_head_ = nullptr; |
||||
|
nghttp2_data_chunk_t* data_chunks_tail_ = nullptr; |
||||
|
|
||||
|
// The RST_STREAM code used to close this stream
|
||||
|
int32_t code_ = NGHTTP2_NO_ERROR; |
||||
|
|
||||
|
int32_t prev_local_window_size_ = 65535; |
||||
|
|
||||
|
friend class Nghttp2Session; |
||||
|
}; |
||||
|
|
||||
|
struct nghttp2_stream_write_t { |
||||
|
void* data; |
||||
|
int status; |
||||
|
Nghttp2Stream* handle; |
||||
|
nghttp2_stream_write_queue* item; |
||||
|
}; |
||||
|
|
||||
|
struct nghttp2_data_chunk_t { |
||||
|
uv_buf_t buf; |
||||
|
nghttp2_data_chunk_t* next = nullptr; |
||||
|
}; |
||||
|
|
||||
|
struct nghttp2_data_chunks_t { |
||||
|
unsigned int nbufs = 0; |
||||
|
uv_buf_t buf[MAX_BUFFER_COUNT]; |
||||
|
|
||||
|
inline ~nghttp2_data_chunks_t(); |
||||
|
}; |
||||
|
|
||||
|
} // namespace http2
|
||||
|
} // namespace node
|
||||
|
|
||||
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
|
||||
|
#endif // SRC_NODE_HTTP2_CORE_H_
|
Loading…
Reference in new issue