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