diff options
author | James M Snell <jasnell@gmail.com> | 2017-07-17 10:17:16 -0700 |
---|---|---|
committer | James M Snell <jasnell@gmail.com> | 2017-08-04 12:55:44 -0700 |
commit | e71e71b5138c3dfee080f4215dd957dc7a6cbdaf (patch) | |
tree | a46b77ae1bd423c0db3cf0f65ea370a721b67c24 /src | |
parent | 71a1876f6c3f2a7131c7019d63fea5c8fa740276 (diff) | |
download | android-node-v8-e71e71b5138c3dfee080f4215dd957dc7a6cbdaf.tar.gz android-node-v8-e71e71b5138c3dfee080f4215dd957dc7a6cbdaf.tar.bz2 android-node-v8-e71e71b5138c3dfee080f4215dd957dc7a6cbdaf.zip |
http2: introducing HTTP/2
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>
Diffstat (limited to 'src')
-rw-r--r-- | src/async-wrap.h | 2 | ||||
-rw-r--r-- | src/env-inl.h | 65 | ||||
-rw-r--r-- | src/env.h | 41 | ||||
-rw-r--r-- | src/freelist.h | 92 | ||||
-rw-r--r-- | src/node.cc | 13 | ||||
-rw-r--r-- | src/node.h | 19 | ||||
-rw-r--r-- | src/node_config.cc | 3 | ||||
-rw-r--r-- | src/node_crypto_bio.cc | 1 | ||||
-rw-r--r-- | src/node_http2.cc | 1326 | ||||
-rw-r--r-- | src/node_http2.h | 572 | ||||
-rw-r--r-- | src/node_http2_core-inl.h | 590 | ||||
-rw-r--r-- | src/node_http2_core.cc | 326 | ||||
-rw-r--r-- | src/node_http2_core.h | 465 | ||||
-rw-r--r-- | src/node_internals.h | 3 | ||||
-rw-r--r-- | src/stream_base.cc | 1 | ||||
-rw-r--r-- | src/stream_base.h | 11 |
16 files changed, 3528 insertions, 2 deletions
diff --git a/src/async-wrap.h b/src/async-wrap.h index 79332d9a42..a02356e845 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -40,6 +40,8 @@ namespace node { V(FSREQWRAP) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ + V(HTTP2SESSION) \ + V(HTTP2SESSIONSHUTDOWNWRAP) \ V(HTTPPARSER) \ V(JSSTREAM) \ V(PIPECONNECTWRAP) \ diff --git a/src/env-inl.h b/src/env-inl.h index 6753979ef5..021d50841d 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -302,6 +302,7 @@ inline Environment::Environment(IsolateData* isolate_data, #endif handle_cleanup_waiting_(0), http_parser_buffer_(nullptr), + http2_socket_buffer_(nullptr), fs_stats_field_array_(nullptr), context_(context->GetIsolate(), context) { // We'll be creating new objects so make sure we've entered the context. @@ -328,6 +329,12 @@ inline Environment::~Environment() { delete[] heap_statistics_buffer_; delete[] heap_space_statistics_buffer_; delete[] http_parser_buffer_; + delete[] http2_socket_buffer_; + delete[] http2_settings_buffer_; + delete[] http2_options_buffer_; + delete[] http2_session_state_buffer_; + delete[] http2_stream_state_buffer_; + delete[] http2_padding_buffer_; } inline v8::Isolate* Environment::isolate() const { @@ -468,6 +475,55 @@ inline void Environment::set_heap_space_statistics_buffer(double* pointer) { heap_space_statistics_buffer_ = pointer; } +inline uint32_t* Environment::http2_settings_buffer() const { + CHECK_NE(http2_settings_buffer_, nullptr); + return http2_settings_buffer_; +} + +inline void Environment::set_http2_settings_buffer(uint32_t* pointer) { + CHECK_EQ(http2_settings_buffer_, nullptr); // Should be set only once + http2_settings_buffer_ = pointer; +} + +inline uint32_t* Environment::http2_options_buffer() const { + CHECK_NE(http2_options_buffer_, nullptr); + return http2_options_buffer_; +} + +inline void Environment::set_http2_options_buffer(uint32_t* pointer) { + CHECK_EQ(http2_options_buffer_, nullptr); // Should be set only once + http2_options_buffer_ = pointer; +} + +inline double* Environment::http2_session_state_buffer() const { + CHECK_NE(http2_session_state_buffer_, nullptr); + return http2_session_state_buffer_; +} + +inline void Environment::set_http2_session_state_buffer(double* pointer) { + CHECK_EQ(http2_session_state_buffer_, nullptr); + http2_session_state_buffer_ = pointer; +} + +inline double* Environment::http2_stream_state_buffer() const { + CHECK_NE(http2_stream_state_buffer_, nullptr); + return http2_stream_state_buffer_; +} + +inline void Environment::set_http2_stream_state_buffer(double* pointer) { + CHECK_EQ(http2_stream_state_buffer_, nullptr); + http2_stream_state_buffer_ = pointer; +} + +inline uint32_t* Environment::http2_padding_buffer() const { + CHECK_NE(http2_padding_buffer_, nullptr); + return http2_padding_buffer_; +} + +inline void Environment::set_http2_padding_buffer(uint32_t* pointer) { + CHECK_EQ(http2_padding_buffer_, nullptr); + http2_padding_buffer_ = pointer; +} inline char* Environment::http_parser_buffer() const { return http_parser_buffer_; @@ -487,6 +543,15 @@ inline void Environment::set_fs_stats_field_array(double* fields) { fs_stats_field_array_ = fields; } +inline char* Environment::http2_socket_buffer() const { + return http2_socket_buffer_; +} + +inline void Environment::set_http2_socket_buffer(char* buffer) { + CHECK_EQ(http2_socket_buffer_, nullptr); // Should be set only once. + http2_socket_buffer_ = buffer; +} + inline IsolateData* Environment::isolate_data() const { return isolate_data_; } @@ -104,6 +104,7 @@ namespace node { V(configurable_string, "configurable") \ V(cwd_string, "cwd") \ V(dest_string, "dest") \ + V(destroy_string, "destroy") \ V(detached_string, "detached") \ V(disposed_string, "_disposed") \ V(dns_a_string, "A") \ @@ -117,11 +118,13 @@ namespace node { V(dns_srv_string, "SRV") \ V(dns_txt_string, "TXT") \ V(domain_string, "domain") \ + V(emit_string, "emit") \ V(emitting_top_level_domain_error_string, "_emittingTopLevelDomainError") \ V(exchange_string, "exchange") \ V(enumerable_string, "enumerable") \ V(idle_string, "idle") \ V(irq_string, "irq") \ + V(enablepush_string, "enablePush") \ V(encoding_string, "encoding") \ V(enter_string, "enter") \ V(entries_string, "entries") \ @@ -148,8 +151,11 @@ namespace node { V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \ V(gid_string, "gid") \ V(handle_string, "handle") \ + V(heap_total_string, "heapTotal") \ + V(heap_used_string, "heapUsed") \ V(homedir_string, "homedir") \ V(hostmaster_string, "hostmaster") \ + V(id_string, "id") \ V(ignore_string, "ignore") \ V(immediate_callback_string, "_immediateCallback") \ V(infoaccess_string, "infoAccess") \ @@ -174,6 +180,7 @@ namespace node { V(netmask_string, "netmask") \ V(nice_string, "nice") \ V(nsname_string, "nsname") \ + V(nexttick_string, "nextTick") \ V(ocsp_request_string, "OCSPRequest") \ V(onchange_string, "onchange") \ V(onclienthello_string, "onclienthello") \ @@ -182,19 +189,27 @@ namespace node { V(ondone_string, "ondone") \ V(onerror_string, "onerror") \ V(onexit_string, "onexit") \ + V(onframeerror_string, "onframeerror") \ + V(ongetpadding_string, "ongetpadding") \ V(onhandshakedone_string, "onhandshakedone") \ V(onhandshakestart_string, "onhandshakestart") \ + V(onheaders_string, "onheaders") \ V(onmessage_string, "onmessage") \ V(onnewsession_string, "onnewsession") \ V(onnewsessiondone_string, "onnewsessiondone") \ V(onocspresponse_string, "onocspresponse") \ + V(ongoawaydata_string, "ongoawaydata") \ + V(onpriority_string, "onpriority") \ V(onread_string, "onread") \ V(onreadstart_string, "onreadstart") \ V(onreadstop_string, "onreadstop") \ V(onselect_string, "onselect") \ + V(onsettings_string, "onsettings") \ V(onshutdown_string, "onshutdown") \ V(onsignal_string, "onsignal") \ V(onstop_string, "onstop") \ + V(onstreamclose_string, "onstreamclose") \ + V(ontrailers_string, "ontrailers") \ V(onwrite_string, "onwrite") \ V(output_string, "output") \ V(order_string, "order") \ @@ -234,6 +249,7 @@ namespace node { V(stack_string, "stack") \ V(status_string, "status") \ V(stdio_string, "stdio") \ + V(stream_string, "stream") \ V(subject_string, "subject") \ V(subjectaltname_string, "subjectaltname") \ V(sys_string, "sys") \ @@ -262,7 +278,7 @@ namespace node { V(write_host_object_string, "_writeHostObject") \ V(write_queue_size_string, "writeQueueSize") \ V(x_forwarded_string, "x-forwarded-for") \ - V(zero_return_string, "ZERO_RETURN") \ + V(zero_return_string, "ZERO_RETURN") #define ENVIRONMENT_STRONG_PERSISTENT_PROPERTIES(V) \ V(as_external, v8::External) \ @@ -579,8 +595,25 @@ class Environment { inline double* heap_space_statistics_buffer() const; inline void set_heap_space_statistics_buffer(double* pointer); + inline uint32_t* http2_settings_buffer() const; + inline void set_http2_settings_buffer(uint32_t* pointer); + + inline uint32_t* http2_options_buffer() const; + inline void set_http2_options_buffer(uint32_t* pointer); + + inline double* http2_session_state_buffer() const; + inline void set_http2_session_state_buffer(double* pointer); + + inline double* http2_stream_state_buffer() const; + inline void set_http2_stream_state_buffer(double* pointer); + + inline uint32_t* http2_padding_buffer() const; + inline void set_http2_padding_buffer(uint32_t* pointer); + inline char* http_parser_buffer() const; inline void set_http_parser_buffer(char* buffer); + inline char* http2_socket_buffer() const; + inline void set_http2_socket_buffer(char* buffer); inline double* fs_stats_field_array() const; inline void set_fs_stats_field_array(double* fields); @@ -686,8 +719,14 @@ class Environment { double* heap_statistics_buffer_ = nullptr; double* heap_space_statistics_buffer_ = nullptr; + uint32_t* http2_settings_buffer_ = nullptr; + uint32_t* http2_options_buffer_ = nullptr; + double* http2_session_state_buffer_ = nullptr; + double* http2_stream_state_buffer_ = nullptr; + uint32_t* http2_padding_buffer_ = nullptr; char* http_parser_buffer_; + char* http2_socket_buffer_; double* fs_stats_field_array_; diff --git a/src/freelist.h b/src/freelist.h new file mode 100644 index 0000000000..7dff56a35d --- /dev/null +++ b/src/freelist.h @@ -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_ diff --git a/src/node.cc b/src/node.cc index 741b5dea82..5b1b9cadd9 100644 --- a/src/node.cc +++ b/src/node.cc @@ -59,6 +59,7 @@ #include "env-inl.h" #include "handle_wrap.h" #include "http_parser.h" +#include "nghttp2/nghttp2ver.h" #include "req-wrap.h" #include "req-wrap-inl.h" #include "string_bytes.h" @@ -232,6 +233,9 @@ std::string config_warning_file; // NOLINT(runtime/string) // that is used by lib/internal/bootstrap_node.js bool config_expose_internals = false; +// Set in node.cc by ParseArgs when --expose-http2 is used. +bool config_expose_http2 = false; + bool v8_initialized = false; bool linux_at_secure = false; @@ -3210,6 +3214,10 @@ void SetupProcessObject(Environment* env, "modules", FIXED_ONE_BYTE_STRING(env->isolate(), node_modules_version)); + READONLY_PROPERTY(versions, + "nghttp2", + FIXED_ONE_BYTE_STRING(env->isolate(), NGHTTP2_VERSION)); + // process._promiseRejectEvent Local<Object> promiseRejectEvent = Object::New(env->isolate()); READONLY_DONT_ENUM_PROPERTY(process, @@ -3648,6 +3656,7 @@ static void PrintHelp() { " --abort-on-uncaught-exception\n" " aborting instead of exiting causes a\n" " core file to be generated for analysis\n" + " --expose-http2 enable experimental HTTP2 support\n" " --trace-warnings show stack traces on process warnings\n" " --redirect-warnings=file\n" " write warnings to file instead of\n" @@ -3768,6 +3777,7 @@ static void CheckIfAllowedInEnv(const char* exe, bool is_env, "--throw-deprecation", "--no-warnings", "--napi-modules", + "--expose-http2", "--trace-warnings", "--redirect-warnings", "--trace-sync-io", @@ -3965,6 +3975,9 @@ static void ParseArgs(int* argc, } else if (strcmp(arg, "--expose-internals") == 0 || strcmp(arg, "--expose_internals") == 0) { config_expose_internals = true; + } else if (strcmp(arg, "--expose-http2") == 0 || + strcmp(arg, "--expose_http2") == 0) { + config_expose_http2 = true; } else if (strcmp(arg, "-") == 0) { break; } else if (strcmp(arg, "--") == 0) { diff --git a/src/node.h b/src/node.h index b1d179bd16..59728685f7 100644 --- a/src/node.h +++ b/src/node.h @@ -253,6 +253,25 @@ NODE_EXTERN void RunAtExit(Environment* env); } \ while (0) +#define NODE_DEFINE_HIDDEN_CONSTANT(target, constant) \ + do { \ + v8::Isolate* isolate = target->GetIsolate(); \ + v8::Local<v8::Context> context = isolate->GetCurrentContext(); \ + v8::Local<v8::String> constant_name = \ + v8::String::NewFromUtf8(isolate, #constant); \ + v8::Local<v8::Number> constant_value = \ + v8::Number::New(isolate, static_cast<double>(constant)); \ + v8::PropertyAttribute constant_attributes = \ + static_cast<v8::PropertyAttribute>(v8::ReadOnly | \ + v8::DontDelete | \ + v8::DontEnum); \ + (target)->DefineOwnProperty(context, \ + constant_name, \ + constant_value, \ + constant_attributes).FromJust(); \ + } \ + while (0) + // Used to be a macro, hence the uppercase name. inline void NODE_SET_METHOD(v8::Local<v8::Template> recv, const char* name, diff --git a/src/node_config.cc b/src/node_config.cc index b309171282..041e18f6b7 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -88,6 +88,9 @@ static void InitConfig(Local<Object> target, if (config_expose_internals) READONLY_BOOLEAN_PROPERTY("exposeInternals"); + + if (config_expose_http2) + READONLY_BOOLEAN_PROPERTY("exposeHTTP2"); } // InitConfig } // namespace node diff --git a/src/node_crypto_bio.cc b/src/node_crypto_bio.cc index 00fd0b420c..4c84973f75 100644 --- a/src/node_crypto_bio.cc +++ b/src/node_crypto_bio.cc @@ -357,7 +357,6 @@ size_t NodeBIO::IndexOf(char delim, size_t limit) { return max; } - void NodeBIO::Write(const char* data, size_t size) { size_t offset = 0; size_t left = size; diff --git a/src/node_http2.cc b/src/node_http2.cc new file mode 100644 index 0000000000..5ad1352cc1 --- /dev/null +++ b/src/node_http2.cc @@ -0,0 +1,1326 @@ +#include "node.h" +#include "node_buffer.h" +#include "node_http2.h" + +namespace node { + +using v8::ArrayBuffer; +using v8::Boolean; +using v8::Context; +using v8::Function; +using v8::Integer; +using v8::Undefined; + +namespace http2 { + +enum Http2SettingsIndex { + IDX_SETTINGS_HEADER_TABLE_SIZE, + IDX_SETTINGS_ENABLE_PUSH, + IDX_SETTINGS_INITIAL_WINDOW_SIZE, + IDX_SETTINGS_MAX_FRAME_SIZE, + IDX_SETTINGS_MAX_CONCURRENT_STREAMS, + IDX_SETTINGS_MAX_HEADER_LIST_SIZE, + IDX_SETTINGS_COUNT +}; + +enum Http2SessionStateIndex { + IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE, + IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH, + IDX_SESSION_STATE_NEXT_STREAM_ID, + IDX_SESSION_STATE_LOCAL_WINDOW_SIZE, + IDX_SESSION_STATE_LAST_PROC_STREAM_ID, + IDX_SESSION_STATE_REMOTE_WINDOW_SIZE, + IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE, + IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE, + IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE, + IDX_SESSION_STATE_COUNT +}; + +enum Http2StreamStateIndex { + IDX_STREAM_STATE, + IDX_STREAM_STATE_WEIGHT, + IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT, + IDX_STREAM_STATE_LOCAL_CLOSE, + IDX_STREAM_STATE_REMOTE_CLOSE, + IDX_STREAM_STATE_LOCAL_WINDOW_SIZE, + IDX_STREAM_STATE_COUNT +}; + +enum Http2OptionsIndex { + IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE, + IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS, + IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH, + IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS, + IDX_OPTIONS_PADDING_STRATEGY, + IDX_OPTIONS_FLAGS +}; + +Http2Options::Http2Options(Environment* env) { + nghttp2_option_new(&options_); + + uint32_t* buffer = env->http2_options_buffer(); + uint32_t flags = buffer[IDX_OPTIONS_FLAGS]; + + if ((flags & (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE)) == + (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE)) { + SetMaxDeflateDynamicTableSize( + buffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE]); + } + + if ((flags & (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS)) == + (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS)) { + SetMaxReservedRemoteStreams( + buffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS]); + } + + if ((flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)) == + (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH)) { + SetMaxSendHeaderBlockLength( + buffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH]); + } + + SetPeerMaxConcurrentStreams(100); // Recommended default + if ((flags & (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)) == + (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)) { + SetPeerMaxConcurrentStreams( + buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]); + } + + if ((flags & (1 << IDX_OPTIONS_PADDING_STRATEGY)) == + (1 << IDX_OPTIONS_PADDING_STRATEGY)) { + SetPaddingStrategy(buffer[IDX_OPTIONS_PADDING_STRATEGY]); + } +} + +inline void CopyHeaders(Isolate* isolate, + Local<Context> context, + MaybeStackBuffer<nghttp2_nv>* list, + Local<Array> headers) { + Local<Value> item; + Local<Array> header; + + for (size_t n = 0; n < headers->Length(); n++) { + item = headers->Get(context, n).ToLocalChecked(); + 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); + nghttp2_nv& nv = (*list)[n]; + nv.flags = NGHTTP2_NV_FLAG_NONE; + Local<Value> flag = header->Get(context, 2).ToLocalChecked(); + if (flag->BooleanValue(context).ToChecked()) + nv.flags |= NGHTTP2_NV_FLAG_NO_INDEX; + nv.name = Malloc<uint8_t>(keylen); + nv.value = Malloc<uint8_t>(valuelen); + nv.namelen = + StringBytes::Write(isolate, + reinterpret_cast<char*>(nv.name), + keylen, key, ASCII); + nv.valuelen = + StringBytes::Write(isolate, + reinterpret_cast<char*>(nv.value), + valuelen, value, ASCII); + } +} + +inline void FreeHeaders(MaybeStackBuffer<nghttp2_nv>* list) { + for (size_t n = 0; n < list->length(); n++) { + free((*list)[n].name); + free((*list)[n].value); + } +} + +void Http2Session::OnFreeSession() { + ::delete this; +} + +ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2("Http2Session: using max frame size padding\n"); + return maxPayloadLen; +} + +ssize_t Http2Session::OnCallbackPadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2("Http2Session: using callback padding\n"); + Isolate* isolate = env()->isolate(); + Local<Context> context = env()->context(); + + HandleScope handle_scope(isolate); + Context::Scope context_scope(context); + + if (object()->Has(context, env()->ongetpadding_string()).FromJust()) { + uint32_t* buffer = env()->http2_padding_buffer(); + buffer[0] = frameLen; + buffer[1] = maxPayloadLen; + MakeCallback(env()->ongetpadding_string(), 0, nullptr); + uint32_t retval = buffer[2]; + retval = retval <= maxPayloadLen ? retval : maxPayloadLen; + retval = retval >= frameLen ? retval : frameLen; + CHECK_GE(retval, frameLen); + CHECK_LE(retval, maxPayloadLen); + return retval; + } + return frameLen; +} + +void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + nghttp2_session* s = session->session(); + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: setting next stream id to %d\n", id); + nghttp2_session_set_next_stream_id(s, id); +} + +void HttpErrorString(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + uint32_t val = args[0]->Uint32Value(env->context()).ToChecked(); + args.GetReturnValue().Set( + OneByteString(env->isolate(), nghttp2_strerror(val))); +} + +// Serializes the settings object into a Buffer instance that +// would be suitable, for instance, for creating the Base64 +// output for an HTTP2-Settings header field. +void PackSettings(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + HandleScope scope(env->isolate()); + + std::vector<nghttp2_settings_entry> entries; + entries.reserve(6); + + uint32_t* const buffer = env->http2_settings_buffer(); + uint32_t flags = buffer[IDX_SETTINGS_COUNT]; + + if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) == + (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { + DEBUG_HTTP2("Setting header table size: %d\n", + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) == + (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { + DEBUG_HTTP2("Setting max concurrent streams: %d\n", + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]); + entries.push_back({NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) == + (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { + DEBUG_HTTP2("Setting max frame size: %d\n", + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_FRAME_SIZE, + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) == + (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { + DEBUG_HTTP2("Setting initial window size: %d\n", + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) == + (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { + DEBUG_HTTP2("Setting max header list size: %d\n", + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) == + (1 << IDX_SETTINGS_ENABLE_PUSH)) { + DEBUG_HTTP2("Setting enable push: %d\n", + buffer[IDX_SETTINGS_ENABLE_PUSH]); + entries.push_back({NGHTTP2_SETTINGS_ENABLE_PUSH, + buffer[IDX_SETTINGS_ENABLE_PUSH]}); + } + + const size_t len = entries.size() * 6; + MaybeStackBuffer<char> buf(len); + ssize_t ret = + nghttp2_pack_settings_payload( + reinterpret_cast<uint8_t*>(*buf), len, &entries[0], entries.size()); + if (ret >= 0) { + args.GetReturnValue().Set( + Buffer::Copy(env, *buf, len).ToLocalChecked()); + } +} + +// Used to fill in the spec defined initial values for each setting. +void RefreshDefaultSettings(const FunctionCallbackInfo<Value>& args) { + DEBUG_HTTP2("Http2Session: refreshing default settings\n"); + Environment* env = Environment::GetCurrent(args); + uint32_t* const buffer = env->http2_settings_buffer(); + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = + DEFAULT_SETTINGS_HEADER_TABLE_SIZE; + buffer[IDX_SETTINGS_ENABLE_PUSH] = + DEFAULT_SETTINGS_ENABLE_PUSH; + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = + DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE; + buffer[IDX_SETTINGS_MAX_FRAME_SIZE] = + DEFAULT_SETTINGS_MAX_FRAME_SIZE; + buffer[IDX_SETTINGS_COUNT] = + (1 << IDX_SETTINGS_HEADER_TABLE_SIZE) | + (1 << IDX_SETTINGS_ENABLE_PUSH) | + (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE) | + (1 << IDX_SETTINGS_MAX_FRAME_SIZE); +} + +template <get_setting fn> +void RefreshSettings(const FunctionCallbackInfo<Value>& args) { + DEBUG_HTTP2("Http2Session: refreshing settings for session\n"); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsObject()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>()); + Environment* env = session->env(); + nghttp2_session* s = session->session(); + + uint32_t* const buffer = env->http2_settings_buffer(); + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = + fn(s, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE); + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = + fn(s, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = + fn(s, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE); + buffer[IDX_SETTINGS_MAX_FRAME_SIZE] = + fn(s, NGHTTP2_SETTINGS_MAX_FRAME_SIZE); + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = + fn(s, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE); + buffer[IDX_SETTINGS_ENABLE_PUSH] = + fn(s, NGHTTP2_SETTINGS_ENABLE_PUSH); +} + +// Used to fill in the spec defined initial values for each setting. +void RefreshSessionState(const FunctionCallbackInfo<Value>& args) { + DEBUG_HTTP2("Http2Session: refreshing session state\n"); + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsObject()); + Environment* env = Environment::GetCurrent(args); + double* const buffer = env->http2_session_state_buffer(); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>()); + nghttp2_session* s = session->session(); + + buffer[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_effective_local_window_size(s); + buffer[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH] = + nghttp2_session_get_effective_recv_data_length(s); + buffer[IDX_SESSION_STATE_NEXT_STREAM_ID] = + nghttp2_session_get_next_stream_id(s); + buffer[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_local_window_size(s); + buffer[IDX_SESSION_STATE_LAST_PROC_STREAM_ID] = + nghttp2_session_get_last_proc_stream_id(s); + buffer[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE] = + nghttp2_session_get_remote_window_size(s); + buffer[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE] = + nghttp2_session_get_outbound_queue_size(s); + buffer[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE] = + nghttp2_session_get_hd_deflate_dynamic_table_size(s); + buffer[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE] = + nghttp2_session_get_hd_inflate_dynamic_table_size(s); +} + +void RefreshStreamState(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 2); + CHECK(args[0]->IsObject()); + CHECK(args[1]->IsNumber()); + int32_t id = args[1]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: refreshing stream %d state\n", id); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As<Object>()); + nghttp2_session* s = session->session(); + Nghttp2Stream* stream; + + double* const buffer = env->http2_stream_state_buffer(); + + if ((stream = session->FindStream(id)) == nullptr) { + buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; + buffer[IDX_STREAM_STATE_WEIGHT] = + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; + return; + } + nghttp2_stream* str = + nghttp2_session_find_stream(s, stream->id()); + + if (str == nullptr) { + buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; + buffer[IDX_STREAM_STATE_WEIGHT] = + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; + } else { + buffer[IDX_STREAM_STATE] = + nghttp2_stream_get_state(str); + buffer[IDX_STREAM_STATE_WEIGHT] = + nghttp2_stream_get_weight(str); + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + nghttp2_stream_get_sum_dependency_weight(str); + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + nghttp2_session_get_stream_local_close(s, id); + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + nghttp2_session_get_stream_remote_close(s, id); + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_stream_local_window_size(s, id); + } +} + +void Http2Session::New(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + + int val = args[0]->IntegerValue(env->context()).ToChecked(); + nghttp2_session_type type = static_cast<nghttp2_session_type>(val); + DEBUG_HTTP2("Http2Session: creating a session of type: %d\n", type); + new Http2Session(env, args.This(), type); +} + + +// Capture the stream that this session will use to send and receive data +void Http2Session::Consume(const FunctionCallbackInfo<Value>& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsExternal()); + session->Consume(args[0].As<External>()); +} + +void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) { + DEBUG_HTTP2("Http2Session: destroying session\n"); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->Unconsume(); + session->Free(); +} + +void Http2Session::SubmitPriority(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local<Context> context = env->context(); + + nghttp2_priority_spec spec; + int32_t id = args[0]->Int32Value(context).ToChecked(); + int32_t parent = args[1]->Int32Value(context).ToChecked(); + int32_t weight = args[2]->Int32Value(context).ToChecked(); + bool exclusive = args[3]->BooleanValue(context).ToChecked(); + bool silent = args[4]->BooleanValue(context).ToChecked(); + DEBUG_HTTP2("Http2Session: submitting priority for stream %d: " + "parent: %d, weight: %d, exclusive: %d, silent: %d\n", + id, parent, weight, exclusive, silent); + CHECK_GT(id, 0); + CHECK_GE(parent, 0); + CHECK_GE(weight, 0); + + Nghttp2Stream* stream; + if (!(stream = session->FindStream(id))) { + // invalid stream + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + nghttp2_priority_spec_init(&spec, parent, weight, exclusive ? 1 : 0); + + args.GetReturnValue().Set(stream->SubmitPriority(&spec, silent)); +} + +void Http2Session::SubmitSettings(const FunctionCallbackInfo<Value>& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + Environment* env = session->env(); + + uint32_t* const buffer = env->http2_settings_buffer(); + uint32_t flags = buffer[IDX_SETTINGS_COUNT]; + + std::vector<nghttp2_settings_entry> entries; + entries.reserve(6); + + if ((flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) == + (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { + DEBUG_HTTP2("Setting header table size: %d\n", + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, + buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) == + (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { + DEBUG_HTTP2("Setting max concurrent streams: %d\n", + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]); + entries.push_back({NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, + buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) == + (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { + DEBUG_HTTP2("Setting max frame size: %d\n", + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_FRAME_SIZE, + buffer[IDX_SETTINGS_MAX_FRAME_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) == + (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { + DEBUG_HTTP2("Setting initial window size: %d\n", + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, + buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) == + (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { + DEBUG_HTTP2("Setting max header list size: %d\n", + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]); + entries.push_back({NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, + buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]}); + } + + if ((flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) == + (1 << IDX_SETTINGS_ENABLE_PUSH)) { + DEBUG_HTTP2("Setting enable push: %d\n", + buffer[IDX_SETTINGS_ENABLE_PUSH]); + entries.push_back({NGHTTP2_SETTINGS_ENABLE_PUSH, + buffer[IDX_SETTINGS_ENABLE_PUSH]}); + } + + if (entries.size() > 0) { + args.GetReturnValue().Set( + session->Nghttp2Session::SubmitSettings(&entries[0], entries.size())); + } else { + args.GetReturnValue().Set( + session->Nghttp2Session::SubmitSettings(nullptr, 0)); + } +} + +void Http2Session::SubmitRstStream(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Local<Context> context = env->context(); + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsNumber()); + + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + int32_t id = args[0]->Int32Value(context).ToChecked(); + uint32_t code = args[1]->Uint32Value(context).ToChecked(); + + Nghttp2Stream* stream; + if (!(stream = session->FindStream(id))) { + // invalid stream + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + DEBUG_HTTP2("Http2Session: sending rst_stream for stream %d, code: %d\n", + id, code); + args.GetReturnValue().Set(stream->SubmitRstStream(code)); +} + +void Http2Session::SubmitRequest(const FunctionCallbackInfo<Value>& args) { + // args[0] Array of headers + // args[1] endStream boolean + // args[2] parentStream ID (for priority spec) + // args[3] weight (for priority spec) + // args[4] exclusive boolean (for priority spec) + CHECK(args[0]->IsArray()); + + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + + Local<Array> headers = args[0].As<Array>(); + bool endStream = args[1]->BooleanValue(context).ToChecked(); + int32_t parent = args[2]->Int32Value(context).ToChecked(); + int32_t weight = args[3]->Int32Value(context).ToChecked(); + bool exclusive = args[4]->BooleanValue(context).ToChecked(); + + DEBUG_HTTP2("Http2Session: submitting request: headers: %d, end-stream: %d, " + "parent: %d, weight: %d, exclusive: %d\n", headers->Length(), + endStream, parent, weight, exclusive); + + nghttp2_priority_spec prispec; + nghttp2_priority_spec_init(&prispec, parent, weight, exclusive ? 1 : 0); + + Headers list(isolate, context, headers); + + int32_t ret = session->Nghttp2Session::SubmitRequest(&prispec, + *list, list.length(), + nullptr, endStream); + DEBUG_HTTP2("Http2Session: request submitted, response: %d\n", ret); + args.GetReturnValue().Set(ret); +} + +void Http2Session::SubmitResponse(const FunctionCallbackInfo<Value>& args) { + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsArray()); + + Http2Session* session; + Nghttp2Stream* stream; + + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + + int32_t id = args[0]->Int32Value(context).ToChecked(); + Local<Array> headers = args[1].As<Array>(); + bool endStream = args[2]->BooleanValue(context).ToChecked(); + + DEBUG_HTTP2("Http2Session: submitting response for stream %d: headers: %d, " + "end-stream: %d\n", id, headers->Length(), endStream); + + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + args.GetReturnValue().Set( + stream->SubmitResponse(*list, list.length(), endStream)); +} + +void Http2Session::SubmitFile(const FunctionCallbackInfo<Value>& args) { + CHECK(args[0]->IsNumber()); // Stream ID + CHECK(args[1]->IsNumber()); // File Descriptor + CHECK(args[2]->IsArray()); // Headers + + Http2Session* session; + Nghttp2Stream* stream; + + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + + int32_t id = args[0]->Int32Value(context).ToChecked(); + int fd = args[1]->Int32Value(context).ToChecked(); + Local<Array> headers = args[2].As<Array>(); + + DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, " + "end-stream: %d\n", fd, id, headers->Length()); + + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length())); +} + +void Http2Session::SendHeaders(const FunctionCallbackInfo<Value>& args) { + CHECK(args[0]->IsNumber()); + CHECK(args[1]->IsArray()); + + Http2Session* session; + Nghttp2Stream* stream; + + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + Local<Array> headers = args[1].As<Array>(); + + DEBUG_HTTP2("Http2Session: sending informational headers for stream %d, " + "count: %d\n", id, headers->Length()); + + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); +} + +void Http2Session::ShutdownStream(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsNumber()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Nghttp2Stream* stream; + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: shutting down stream %d\n", id); + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->Shutdown(); +} + + +void Http2Session::StreamReadStart(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsNumber()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Nghttp2Stream* stream; + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->ReadStart(); +} + + +void Http2Session::StreamReadStop(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsNumber()); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Nghttp2Stream* stream; + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->ReadStop(); +} + +void Http2Session::SendShutdownNotice( + const FunctionCallbackInfo<Value>& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->SubmitShutdownNotice(); +} + +void Http2Session::SubmitGoaway(const FunctionCallbackInfo<Value>& args) { + Http2Session* session; + Environment* env = Environment::GetCurrent(args); + Local<Context> context = env->context(); + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + uint32_t errorCode = args[0]->Uint32Value(context).ToChecked(); + int32_t lastStreamID = args[1]->Int32Value(context).ToChecked(); + Local<Value> opaqueData = args[2]; + + uint8_t* data = NULL; + size_t length = 0; + + if (opaqueData->BooleanValue(context).ToChecked()) { + THROW_AND_RETURN_UNLESS_BUFFER(env, opaqueData); + SPREAD_BUFFER_ARG(opaqueData, buf); + data = reinterpret_cast<uint8_t*>(buf_data); + length = buf_length; + } + + DEBUG_HTTP2("Http2Session: initiating immediate shutdown. " + "last-stream-id: %d, code: %d, opaque-data: %d\n", + lastStreamID, errorCode, length); + int status = nghttp2_submit_goaway(session->session(), + NGHTTP2_FLAG_NONE, + lastStreamID, + errorCode, + data, length); + args.GetReturnValue().Set(status); +} + +void Http2Session::DestroyStream(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsNumber()); + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + DEBUG_HTTP2("Http2Session: destroy stream %d\n", id); + Nghttp2Stream* stream; + if (!(stream = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + stream->Destroy(); +} + +void Http2Session::SubmitPushPromise(const FunctionCallbackInfo<Value>& args) { + Http2Session* session; + Environment* env = Environment::GetCurrent(args); + Local<Context> context = env->context(); + Isolate* isolate = env->isolate(); + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + CHECK(args[0]->IsNumber()); // parent stream ID + CHECK(args[1]->IsArray()); // headers array + + Nghttp2Stream* parent; + int32_t id = args[0]->Int32Value(context).ToChecked(); + Local<Array> headers = args[1].As<Array>(); + bool endStream = args[2]->BooleanValue(context).ToChecked(); + + DEBUG_HTTP2("Http2Session: submitting push promise for stream %d: " + "end-stream: %d, headers: %d\n", id, endStream, + headers->Length()); + + if (!(parent = session->FindStream(id))) { + return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + } + + Headers list(isolate, context, headers); + + int32_t ret = parent->SubmitPushPromise(*list, list.length(), + nullptr, endStream); + DEBUG_HTTP2("Http2Session: push promise submitted, ret: %d\n", ret); + args.GetReturnValue().Set(ret); +} + +int Http2Session::DoWrite(WriteWrap* req_wrap, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) { + Environment* env = req_wrap->env(); + Local<Object> req_wrap_obj = req_wrap->object(); + Local<Context> context = env->context(); + + Nghttp2Stream* stream; + { + Local<Value> val = + req_wrap_obj->Get(context, env->stream_string()).ToLocalChecked(); + int32_t id = val->Int32Value(context).ToChecked(); + if (!val->IsNumber() || !(stream = FindStream(id))) { + // invalid stream + req_wrap->Dispatched(); + req_wrap->Done(0); + return NGHTTP2_ERR_INVALID_STREAM_ID; + } + } + + nghttp2_stream_write_t* req = new nghttp2_stream_write_t; + req->data = req_wrap; + + auto AfterWrite = [](nghttp2_stream_write_t* req, int status) { + WriteWrap* wrap = static_cast<WriteWrap*>(req->data); + wrap->Done(status); + delete req; + }; + req_wrap->Dispatched(); + stream->Write(req, bufs, count, AfterWrite); + return 0; +} + +void Http2Session::AllocateSend(size_t recommended, uv_buf_t* buf) { + buf->base = stream_alloc(); + buf->len = kAllocBufferSize; +} + +void Http2Session::Send(uv_buf_t* buf, size_t length) { + if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) { + return; + } + HandleScope scope(env()->isolate()); + + auto AfterWrite = [](WriteWrap* req_wrap, int status) { + req_wrap->Dispose(); + }; + Local<Object> req_wrap_obj = + env()->write_wrap_constructor_function() + ->NewInstance(env()->context()).ToLocalChecked(); + WriteWrap* write_req = WriteWrap::New(env(), + req_wrap_obj, + this, + AfterWrite); + + uv_buf_t actual = uv_buf_init(buf->base, length); + if (stream_->DoWrite(write_req, &actual, 1, nullptr)) { + write_req->Dispose(); + } +} + +void Http2Session::OnTrailers(Nghttp2Stream* stream, + MaybeStackBuffer<nghttp2_nv>* trailers) { + DEBUG_HTTP2("Http2Session: prompting for trailers on stream %d\n", + stream->id()); + Local<Context> context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + + if (object()->Has(context, env()->ontrailers_string()).FromJust()) { + Local<Value> argv[1] = { + Integer::New(isolate, stream->id()) + }; + + Local<Value> ret = MakeCallback(env()->ontrailers_string(), + arraysize(argv), argv); + if (!ret.IsEmpty()) { + if (ret->IsArray()) { + Local<Array> headers = ret.As<Array>(); + if (headers->Length() > 0) { + trailers->AllocateSufficientStorage(headers->Length()); + CopyHeaders(isolate, context, trailers, headers); + } + } + } + } +} + +void Http2Session::OnHeaders(Nghttp2Stream* stream, + nghttp2_header_list* headers, + nghttp2_headers_category cat, + uint8_t flags) { + Local<Context> context = env()->context(); + Isolate* isolate = env()->isolate(); + Context::Scope context_scope(context); + HandleScope scope(isolate); + Local<String> name_str; + Local<String> value_str; + + Local<Array> holder = Array::New(isolate); + Local<Function> fn = env()->push_values_to_array_function(); + Local<Value> argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2]; + + // The headers are passed in above as a linked list of nghttp2_header_list + // structs. The following converts that into a JS array with the structure: + // [name1, value1, name2, value2, name3, value3, name3, value4] and so on. + // That array is passed up to the JS layer and converted into an Object form + // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it + // this way for performance reasons (it's faster to generate and pass an + // array than it is to generate and pass the object). + do { + size_t j = 0; + while (headers != nullptr && j < arraysize(argv) / 2) { + nghttp2_header_list* item = headers; + // The header name and value are passed as external one-byte strings + name_str = ExternalHeader::New(isolate, item->name); + value_str = ExternalHeader::New(isolate, item->value); + argv[j * 2] = name_str; + argv[j * 2 + 1] = value_str; + headers = item->next; + j++; + } + // For performance, we pass name and value pairs to array.protototype.push + // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no + // more items to push. + if (j > 0) { + fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked(); + } + } while (headers != nullptr); + + if (object()->Has(context, env()->onheaders_string()).FromJust()) { + Local<Value> argv[4] = { + Integer::New(isolate, stream->id()), + Integer::New(isolate, cat), + Integer::New(isolate, flags), + holder + }; + MakeCallback(env()->onheaders_string(), arraysize(argv), argv); + } +} + + +void Http2Session::OnStreamClose(int32_t id, uint32_t code) { + Isolate* isolate = env()->isolate(); + Local<Context> context = env()->context(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onstreamclose_string()).FromJust()) { + Local<Value> argv[2] = { + Integer::New(isolate, id), + Integer::NewFromUnsigned(isolate, code) + }; + MakeCallback(env()->onstreamclose_string(), arraysize(argv), argv); + } +} + +void FreeDataChunk(char* data, void* hint) { + nghttp2_data_chunk_t* item = reinterpret_cast<nghttp2_data_chunk_t*>(hint); + delete[] data; + data_chunk_free_list.push(item); +} + +void Http2Session::OnDataChunk( + Nghttp2Stream* stream, + nghttp2_data_chunk_t* chunk) { + Isolate* isolate = env()->isolate(); + Local<Context> context = env()->context(); + HandleScope scope(isolate); + Local<Object> obj = Object::New(isolate); + obj->Set(context, + env()->id_string(), + Integer::New(isolate, stream->id())).FromJust(); + ssize_t len = -1; + Local<Object> buf; + if (chunk != nullptr) { + len = chunk->buf.len; + buf = Buffer::New(isolate, + chunk->buf.base, len, + FreeDataChunk, + chunk).ToLocalChecked(); + } + EmitData(len, buf, obj); +} + +void Http2Session::OnSettings(bool ack) { + Local<Context> context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onsettings_string()).FromJust()) { + Local<Value> argv[1] = { Boolean::New(isolate, ack) }; + MakeCallback(env()->onsettings_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnFrameError(int32_t id, uint8_t type, int error_code) { + Local<Context> context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onframeerror_string()).FromJust()) { + Local<Value> argv[3] = { + Integer::New(isolate, id), + Integer::New(isolate, type), + Integer::New(isolate, error_code) + }; + MakeCallback(env()->onframeerror_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnPriority(int32_t stream, + int32_t parent, + int32_t weight, + int8_t exclusive) { + Local<Context> context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->onpriority_string()).FromJust()) { + Local<Value> argv[4] = { + Integer::New(isolate, stream), + Integer::New(isolate, parent), + Integer::New(isolate, weight), + Boolean::New(isolate, exclusive) + }; + MakeCallback(env()->onpriority_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnGoAway(int32_t lastStreamID, + uint32_t errorCode, + uint8_t* data, + size_t length) { + Local<Context> context = env()->context(); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Context::Scope context_scope(context); + if (object()->Has(context, env()->ongoawaydata_string()).FromJust()) { + Local<Value> argv[3] = { + Integer::NewFromUnsigned(isolate, errorCode), + Integer::New(isolate, lastStreamID), + Undefined(isolate) + }; + + if (length > 0) { + argv[2] = Buffer::Copy(isolate, + reinterpret_cast<char*>(data), + length).ToLocalChecked(); + } + + MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); + } +} + +void Http2Session::OnStreamAllocImpl(size_t suggested_size, + uv_buf_t* buf, + void* ctx) { + Http2Session* session = static_cast<Http2Session*>(ctx); + buf->base = session->stream_alloc(); + buf->len = kAllocBufferSize; +} + + +void Http2Session::OnStreamReadImpl(ssize_t nread, + const uv_buf_t* bufs, + uv_handle_type pending, + void* ctx) { + Http2Session* session = static_cast<Http2Session*>(ctx); + if (nread < 0) { + uv_buf_t tmp_buf; + tmp_buf.base = nullptr; + tmp_buf.len = 0; + session->prev_read_cb_.fn(nread, + &tmp_buf, + pending, + session->prev_read_cb_.ctx); + return; + } + if (nread > 0) { + // Only pass data on if nread > 0 + uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) }; + ssize_t ret = session->Write(buf, 1); + if (ret < 0) { + DEBUG_HTTP2("Http2Session: fatal error receiving data: %d\n", ret); + nghttp2_session_terminate_session(session->session(), + NGHTTP2_PROTOCOL_ERROR); + } + } +} + + +void Http2Session::Consume(Local<External> external) { + DEBUG_HTTP2("Http2Session: consuming socket\n"); + CHECK(prev_alloc_cb_.is_empty()); + StreamBase* stream = static_cast<StreamBase*>(external->Value()); + CHECK_NE(stream, nullptr); + stream->Consume(); + stream_ = stream; + prev_alloc_cb_ = stream->alloc_cb(); + prev_read_cb_ = stream->read_cb(); + stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this }); + stream->set_read_cb({ Http2Session::OnStreamReadImpl, this }); +} + + +void Http2Session::Unconsume() { + DEBUG_HTTP2("Http2Session: unconsuming socket\n"); + if (prev_alloc_cb_.is_empty()) + return; + stream_->set_alloc_cb(prev_alloc_cb_); + stream_->set_read_cb(prev_read_cb_); + prev_alloc_cb_.clear(); + prev_read_cb_.clear(); + stream_ = nullptr; +} + + +void Initialize(Local<Object> target, + Local<Value> unused, + Local<Context> context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + + // Initialize the buffer used for padding callbacks + env->set_http2_padding_buffer(new uint32_t[3]); + const size_t http2_padding_buffer_byte_length = + sizeof(*env->http2_padding_buffer()) * 3; + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "paddingArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_padding_buffer(), + http2_padding_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the session state + env->set_http2_session_state_buffer( + new double[IDX_SESSION_STATE_COUNT]); + + const size_t http2_session_state_buffer_byte_length = + sizeof(*env->http2_session_state_buffer()) * + IDX_SESSION_STATE_COUNT; + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "sessionStateArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_session_state_buffer(), + http2_session_state_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the stream state + env->set_http2_stream_state_buffer( + new double[IDX_STREAM_STATE_COUNT]); + + const size_t http2_stream_state_buffer_byte_length = + sizeof(*env->http2_stream_state_buffer()) * + IDX_STREAM_STATE_COUNT; + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "streamStateArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_stream_state_buffer(), + http2_stream_state_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the current settings + env->set_http2_settings_buffer( + new uint32_t[IDX_SETTINGS_COUNT + 1]); + + const size_t http2_settings_buffer_byte_length = + sizeof(*env->http2_settings_buffer()) * + (IDX_SETTINGS_COUNT + 1); + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "settingsArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_settings_buffer(), + http2_settings_buffer_byte_length)) + .FromJust(); + + // Initialize the buffer used to store the options + env->set_http2_options_buffer( + new uint32_t[IDX_OPTIONS_FLAGS + 1]); + + const size_t http2_options_buffer_byte_length = + sizeof(*env->http2_options_buffer()) * + (IDX_OPTIONS_FLAGS + 1); + + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "optionsArrayBuffer"), + ArrayBuffer::New(env->isolate(), + env->http2_options_buffer(), + http2_options_buffer_byte_length)) + .FromJust(); + + // Method to fetch the nghttp2 string description of an nghttp2 error code + env->SetMethod(target, "nghttp2ErrorString", HttpErrorString); + + Local<String> http2SessionClassName = + String::NewFromUtf8(isolate, "Http2Session", + v8::NewStringType::kInternalized).ToLocalChecked(); + + Local<FunctionTemplate> session = + env->NewFunctionTemplate(Http2Session::New); + session->SetClassName(http2SessionClassName); + session->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(session, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(session, "consume", + Http2Session::Consume); + env->SetProtoMethod(session, "destroy", + Http2Session::Destroy); + env->SetProtoMethod(session, "sendHeaders", + Http2Session::SendHeaders); + env->SetProtoMethod(session, "submitShutdownNotice", + Http2Session::SendShutdownNotice); + env->SetProtoMethod(session, "submitGoaway", + Http2Session::SubmitGoaway); + env->SetProtoMethod(session, "submitSettings", + Http2Session::SubmitSettings); + env->SetProtoMethod(session, "submitPushPromise", + Http2Session::SubmitPushPromise); + env->SetProtoMethod(session, "submitRstStream", + Http2Session::SubmitRstStream); + env->SetProtoMethod(session, "submitResponse", + Http2Session::SubmitResponse); + env->SetProtoMethod(session, "submitFile", + Http2Session::SubmitFile); + env->SetProtoMethod(session, "submitRequest", + Http2Session::SubmitRequest); + env->SetProtoMethod(session, "submitPriority", + Http2Session::SubmitPriority); + env->SetProtoMethod(session, "shutdownStream", + Http2Session::ShutdownStream); + env->SetProtoMethod(session, "streamReadStart", + Http2Session::StreamReadStart); + env->SetProtoMethod(session, "streamReadStop", + Http2Session::StreamReadStop); + env->SetProtoMethod(session, "setNextStreamID", + Http2Session::SetNextStreamID); + env->SetProtoMethod(session, "destroyStream", + Http2Session::DestroyStream); + StreamBase::AddMethods<Http2Session>(env, session, + StreamBase::kFlagHasWritev | + StreamBase::kFlagNoShutdown); + target->Set(context, + http2SessionClassName, + session->GetFunction()).FromJust(); + + Local<Object> constants = Object::New(isolate); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SESSION_SERVER); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SESSION_CLIENT); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_IDLE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_OPEN); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_RESERVED_LOCAL); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_RESERVED_REMOTE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_STATE_CLOSED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_NO_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_PROTOCOL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_INTERNAL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLOW_CONTROL_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_TIMEOUT); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_STREAM_CLOSED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FRAME_SIZE_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_REFUSED_STREAM); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_CANCEL); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_COMPRESSION_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_CONNECT_ERROR); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_ENHANCE_YOUR_CALM); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_INADEQUATE_SECURITY); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_HTTP_1_1_REQUIRED); + + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_REQUEST); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_RESPONSE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_PUSH_RESPONSE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_HCAT_HEADERS); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NONE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NO_INDEX); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_DEFERRED); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_NOMEM); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_INVALID_ARGUMENT); + NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_CLOSED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_ERR_FRAME_SIZE_ERROR); + + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_NONE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_END_STREAM); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_END_HEADERS); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_ACK); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_PADDED); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_FLAG_PRIORITY); + + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_HEADER_TABLE_SIZE); + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_ENABLE_PUSH); + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE); + NODE_DEFINE_CONSTANT(constants, DEFAULT_SETTINGS_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, MAX_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, MIN_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, MAX_INITIAL_WINDOW_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_DEFAULT_WEIGHT); + + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_ENABLE_PUSH); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_FRAME_SIZE); + NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE); + + NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE); + NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_MAX); + NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_CALLBACK); + +#define STRING_CONSTANT(NAME, VALUE) \ + NODE_DEFINE_STRING_CONSTANT(constants, "HTTP2_HEADER_" # NAME, VALUE); +HTTP_KNOWN_HEADERS(STRING_CONSTANT) +#undef STRING_CONSTANT + +#define STRING_CONSTANT(NAME, VALUE) \ + NODE_DEFINE_STRING_CONSTANT(constants, "HTTP2_METHOD_" # NAME, VALUE); +HTTP_KNOWN_METHODS(STRING_CONSTANT) +#undef STRING_CONSTANT + +#define V(name, _) NODE_DEFINE_CONSTANT(constants, HTTP_STATUS_##name); +HTTP_STATUS_CODES(V) +#undef V + + env->SetMethod(target, "refreshLocalSettings", + RefreshSettings<nghttp2_session_get_local_settings>); + env->SetMethod(target, "refreshRemoteSettings", + RefreshSettings<nghttp2_session_get_remote_settings>); + env->SetMethod(target, "refreshDefaultSettings", RefreshDefaultSettings); + env->SetMethod(target, "refreshSessionState", RefreshSessionState); + env->SetMethod(target, "refreshStreamState", RefreshStreamState); + env->SetMethod(target, "packSettings", PackSettings); + + target->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "constants"), + constants).FromJust(); +} +} // namespace http2 +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(http2, node::http2::Initialize) diff --git a/src/node_http2.h b/src/node_http2.h new file mode 100644 index 0000000000..f6ccad2984 --- /dev/null +++ b/src/node_http2.h @@ -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_ diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h new file mode 100644 index 0000000000..49ec63b59b --- /dev/null +++ b/src/node_http2_core-inl.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_ diff --git a/src/node_http2_core.cc b/src/node_http2_core.cc new file mode 100644 index 0000000000..4d9ab4a4df --- /dev/null +++ b/src/node_http2_core.cc @@ -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 diff --git a/src/node_http2_core.h b/src/node_http2_core.h new file mode 100644 index 0000000000..10acd7736b --- /dev/null +++ b/src/node_http2_core.h @@ -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_ diff --git a/src/node_internals.h b/src/node_internals.h index d6bdf9b5ba..5d437fa302 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -83,6 +83,9 @@ extern std::string openssl_config; // that is used by lib/module.js extern bool config_preserve_symlinks; +// Set in node.cc by ParseArgs when --expose-http2 is used. +extern bool config_expose_http2; + // Set in node.cc by ParseArgs when --expose-internals or --expose_internals is // used. // Used in node_config.cc to set a constant on process.binding('config') diff --git a/src/stream_base.cc b/src/stream_base.cc index 51bad94a4f..3e94054546 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -408,6 +408,7 @@ void StreamBase::AfterWrite(WriteWrap* req_wrap, int status) { // Unref handle property Local<Object> req_wrap_obj = req_wrap->object(); req_wrap_obj->Delete(env->context(), env->handle_string()).FromJust(); + wrap->OnAfterWrite(req_wrap); Local<Value> argv[] = { diff --git a/src/stream_base.h b/src/stream_base.h index 68c82d243f..1b486e61db 100644 --- a/src/stream_base.h +++ b/src/stream_base.h @@ -89,6 +89,17 @@ class WriteWrap: public ReqWrap<uv_write_t>, static const size_t kAlignSize = 16; + WriteWrap(Environment* env, + v8::Local<v8::Object> obj, + StreamBase* wrap, + DoneCb cb) + : ReqWrap(env, obj, AsyncWrap::PROVIDER_WRITEWRAP), + StreamReq<WriteWrap>(cb), + wrap_(wrap), + storage_size_(0) { + Wrap(obj, this); + } + protected: WriteWrap(Environment* env, v8::Local<v8::Object> obj, |