diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 19:37:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 19:37:08 +0000 |
commit | d710a65c8b50bc3d4d0920dc6e865296f42edd5e (patch) | |
tree | d3bf9843448af9398b55f49a50a194bbaacd724e /src | |
parent | Initial commit. (diff) | |
download | nghttp2-d710a65c8b50bc3d4d0920dc6e865296f42edd5e.tar.xz nghttp2-d710a65c8b50bc3d4d0920dc6e865296f42edd5e.zip |
Adding upstream version 1.59.0.upstream/1.59.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src')
194 files changed, 77024 insertions, 0 deletions
diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..27631a7 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,13 @@ +# programs +deflatehd +h2load +inflatehd +nghttp +nghttpd +nghttpx + +# build +libnghttpx.a + +# tests +nghttpx-unittest diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..201c5a2 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,243 @@ +file(GLOB c_sources *.c) +set_source_files_properties(${c_sources} PROPERTIES + COMPILE_FLAGS "${WARNCFLAGS}") +file(GLOB cxx_sources *.cc) +set_source_files_properties(${cxx_sources} PROPERTIES + COMPILE_FLAGS "${WARNCXXFLAGS} ${CXX1XCXXFLAGS}") + +include_directories( + "${CMAKE_CURRENT_SOURCE_DIR}/includes" + "${CMAKE_CURRENT_SOURCE_DIR}/../third-party" + "${CMAKE_CURRENT_SOURCE_DIR}/../third-party/llhttp/include" + + ${JEMALLOC_INCLUDE_DIRS} + ${LIBXML2_INCLUDE_DIRS} + ${LIBEV_INCLUDE_DIRS} + ${LIBNGHTTP3_INCLUDE_DIRS} + ${LIBNGTCP2_INCLUDE_DIRS} + ${LIBNGTCP2_CRYPTO_QUICTLS_INCLUDE_DIRS} + ${OPENSSL_INCLUDE_DIRS} + ${LIBCARES_INCLUDE_DIRS} + ${JANSSON_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + ${LIBBPF_INCLUDE_DIRS} +) + +# XXX per-target? +link_libraries( + nghttp2 + ${JEMALLOC_LIBRARIES} + ${LIBXML2_LIBRARIES} + ${LIBEV_LIBRARIES} + ${LIBNGHTTP3_LIBRARIES} + ${LIBNGTCP2_LIBRARIES} + ${LIBNGTCP2_CRYPTO_QUICTLS_LIBRARIES} + ${OPENSSL_LIBRARIES} + ${LIBCARES_LIBRARIES} + ${JANSSON_LIBRARIES} + ${ZLIB_LIBRARIES} + ${APP_LIBRARIES} + ${LIBBPF_LIBRARIES} +) + +if(ENABLE_APP) + set(HELPER_OBJECTS + util.cc + http2.cc timegm.c app_helper.cc nghttp2_gzip.c + ) + + # nghttp client + set(NGHTTP_SOURCES + ${HELPER_OBJECTS} + nghttp.cc + tls.cc + ) + if(HAVE_LIBXML2) + list(APPEND NGHTTP_SOURCES HtmlParser.cc) + endif() + + # nghttpd + set(NGHTTPD_SOURCES + ${HELPER_OBJECTS} + nghttpd.cc + tls.cc + HttpServer.cc + ) + + # h2load + set(H2LOAD_SOURCES + util.cc + http2.cc h2load.cc + timegm.c + tls.cc + h2load_http2_session.cc + h2load_http1_session.cc + ) + if(ENABLE_HTTP3) + list(APPEND H2LOAD_SOURCES + h2load_http3_session.cc + h2load_quic.cc + quic.cc + ) + endif() + + # Common libnhttpx sources (used for nghttpx and unit tests) + set(NGHTTPX_SRCS + util.cc http2.cc timegm.c + app_helper.cc + tls.cc + shrpx_config.cc + shrpx_accept_handler.cc + shrpx_connection_handler.cc + shrpx_client_handler.cc + shrpx_http2_upstream.cc + shrpx_https_upstream.cc + shrpx_downstream.cc + shrpx_downstream_connection.cc + shrpx_http_downstream_connection.cc + shrpx_http2_downstream_connection.cc + shrpx_http2_session.cc + shrpx_downstream_queue.cc + shrpx_log.cc + shrpx_http.cc + shrpx_io_control.cc + shrpx_tls.cc + shrpx_worker.cc + shrpx_log_config.cc + shrpx_connect_blocker.cc + shrpx_live_check.cc + shrpx_downstream_connection_pool.cc + shrpx_rate_limit.cc + shrpx_connection.cc + shrpx_memcached_dispatcher.cc + shrpx_memcached_connection.cc + shrpx_worker_process.cc + shrpx_signal.cc + shrpx_router.cc + shrpx_api_downstream_connection.cc + shrpx_health_monitor_downstream_connection.cc + shrpx_null_downstream_connection.cc + shrpx_exec.cc + shrpx_dns_resolver.cc + shrpx_dual_dns_resolver.cc + shrpx_dns_tracker.cc + xsi_strerror.c + ) + if(HAVE_MRUBY) + list(APPEND NGHTTPX_SRCS + shrpx_mruby.cc + shrpx_mruby_module.cc + shrpx_mruby_module_env.cc + shrpx_mruby_module_request.cc + shrpx_mruby_module_response.cc + ) + endif() + if(ENABLE_HTTP3) + list(APPEND NGHTTPX_SRCS + shrpx_quic.cc + shrpx_quic_listener.cc + shrpx_quic_connection_handler.cc + shrpx_http3_upstream.cc + http3.cc + quic.cc + ) + endif() + add_library(nghttpx_static STATIC ${NGHTTPX_SRCS}) + set_target_properties(nghttpx_static PROPERTIES ARCHIVE_OUTPUT_NAME nghttpx) + + set(NGHTTPX-bin_SOURCES + shrpx.cc + ) + + if(HAVE_SYSTEMD) + target_link_libraries(nghttpx_static ${SYSTEMD_LIBRARIES}) + target_compile_definitions(nghttpx_static PUBLIC HAVE_LIBSYSTEMD) + target_include_directories(nghttpx_static PUBLIC ${SYSTEMD_INCLUDE_DIRS}) + endif() + + if(HAVE_MRUBY) + target_link_libraries(nghttpx_static mruby-lib) + endif() + + if(HAVE_NEVERBLEED) + target_link_libraries(nghttpx_static neverbleed) + endif() + + + if(HAVE_CUNIT) + set(NGHTTPX_UNITTEST_SOURCES + shrpx-unittest.cc + shrpx_tls_test.cc + shrpx_downstream_test.cc + shrpx_config_test.cc + shrpx_worker_test.cc + shrpx_http_test.cc + shrpx_router_test.cc + http2_test.cc + util_test.cc + nghttp2_gzip_test.c + nghttp2_gzip.c + buffer_test.cc + memchunk_test.cc + template_test.cc + base64_test.cc + ) + add_executable(nghttpx-unittest EXCLUDE_FROM_ALL + ${NGHTTPX_UNITTEST_SOURCES} + $<TARGET_OBJECTS:llhttp> + $<TARGET_OBJECTS:url-parser> + ) + target_include_directories(nghttpx-unittest PRIVATE ${CUNIT_INCLUDE_DIRS}) + target_compile_definitions(nghttpx-unittest + PRIVATE "-DNGHTTP2_SRC_DIR=\"${CMAKE_SOURCE_DIR}/src\"" + ) + target_link_libraries(nghttpx-unittest nghttpx_static ${CUNIT_LIBRARIES}) + if(HAVE_MRUBY) + target_link_libraries(nghttpx-unittest mruby-lib) + endif() + if(HAVE_NEVERBLEED) + target_link_libraries(nghttpx-unittest neverbleed) + endif() + + add_test(nghttpx-unittest nghttpx-unittest) + add_dependencies(check nghttpx-unittest) + endif() + + add_executable(nghttp ${NGHTTP_SOURCES} $<TARGET_OBJECTS:llhttp> + $<TARGET_OBJECTS:url-parser> + ) + add_executable(nghttpd ${NGHTTPD_SOURCES} $<TARGET_OBJECTS:llhttp> + $<TARGET_OBJECTS:url-parser> + ) + add_executable(nghttpx ${NGHTTPX-bin_SOURCES} $<TARGET_OBJECTS:llhttp> + $<TARGET_OBJECTS:url-parser> + ) + target_compile_definitions(nghttpx PRIVATE + "-DPKGDATADIR=\"${PKGDATADIR}\"" + "-DPKGLIBDIR=\"${PKGLIBDIR}\"" + ) + target_link_libraries(nghttpx nghttpx_static) + add_executable(h2load ${H2LOAD_SOURCES} $<TARGET_OBJECTS:llhttp> + $<TARGET_OBJECTS:url-parser> + ) + + install(TARGETS nghttp nghttpd nghttpx h2load + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +endif() + +if(ENABLE_HPACK_TOOLS) + set(inflatehd_SOURCES + inflatehd.cc + comp_helper.c + ) + set(deflatehd_SOURCES + deflatehd.cc + comp_helper.c + util.cc + timegm.c + ) + add_executable(inflatehd ${inflatehd_SOURCES}) + add_executable(deflatehd ${deflatehd_SOURCES}) + install(TARGETS inflatehd deflatehd + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +endif() diff --git a/src/HtmlParser.cc b/src/HtmlParser.cc new file mode 100644 index 0000000..591c4c7 --- /dev/null +++ b/src/HtmlParser.cc @@ -0,0 +1,217 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "HtmlParser.h" + +#include <libxml/uri.h> + +#include "util.h" + +namespace nghttp2 { + +ParserData::ParserData(const std::string &base_uri) + : base_uri(base_uri), inside_head(0) {} + +HtmlParser::HtmlParser(const std::string &base_uri) + : base_uri_(base_uri), parser_ctx_(nullptr), parser_data_(base_uri) {} + +HtmlParser::~HtmlParser() { htmlFreeParserCtxt(parser_ctx_); } + +namespace { +StringRef get_attr(const xmlChar **attrs, const StringRef &name) { + if (attrs == nullptr) { + return StringRef{}; + } + for (; *attrs; attrs += 2) { + if (util::strieq(StringRef{attrs[0], strlen(reinterpret_cast<const char *>( + attrs[0]))}, + name)) { + return StringRef{attrs[1], + strlen(reinterpret_cast<const char *>(attrs[1]))}; + } + } + return StringRef{}; +} +} // namespace + +namespace { +ResourceType +get_resource_type_for_preload_as(const StringRef &attribute_value) { + if (util::strieq_l("image", attribute_value)) { + return REQ_IMG; + } else if (util::strieq_l("style", attribute_value)) { + return REQ_CSS; + } else if (util::strieq_l("script", attribute_value)) { + return REQ_UNBLOCK_JS; + } else { + return REQ_OTHERS; + } +} +} // namespace + +namespace { +void add_link(ParserData *parser_data, const StringRef &uri, + ResourceType res_type) { + auto u = xmlBuildURI( + reinterpret_cast<const xmlChar *>(uri.c_str()), + reinterpret_cast<const xmlChar *>(parser_data->base_uri.c_str())); + if (u) { + parser_data->links.push_back( + std::make_pair(reinterpret_cast<char *>(u), res_type)); + free(u); + } +} +} // namespace + +namespace { +void start_element_func(void *user_data, const xmlChar *src_name, + const xmlChar **attrs) { + auto parser_data = static_cast<ParserData *>(user_data); + auto name = + StringRef{src_name, strlen(reinterpret_cast<const char *>(src_name))}; + if (util::strieq_l("head", name)) { + ++parser_data->inside_head; + } + if (util::strieq_l("link", name)) { + auto rel_attr = get_attr(attrs, StringRef::from_lit("rel")); + auto href_attr = get_attr(attrs, StringRef::from_lit("href")); + if (rel_attr.empty() || href_attr.empty()) { + return; + } + if (util::strieq_l("shortcut icon", rel_attr)) { + add_link(parser_data, href_attr, REQ_OTHERS); + } else if (util::strieq_l("stylesheet", rel_attr)) { + add_link(parser_data, href_attr, REQ_CSS); + } else if (util::strieq_l("preload", rel_attr)) { + auto as_attr = get_attr(attrs, StringRef::from_lit("as")); + if (as_attr.empty()) { + return; + } + add_link(parser_data, href_attr, + get_resource_type_for_preload_as(as_attr)); + } + } else if (util::strieq_l("img", name)) { + auto src_attr = get_attr(attrs, StringRef::from_lit("src")); + if (src_attr.empty()) { + return; + } + add_link(parser_data, src_attr, REQ_IMG); + } else if (util::strieq_l("script", name)) { + auto src_attr = get_attr(attrs, StringRef::from_lit("src")); + if (src_attr.empty()) { + return; + } + if (parser_data->inside_head) { + add_link(parser_data, src_attr, REQ_JS); + } else { + add_link(parser_data, src_attr, REQ_UNBLOCK_JS); + } + } +} +} // namespace + +namespace { +void end_element_func(void *user_data, const xmlChar *name) { + auto parser_data = static_cast<ParserData *>(user_data); + if (util::strieq_l( + "head", + StringRef{name, strlen(reinterpret_cast<const char *>(name))})) { + --parser_data->inside_head; + } +} +} // namespace + +namespace { +xmlSAXHandler saxHandler = { + nullptr, // internalSubsetSAXFunc + nullptr, // isStandaloneSAXFunc + nullptr, // hasInternalSubsetSAXFunc + nullptr, // hasExternalSubsetSAXFunc + nullptr, // resolveEntitySAXFunc + nullptr, // getEntitySAXFunc + nullptr, // entityDeclSAXFunc + nullptr, // notationDeclSAXFunc + nullptr, // attributeDeclSAXFunc + nullptr, // elementDeclSAXFunc + nullptr, // unparsedEntityDeclSAXFunc + nullptr, // setDocumentLocatorSAXFunc + nullptr, // startDocumentSAXFunc + nullptr, // endDocumentSAXFunc + &start_element_func, // startElementSAXFunc + &end_element_func, // endElementSAXFunc + nullptr, // referenceSAXFunc + nullptr, // charactersSAXFunc + nullptr, // ignorableWhitespaceSAXFunc + nullptr, // processingInstructionSAXFunc + nullptr, // commentSAXFunc + nullptr, // warningSAXFunc + nullptr, // errorSAXFunc + nullptr, // fatalErrorSAXFunc + nullptr, // getParameterEntitySAXFunc + nullptr, // cdataBlockSAXFunc + nullptr, // externalSubsetSAXFunc + 0, // unsigned int initialized + nullptr, // void * _private + nullptr, // startElementNsSAX2Func + nullptr, // endElementNsSAX2Func + nullptr, // xmlStructuredErrorFunc +}; +} // namespace + +int HtmlParser::parse_chunk(const char *chunk, size_t size, int fin) { + if (!parser_ctx_) { + parser_ctx_ = + htmlCreatePushParserCtxt(&saxHandler, &parser_data_, chunk, size, + base_uri_.c_str(), XML_CHAR_ENCODING_NONE); + if (!parser_ctx_) { + return -1; + } else { + if (fin) { + return parse_chunk_internal(nullptr, 0, fin); + } else { + return 0; + } + } + } else { + return parse_chunk_internal(chunk, size, fin); + } +} + +int HtmlParser::parse_chunk_internal(const char *chunk, size_t size, int fin) { + int rv = htmlParseChunk(parser_ctx_, chunk, size, fin); + if (rv == 0) { + return 0; + } else { + return -1; + } +} + +const std::vector<std::pair<std::string, ResourceType>> & +HtmlParser::get_links() const { + return parser_data_.links; +} + +void HtmlParser::clear_links() { parser_data_.links.clear(); } + +} // namespace nghttp2 diff --git a/src/HtmlParser.h b/src/HtmlParser.h new file mode 100644 index 0000000..1e84688 --- /dev/null +++ b/src/HtmlParser.h @@ -0,0 +1,94 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTML_PARSER_H +#define HTML_PARSER_H + +#include "nghttp2_config.h" + +#include <vector> +#include <string> + +#ifdef HAVE_LIBXML2 + +# include <libxml/HTMLparser.h> + +#endif // HAVE_LIBXML2 + +namespace nghttp2 { + +enum ResourceType { + REQ_CSS = 1, + REQ_JS, + REQ_UNBLOCK_JS, + REQ_IMG, + REQ_OTHERS, +}; + +struct ParserData { + std::string base_uri; + std::vector<std::pair<std::string, ResourceType>> links; + // > 0 if we are inside "head" element. + int inside_head; + ParserData(const std::string &base_uri); +}; + +#ifdef HAVE_LIBXML2 + +class HtmlParser { +public: + HtmlParser(const std::string &base_uri); + ~HtmlParser(); + int parse_chunk(const char *chunk, size_t size, int fin); + const std::vector<std::pair<std::string, ResourceType>> &get_links() const; + void clear_links(); + +private: + int parse_chunk_internal(const char *chunk, size_t size, int fin); + + std::string base_uri_; + htmlParserCtxtPtr parser_ctx_; + ParserData parser_data_; +}; + +#else // !HAVE_LIBXML2 + +class HtmlParser { +public: + HtmlParser(const std::string &base_uri) {} + int parse_chunk(const char *chunk, size_t size, int fin) { return 0; } + const std::vector<std::pair<std::string, ResourceType>> &get_links() const { + return links_; + } + void clear_links() {} + +private: + std::vector<std::pair<std::string, ResourceType>> links_; +}; + +#endif // !HAVE_LIBXML2 + +} // namespace nghttp2 + +#endif // HTML_PARSER_H diff --git a/src/HttpServer.cc b/src/HttpServer.cc new file mode 100644 index 0000000..0385cd0 --- /dev/null +++ b/src/HttpServer.cc @@ -0,0 +1,2248 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "HttpServer.h" + +#include <sys/stat.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#include <netinet/tcp.h> +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif // HAVE_ARPA_INET_H + +#include <cassert> +#include <set> +#include <iostream> +#include <thread> +#include <mutex> +#include <deque> + +#include "ssl_compat.h" + +#include <openssl/err.h> +#include <openssl/dh.h> +#if OPENSSL_3_0_0_API +# include <openssl/decoder.h> +#endif // OPENSSL_3_0_0_API + +#include <zlib.h> + +#include "app_helper.h" +#include "http2.h" +#include "util.h" +#include "tls.h" +#include "template.h" + +#ifndef O_BINARY +# define O_BINARY (0) +#endif // O_BINARY + +using namespace std::chrono_literals; + +namespace nghttp2 { + +namespace { +// TODO could be constexpr +constexpr auto DEFAULT_HTML = StringRef::from_lit("index.html"); +constexpr auto NGHTTPD_SERVER = + StringRef::from_lit("nghttpd nghttp2/" NGHTTP2_VERSION); +} // namespace + +namespace { +void delete_handler(Http2Handler *handler) { + handler->remove_self(); + delete handler; +} +} // namespace + +namespace { +void print_session_id(int64_t id) { std::cout << "[id=" << id << "] "; } +} // namespace + +Config::Config() + : mime_types_file("/etc/mime.types"), + stream_read_timeout(1_min), + stream_write_timeout(1_min), + data_ptr(nullptr), + padding(0), + num_worker(1), + max_concurrent_streams(100), + header_table_size(-1), + encoder_header_table_size(-1), + window_bits(-1), + connection_window_bits(-1), + port(0), + verbose(false), + daemon(false), + verify_client(false), + no_tls(false), + error_gzip(false), + early_response(false), + hexdump(false), + echo_upload(false), + no_content_length(false), + ktls(false), + no_rfc7540_pri(false) {} + +Config::~Config() {} + +namespace { +void stream_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + int rv; + auto stream = static_cast<Stream *>(w->data); + auto hd = stream->handler; + auto config = hd->get_config(); + + ev_timer_stop(hd->get_loop(), &stream->rtimer); + ev_timer_stop(hd->get_loop(), &stream->wtimer); + + if (config->verbose) { + print_session_id(hd->session_id()); + print_timer(); + std::cout << " timeout stream_id=" << stream->stream_id << std::endl; + } + + hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); + + rv = hd->on_write(); + if (rv == -1) { + delete_handler(hd); + } +} +} // namespace + +namespace { +void add_stream_read_timeout(Stream *stream) { + auto hd = stream->handler; + ev_timer_again(hd->get_loop(), &stream->rtimer); +} +} // namespace + +namespace { +void add_stream_read_timeout_if_pending(Stream *stream) { + auto hd = stream->handler; + if (ev_is_active(&stream->rtimer)) { + ev_timer_again(hd->get_loop(), &stream->rtimer); + } +} +} // namespace + +namespace { +void add_stream_write_timeout(Stream *stream) { + auto hd = stream->handler; + ev_timer_again(hd->get_loop(), &stream->wtimer); +} +} // namespace + +namespace { +void remove_stream_read_timeout(Stream *stream) { + auto hd = stream->handler; + ev_timer_stop(hd->get_loop(), &stream->rtimer); +} +} // namespace + +namespace { +void remove_stream_write_timeout(Stream *stream) { + auto hd = stream->handler; + ev_timer_stop(hd->get_loop(), &stream->wtimer); +} +} // namespace + +namespace { +void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config); +} // namespace + +namespace { +constexpr ev_tstamp RELEASE_FD_TIMEOUT = 2.; +} // namespace + +namespace { +void release_fd_cb(struct ev_loop *loop, ev_timer *w, int revents); +} // namespace + +namespace { +constexpr auto FILE_ENTRY_MAX_AGE = 10s; +} // namespace + +namespace { +constexpr size_t FILE_ENTRY_EVICT_THRES = 2048; +} // namespace + +namespace { +bool need_validation_file_entry( + const FileEntry *ent, const std::chrono::steady_clock::time_point &now) { + return ent->last_valid + FILE_ENTRY_MAX_AGE < now; +} +} // namespace + +namespace { +bool validate_file_entry(FileEntry *ent, + const std::chrono::steady_clock::time_point &now) { + struct stat stbuf; + int rv; + + rv = fstat(ent->fd, &stbuf); + if (rv != 0) { + ent->stale = true; + return false; + } + + if (stbuf.st_nlink == 0 || ent->mtime != stbuf.st_mtime) { + ent->stale = true; + return false; + } + + ent->mtime = stbuf.st_mtime; + ent->last_valid = now; + + return true; +} +} // namespace + +class Sessions { +public: + Sessions(HttpServer *sv, struct ev_loop *loop, const Config *config, + SSL_CTX *ssl_ctx) + : sv_(sv), + loop_(loop), + config_(config), + ssl_ctx_(ssl_ctx), + callbacks_(nullptr), + option_(nullptr), + next_session_id_(1), + tstamp_cached_(ev_now(loop)), + cached_date_(util::http_date(tstamp_cached_)) { + nghttp2_session_callbacks_new(&callbacks_); + + fill_callback(callbacks_, config_); + + nghttp2_option_new(&option_); + + if (config_->encoder_header_table_size != -1) { + nghttp2_option_set_max_deflate_dynamic_table_size( + option_, config_->encoder_header_table_size); + } + + ev_timer_init(&release_fd_timer_, release_fd_cb, 0., RELEASE_FD_TIMEOUT); + release_fd_timer_.data = this; + } + ~Sessions() { + ev_timer_stop(loop_, &release_fd_timer_); + for (auto handler : handlers_) { + delete handler; + } + nghttp2_option_del(option_); + nghttp2_session_callbacks_del(callbacks_); + } + void add_handler(Http2Handler *handler) { handlers_.insert(handler); } + void remove_handler(Http2Handler *handler) { + handlers_.erase(handler); + if (handlers_.empty() && !fd_cache_.empty()) { + ev_timer_again(loop_, &release_fd_timer_); + } + } + SSL_CTX *get_ssl_ctx() const { return ssl_ctx_; } + SSL *ssl_session_new(int fd) { + SSL *ssl = SSL_new(ssl_ctx_); + if (!ssl) { + std::cerr << "SSL_new() failed" << std::endl; + return nullptr; + } + if (SSL_set_fd(ssl, fd) == 0) { + std::cerr << "SSL_set_fd() failed" << std::endl; + SSL_free(ssl); + return nullptr; + } + return ssl; + } + const Config *get_config() const { return config_; } + struct ev_loop *get_loop() const { return loop_; } + int64_t get_next_session_id() { + auto session_id = next_session_id_; + if (next_session_id_ == std::numeric_limits<int64_t>::max()) { + next_session_id_ = 1; + } else { + ++next_session_id_; + } + return session_id; + } + const nghttp2_session_callbacks *get_callbacks() const { return callbacks_; } + const nghttp2_option *get_option() const { return option_; } + void accept_connection(int fd) { + util::make_socket_nodelay(fd); + SSL *ssl = nullptr; + if (ssl_ctx_) { + ssl = ssl_session_new(fd); + if (!ssl) { + close(fd); + return; + } + } + auto handler = + std::make_unique<Http2Handler>(this, fd, ssl, get_next_session_id()); + if (!ssl) { + if (handler->connection_made() != 0) { + return; + } + } + add_handler(handler.release()); + } + void update_cached_date() { cached_date_ = util::http_date(tstamp_cached_); } + const std::string &get_cached_date() { + auto t = ev_now(loop_); + if (t != tstamp_cached_) { + tstamp_cached_ = t; + update_cached_date(); + } + return cached_date_; + } + FileEntry *get_cached_fd(const std::string &path) { + auto range = fd_cache_.equal_range(path); + if (range.first == range.second) { + return nullptr; + } + + auto now = std::chrono::steady_clock::now(); + + for (auto it = range.first; it != range.second;) { + auto &ent = (*it).second; + if (ent->stale) { + ++it; + continue; + } + if (need_validation_file_entry(ent.get(), now) && + !validate_file_entry(ent.get(), now)) { + if (ent->usecount == 0) { + fd_cache_lru_.remove(ent.get()); + close(ent->fd); + it = fd_cache_.erase(it); + continue; + } + ++it; + continue; + } + + fd_cache_lru_.remove(ent.get()); + fd_cache_lru_.append(ent.get()); + + ++ent->usecount; + return ent.get(); + } + return nullptr; + } + FileEntry *cache_fd(const std::string &path, const FileEntry &ent) { +#ifdef HAVE_STD_MAP_EMPLACE + auto rv = fd_cache_.emplace(path, std::make_unique<FileEntry>(ent)); +#else // !HAVE_STD_MAP_EMPLACE + // for gcc-4.7 + auto rv = fd_cache_.insert( + std::make_pair(path, std::make_unique<FileEntry>(ent))); +#endif // !HAVE_STD_MAP_EMPLACE + auto &res = (*rv).second; + res->it = rv; + fd_cache_lru_.append(res.get()); + + while (fd_cache_.size() > FILE_ENTRY_EVICT_THRES) { + auto ent = fd_cache_lru_.head; + if (ent->usecount) { + break; + } + fd_cache_lru_.remove(ent); + close(ent->fd); + fd_cache_.erase(ent->it); + } + + return res.get(); + } + void release_fd(FileEntry *target) { + --target->usecount; + + if (target->usecount == 0 && target->stale) { + fd_cache_lru_.remove(target); + close(target->fd); + fd_cache_.erase(target->it); + return; + } + + // We use timer to close file descriptor and delete the entry from + // cache. The timer will be started when there is no handler. + } + void release_unused_fd() { + for (auto i = std::begin(fd_cache_); i != std::end(fd_cache_);) { + auto &ent = (*i).second; + if (ent->usecount != 0) { + ++i; + continue; + } + + fd_cache_lru_.remove(ent.get()); + close(ent->fd); + i = fd_cache_.erase(i); + } + } + const HttpServer *get_server() const { return sv_; } + bool handlers_empty() const { return handlers_.empty(); } + +private: + std::set<Http2Handler *> handlers_; + // cache for file descriptors to read file. + std::multimap<std::string, std::unique_ptr<FileEntry>> fd_cache_; + DList<FileEntry> fd_cache_lru_; + HttpServer *sv_; + struct ev_loop *loop_; + const Config *config_; + SSL_CTX *ssl_ctx_; + nghttp2_session_callbacks *callbacks_; + nghttp2_option *option_; + ev_timer release_fd_timer_; + int64_t next_session_id_; + ev_tstamp tstamp_cached_; + std::string cached_date_; +}; + +namespace { +void release_fd_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto sessions = static_cast<Sessions *>(w->data); + + ev_timer_stop(loop, w); + + if (!sessions->handlers_empty()) { + return; + } + + sessions->release_unused_fd(); +} +} // namespace + +Stream::Stream(Http2Handler *handler, int32_t stream_id) + : balloc(1024, 1024), + header{}, + handler(handler), + file_ent(nullptr), + body_length(0), + body_offset(0), + header_buffer_size(0), + stream_id(stream_id), + echo_upload(false) { + auto config = handler->get_config(); + ev_timer_init(&rtimer, stream_timeout_cb, 0., config->stream_read_timeout); + ev_timer_init(&wtimer, stream_timeout_cb, 0., config->stream_write_timeout); + rtimer.data = this; + wtimer.data = this; +} + +Stream::~Stream() { + if (file_ent != nullptr) { + auto sessions = handler->get_sessions(); + sessions->release_fd(file_ent); + } + + auto &rcbuf = header.rcbuf; + nghttp2_rcbuf_decref(rcbuf.method); + nghttp2_rcbuf_decref(rcbuf.scheme); + nghttp2_rcbuf_decref(rcbuf.authority); + nghttp2_rcbuf_decref(rcbuf.host); + nghttp2_rcbuf_decref(rcbuf.path); + nghttp2_rcbuf_decref(rcbuf.ims); + nghttp2_rcbuf_decref(rcbuf.expect); + + auto loop = handler->get_loop(); + ev_timer_stop(loop, &rtimer); + ev_timer_stop(loop, &wtimer); +} + +namespace { +void on_session_closed(Http2Handler *hd, int64_t session_id) { + if (hd->get_config()->verbose) { + print_session_id(session_id); + print_timer(); + std::cout << " closed" << std::endl; + } +} +} // namespace + +namespace { +void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + int rv; + auto hd = static_cast<Http2Handler *>(w->data); + hd->terminate_session(NGHTTP2_SETTINGS_TIMEOUT); + rv = hd->on_write(); + if (rv == -1) { + delete_handler(hd); + } +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto handler = static_cast<Http2Handler *>(w->data); + + rv = handler->on_read(); + if (rv == -1) { + delete_handler(handler); + } +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto handler = static_cast<Http2Handler *>(w->data); + + rv = handler->on_write(); + if (rv == -1) { + delete_handler(handler); + } +} +} // namespace + +Http2Handler::Http2Handler(Sessions *sessions, int fd, SSL *ssl, + int64_t session_id) + : session_id_(session_id), + session_(nullptr), + sessions_(sessions), + ssl_(ssl), + data_pending_(nullptr), + data_pendinglen_(0), + fd_(fd) { + ev_timer_init(&settings_timerev_, settings_timeout_cb, 10., 0.); + ev_io_init(&wev_, writecb, fd, EV_WRITE); + ev_io_init(&rev_, readcb, fd, EV_READ); + + settings_timerev_.data = this; + wev_.data = this; + rev_.data = this; + + auto loop = sessions_->get_loop(); + ev_io_start(loop, &rev_); + + if (ssl) { + SSL_set_accept_state(ssl); + read_ = &Http2Handler::tls_handshake; + write_ = &Http2Handler::tls_handshake; + } else { + read_ = &Http2Handler::read_clear; + write_ = &Http2Handler::write_clear; + } +} + +Http2Handler::~Http2Handler() { + on_session_closed(this, session_id_); + nghttp2_session_del(session_); + if (ssl_) { + SSL_set_shutdown(ssl_, SSL_get_shutdown(ssl_) | SSL_RECEIVED_SHUTDOWN); + ERR_clear_error(); + SSL_shutdown(ssl_); + } + auto loop = sessions_->get_loop(); + ev_timer_stop(loop, &settings_timerev_); + ev_io_stop(loop, &rev_); + ev_io_stop(loop, &wev_); + if (ssl_) { + SSL_free(ssl_); + } + shutdown(fd_, SHUT_WR); + close(fd_); +} + +void Http2Handler::remove_self() { sessions_->remove_handler(this); } + +struct ev_loop *Http2Handler::get_loop() const { return sessions_->get_loop(); } + +Http2Handler::WriteBuf *Http2Handler::get_wb() { return &wb_; } + +void Http2Handler::start_settings_timer() { + ev_timer_start(sessions_->get_loop(), &settings_timerev_); +} + +int Http2Handler::fill_wb() { + if (data_pending_) { + auto n = std::min(wb_.wleft(), data_pendinglen_); + wb_.write(data_pending_, n); + if (n < data_pendinglen_) { + data_pending_ += n; + data_pendinglen_ -= n; + return 0; + } + + data_pending_ = nullptr; + data_pendinglen_ = 0; + } + + for (;;) { + const uint8_t *data; + auto datalen = nghttp2_session_mem_send(session_, &data); + + if (datalen < 0) { + std::cerr << "nghttp2_session_mem_send() returned error: " + << nghttp2_strerror(datalen) << std::endl; + return -1; + } + if (datalen == 0) { + break; + } + auto n = wb_.write(data, datalen); + if (n < static_cast<decltype(n)>(datalen)) { + data_pending_ = data + n; + data_pendinglen_ = datalen - n; + break; + } + } + return 0; +} + +int Http2Handler::read_clear() { + int rv; + std::array<uint8_t, 8_k> buf; + + ssize_t nread; + while ((nread = read(fd_, buf.data(), buf.size())) == -1 && errno == EINTR) + ; + if (nread == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return write_(*this); + } + return -1; + } + if (nread == 0) { + return -1; + } + + if (get_config()->hexdump) { + util::hexdump(stdout, buf.data(), nread); + } + + rv = nghttp2_session_mem_recv(session_, buf.data(), nread); + if (rv < 0) { + if (rv != NGHTTP2_ERR_BAD_CLIENT_MAGIC) { + std::cerr << "nghttp2_session_mem_recv() returned error: " + << nghttp2_strerror(rv) << std::endl; + } + return -1; + } + + return write_(*this); +} + +int Http2Handler::write_clear() { + auto loop = sessions_->get_loop(); + for (;;) { + if (wb_.rleft() > 0) { + ssize_t nwrite; + while ((nwrite = write(fd_, wb_.pos, wb_.rleft())) == -1 && + errno == EINTR) + ; + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + ev_io_start(loop, &wev_); + return 0; + } + return -1; + } + wb_.drain(nwrite); + continue; + } + wb_.reset(); + if (fill_wb() != 0) { + return -1; + } + if (wb_.rleft() == 0) { + break; + } + } + + if (wb_.rleft() == 0) { + ev_io_stop(loop, &wev_); + } else { + ev_io_start(loop, &wev_); + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + return -1; + } + + return 0; +} + +int Http2Handler::tls_handshake() { + ev_io_stop(sessions_->get_loop(), &wev_); + + ERR_clear_error(); + + auto rv = SSL_do_handshake(ssl_); + + if (rv <= 0) { + auto err = SSL_get_error(ssl_, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + return 0; + case SSL_ERROR_WANT_WRITE: + ev_io_start(sessions_->get_loop(), &wev_); + return 0; + default: + return -1; + } + } + + if (sessions_->get_config()->verbose) { + std::cerr << "SSL/TLS handshake completed" << std::endl; + } + + if (verify_alpn_result() != 0) { + return -1; + } + + read_ = &Http2Handler::read_tls; + write_ = &Http2Handler::write_tls; + + if (connection_made() != 0) { + return -1; + } + + if (sessions_->get_config()->verbose) { + if (SSL_session_reused(ssl_)) { + std::cerr << "SSL/TLS session reused" << std::endl; + } + } + + return 0; +} + +int Http2Handler::read_tls() { + std::array<uint8_t, 8_k> buf; + + ERR_clear_error(); + + auto rv = SSL_read(ssl_, buf.data(), buf.size()); + + if (rv <= 0) { + auto err = SSL_get_error(ssl_, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + return write_(*this); + case SSL_ERROR_WANT_WRITE: + // renegotiation started + return -1; + default: + return -1; + } + } + + auto nread = rv; + + if (get_config()->hexdump) { + util::hexdump(stdout, buf.data(), nread); + } + + rv = nghttp2_session_mem_recv(session_, buf.data(), nread); + if (rv < 0) { + if (rv != NGHTTP2_ERR_BAD_CLIENT_MAGIC) { + std::cerr << "nghttp2_session_mem_recv() returned error: " + << nghttp2_strerror(rv) << std::endl; + } + return -1; + } + + return write_(*this); +} + +int Http2Handler::write_tls() { + auto loop = sessions_->get_loop(); + + ERR_clear_error(); + + for (;;) { + if (wb_.rleft() > 0) { + auto rv = SSL_write(ssl_, wb_.pos, wb_.rleft()); + + if (rv <= 0) { + auto err = SSL_get_error(ssl_, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + // renegotiation started + return -1; + case SSL_ERROR_WANT_WRITE: + ev_io_start(sessions_->get_loop(), &wev_); + return 0; + default: + return -1; + } + } + + wb_.drain(rv); + continue; + } + wb_.reset(); + if (fill_wb() != 0) { + return -1; + } + if (wb_.rleft() == 0) { + break; + } + } + + if (wb_.rleft() == 0) { + ev_io_stop(loop, &wev_); + } else { + ev_io_start(loop, &wev_); + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + return -1; + } + + return 0; +} + +int Http2Handler::on_read() { return read_(*this); } + +int Http2Handler::on_write() { return write_(*this); } + +int Http2Handler::connection_made() { + int r; + + r = nghttp2_session_server_new2(&session_, sessions_->get_callbacks(), this, + sessions_->get_option()); + + if (r != 0) { + return r; + } + + auto config = sessions_->get_config(); + std::array<nghttp2_settings_entry, 4> entry; + size_t niv = 1; + + entry[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + entry[0].value = config->max_concurrent_streams; + + if (config->header_table_size >= 0) { + entry[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; + entry[niv].value = config->header_table_size; + ++niv; + } + + if (config->window_bits != -1) { + entry[niv].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + entry[niv].value = (1 << config->window_bits) - 1; + ++niv; + } + + if (config->no_rfc7540_pri) { + entry[niv].settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; + entry[niv].value = 1; + ++niv; + } + + r = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry.data(), niv); + if (r != 0) { + return r; + } + + if (config->connection_window_bits != -1) { + r = nghttp2_session_set_local_window_size( + session_, NGHTTP2_FLAG_NONE, 0, + (1 << config->connection_window_bits) - 1); + if (r != 0) { + return r; + } + } + + if (ssl_ && !nghttp2::tls::check_http2_requirement(ssl_)) { + terminate_session(NGHTTP2_INADEQUATE_SECURITY); + } + + return on_write(); +} + +int Http2Handler::verify_alpn_result() { + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len; + // Check the negotiated protocol in ALPN + SSL_get0_alpn_selected(ssl_, &next_proto, &next_proto_len); + if (next_proto) { + auto proto = StringRef{next_proto, next_proto_len}; + if (sessions_->get_config()->verbose) { + std::cout << "The negotiated protocol: " << proto << std::endl; + } + if (util::check_h2_is_selected(proto)) { + return 0; + } + } + if (sessions_->get_config()->verbose) { + std::cerr << "Client did not advertise HTTP/2 protocol." + << " (nghttp2 expects " << NGHTTP2_PROTO_VERSION_ID << ")" + << std::endl; + } + return -1; +} + +int Http2Handler::submit_file_response(const StringRef &status, Stream *stream, + time_t last_modified, off_t file_length, + const std::string *content_type, + nghttp2_data_provider *data_prd) { + std::string last_modified_str; + auto nva = make_array(http2::make_nv_ls_nocopy(":status", status), + http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER), + http2::make_nv_ll("cache-control", "max-age=3600"), + http2::make_nv_ls("date", sessions_->get_cached_date()), + http2::make_nv_ll("", ""), http2::make_nv_ll("", ""), + http2::make_nv_ll("", ""), http2::make_nv_ll("", "")); + size_t nvlen = 4; + if (!get_config()->no_content_length) { + nva[nvlen++] = http2::make_nv_ls_nocopy( + "content-length", + util::make_string_ref_uint(stream->balloc, file_length)); + } + if (last_modified != 0) { + last_modified_str = util::http_date(last_modified); + nva[nvlen++] = http2::make_nv_ls("last-modified", last_modified_str); + } + if (content_type) { + nva[nvlen++] = http2::make_nv_ls("content-type", *content_type); + } + auto &trailer_names = get_config()->trailer_names; + if (!trailer_names.empty()) { + nva[nvlen++] = http2::make_nv_ls_nocopy("trailer", trailer_names); + } + return nghttp2_submit_response(session_, stream->stream_id, nva.data(), nvlen, + data_prd); +} + +int Http2Handler::submit_response(const StringRef &status, int32_t stream_id, + const HeaderRefs &headers, + nghttp2_data_provider *data_prd) { + auto nva = std::vector<nghttp2_nv>(); + nva.reserve(4 + headers.size()); + nva.push_back(http2::make_nv_ls_nocopy(":status", status)); + nva.push_back(http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER)); + nva.push_back(http2::make_nv_ls("date", sessions_->get_cached_date())); + + if (data_prd) { + auto &trailer_names = get_config()->trailer_names; + if (!trailer_names.empty()) { + nva.push_back(http2::make_nv_ls_nocopy("trailer", trailer_names)); + } + } + + for (auto &nv : headers) { + nva.push_back(http2::make_nv_nocopy(nv.name, nv.value, nv.no_index)); + } + int r = nghttp2_submit_response(session_, stream_id, nva.data(), nva.size(), + data_prd); + return r; +} + +int Http2Handler::submit_response(const StringRef &status, int32_t stream_id, + nghttp2_data_provider *data_prd) { + auto nva = make_array(http2::make_nv_ls_nocopy(":status", status), + http2::make_nv_ls_nocopy("server", NGHTTPD_SERVER), + http2::make_nv_ls("date", sessions_->get_cached_date()), + http2::make_nv_ll("", "")); + size_t nvlen = 3; + + if (data_prd) { + auto &trailer_names = get_config()->trailer_names; + if (!trailer_names.empty()) { + nva[nvlen++] = http2::make_nv_ls_nocopy("trailer", trailer_names); + } + } + + return nghttp2_submit_response(session_, stream_id, nva.data(), nvlen, + data_prd); +} + +int Http2Handler::submit_non_final_response(const std::string &status, + int32_t stream_id) { + auto nva = make_array(http2::make_nv_ls(":status", status)); + return nghttp2_submit_headers(session_, NGHTTP2_FLAG_NONE, stream_id, nullptr, + nva.data(), nva.size(), nullptr); +} + +int Http2Handler::submit_push_promise(Stream *stream, + const StringRef &push_path) { + auto authority = stream->header.authority; + + if (authority.empty()) { + authority = stream->header.host; + } + + auto scheme = get_config()->no_tls ? StringRef::from_lit("http") + : StringRef::from_lit("https"); + + auto nva = make_array(http2::make_nv_ll(":method", "GET"), + http2::make_nv_ls_nocopy(":path", push_path), + http2::make_nv_ls_nocopy(":scheme", scheme), + http2::make_nv_ls_nocopy(":authority", authority)); + + auto promised_stream_id = nghttp2_submit_push_promise( + session_, NGHTTP2_FLAG_END_HEADERS, stream->stream_id, nva.data(), + nva.size(), nullptr); + + if (promised_stream_id < 0) { + return promised_stream_id; + } + + auto promised_stream = std::make_unique<Stream>(this, promised_stream_id); + + auto &promised_header = promised_stream->header; + promised_header.method = StringRef::from_lit("GET"); + promised_header.path = push_path; + promised_header.scheme = scheme; + promised_header.authority = + make_string_ref(promised_stream->balloc, authority); + + add_stream(promised_stream_id, std::move(promised_stream)); + + return 0; +} + +int Http2Handler::submit_rst_stream(Stream *stream, uint32_t error_code) { + remove_stream_read_timeout(stream); + remove_stream_write_timeout(stream); + + return nghttp2_submit_rst_stream(session_, NGHTTP2_FLAG_NONE, + stream->stream_id, error_code); +} + +void Http2Handler::add_stream(int32_t stream_id, + std::unique_ptr<Stream> stream) { + id2stream_[stream_id] = std::move(stream); +} + +void Http2Handler::remove_stream(int32_t stream_id) { + id2stream_.erase(stream_id); +} + +Stream *Http2Handler::get_stream(int32_t stream_id) { + auto itr = id2stream_.find(stream_id); + if (itr == std::end(id2stream_)) { + return nullptr; + } else { + return (*itr).second.get(); + } +} + +int64_t Http2Handler::session_id() const { return session_id_; } + +Sessions *Http2Handler::get_sessions() const { return sessions_; } + +const Config *Http2Handler::get_config() const { + return sessions_->get_config(); +} + +void Http2Handler::remove_settings_timer() { + ev_timer_stop(sessions_->get_loop(), &settings_timerev_); +} + +void Http2Handler::terminate_session(uint32_t error_code) { + nghttp2_session_terminate_session(session_, error_code); +} + +ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, + uint8_t *buf, size_t length, uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) { + int rv; + auto hd = static_cast<Http2Handler *>(user_data); + auto stream = hd->get_stream(stream_id); + + auto nread = std::min(stream->body_length - stream->body_offset, + static_cast<int64_t>(length)); + + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + + if (nread == 0 || stream->body_length == stream->body_offset + nread) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + + auto config = hd->get_config(); + if (!config->trailer.empty()) { + std::vector<nghttp2_nv> nva; + nva.reserve(config->trailer.size()); + for (auto &kv : config->trailer) { + nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); + } + rv = nghttp2_submit_trailer(session, stream_id, nva.data(), nva.size()); + if (rv != 0) { + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } else { + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + } + } + + if (nghttp2_session_get_stream_remote_close(session, stream_id) == 0) { + remove_stream_read_timeout(stream); + remove_stream_write_timeout(stream); + + hd->submit_rst_stream(stream, NGHTTP2_NO_ERROR); + } + } + + return nread; +} + +namespace { +void prepare_status_response(Stream *stream, Http2Handler *hd, int status) { + auto sessions = hd->get_sessions(); + auto status_page = sessions->get_server()->get_status_page(status); + auto file_ent = &status_page->file_ent; + + // we don't set stream->file_ent since we don't want to expire it. + stream->body_length = file_ent->length; + nghttp2_data_provider data_prd; + data_prd.source.fd = file_ent->fd; + data_prd.read_callback = file_read_callback; + + HeaderRefs headers; + headers.reserve(2); + headers.emplace_back(StringRef::from_lit("content-type"), + StringRef::from_lit("text/html; charset=UTF-8")); + headers.emplace_back( + StringRef::from_lit("content-length"), + util::make_string_ref_uint(stream->balloc, file_ent->length)); + hd->submit_response(StringRef{status_page->status}, stream->stream_id, + headers, &data_prd); +} +} // namespace + +namespace { +void prepare_echo_response(Stream *stream, Http2Handler *hd) { + auto length = lseek(stream->file_ent->fd, 0, SEEK_END); + if (length == -1) { + hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); + return; + } + stream->body_length = length; + if (lseek(stream->file_ent->fd, 0, SEEK_SET) == -1) { + hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); + return; + } + nghttp2_data_provider data_prd; + data_prd.source.fd = stream->file_ent->fd; + data_prd.read_callback = file_read_callback; + + HeaderRefs headers; + headers.emplace_back(StringRef::from_lit("nghttpd-response"), + StringRef::from_lit("echo")); + if (!hd->get_config()->no_content_length) { + headers.emplace_back(StringRef::from_lit("content-length"), + util::make_string_ref_uint(stream->balloc, length)); + } + + hd->submit_response(StringRef::from_lit("200"), stream->stream_id, headers, + &data_prd); +} +} // namespace + +namespace { +bool prepare_upload_temp_store(Stream *stream, Http2Handler *hd) { + auto sessions = hd->get_sessions(); + + char tempfn[] = "/tmp/nghttpd.temp.XXXXXX"; + auto fd = mkstemp(tempfn); + if (fd == -1) { + return false; + } + unlink(tempfn); + // Ordinary request never start with "echo:". The length is 0 for + // now. We will update it when we get whole request body. + auto path = std::string("echo:") + tempfn; + stream->file_ent = + sessions->cache_fd(path, FileEntry(path, 0, 0, fd, nullptr, {}, true)); + stream->echo_upload = true; + return true; +} +} // namespace + +namespace { +void prepare_redirect_response(Stream *stream, Http2Handler *hd, + const StringRef &path, int status) { + auto scheme = stream->header.scheme; + + auto authority = stream->header.authority; + if (authority.empty()) { + authority = stream->header.host; + } + + auto location = concat_string_ref( + stream->balloc, scheme, StringRef::from_lit("://"), authority, path); + + auto headers = HeaderRefs{{StringRef::from_lit("location"), location}}; + + auto sessions = hd->get_sessions(); + auto status_page = sessions->get_server()->get_status_page(status); + + hd->submit_response(StringRef{status_page->status}, stream->stream_id, + headers, nullptr); +} +} // namespace + +namespace { +void prepare_response(Stream *stream, Http2Handler *hd, + bool allow_push = true) { + int rv; + auto reqpath = stream->header.path; + if (reqpath.empty()) { + prepare_status_response(stream, hd, 405); + return; + } + + auto ims = stream->header.ims; + + time_t last_mod = 0; + bool last_mod_found = false; + if (!ims.empty()) { + last_mod_found = true; + last_mod = util::parse_http_date(ims); + } + + StringRef raw_path, raw_query; + auto query_pos = std::find(std::begin(reqpath), std::end(reqpath), '?'); + if (query_pos != std::end(reqpath)) { + // Do not response to this request to allow clients to test timeouts. + if (util::streq_l("nghttpd_do_not_respond_to_req=yes", + StringRef{query_pos, std::end(reqpath)})) { + return; + } + raw_path = StringRef{std::begin(reqpath), query_pos}; + raw_query = StringRef{query_pos, std::end(reqpath)}; + } else { + raw_path = reqpath; + } + + auto sessions = hd->get_sessions(); + + StringRef path; + if (std::find(std::begin(raw_path), std::end(raw_path), '%') == + std::end(raw_path)) { + path = raw_path; + } else { + path = util::percent_decode(stream->balloc, raw_path); + } + + path = http2::path_join(stream->balloc, StringRef{}, StringRef{}, path, + StringRef{}); + + if (std::find(std::begin(path), std::end(path), '\\') != std::end(path)) { + if (stream->file_ent) { + sessions->release_fd(stream->file_ent); + stream->file_ent = nullptr; + } + prepare_status_response(stream, hd, 404); + return; + } + + if (!hd->get_config()->push.empty()) { + auto push_itr = hd->get_config()->push.find(path.str()); + if (allow_push && push_itr != std::end(hd->get_config()->push)) { + for (auto &push_path : (*push_itr).second) { + rv = hd->submit_push_promise(stream, StringRef{push_path}); + if (rv != 0) { + std::cerr << "nghttp2_submit_push_promise() returned error: " + << nghttp2_strerror(rv) << std::endl; + } + } + } + } + + std::string file_path; + { + auto len = hd->get_config()->htdocs.size() + path.size(); + + auto trailing_slash = path[path.size() - 1] == '/'; + if (trailing_slash) { + len += DEFAULT_HTML.size(); + } + + file_path.resize(len); + + auto p = &file_path[0]; + + auto &htdocs = hd->get_config()->htdocs; + p = std::copy(std::begin(htdocs), std::end(htdocs), p); + p = std::copy(std::begin(path), std::end(path), p); + if (trailing_slash) { + std::copy(std::begin(DEFAULT_HTML), std::end(DEFAULT_HTML), p); + } + } + + if (stream->echo_upload) { + assert(stream->file_ent); + prepare_echo_response(stream, hd); + return; + } + + auto file_ent = sessions->get_cached_fd(file_path); + + if (file_ent == nullptr) { + int file = open(file_path.c_str(), O_RDONLY | O_BINARY); + if (file == -1) { + prepare_status_response(stream, hd, 404); + + return; + } + + struct stat buf; + + if (fstat(file, &buf) == -1) { + close(file); + prepare_status_response(stream, hd, 404); + + return; + } + + if (buf.st_mode & S_IFDIR) { + close(file); + + auto reqpath = concat_string_ref(stream->balloc, raw_path, + StringRef::from_lit("/"), raw_query); + + prepare_redirect_response(stream, hd, reqpath, 301); + + return; + } + + const std::string *content_type = nullptr; + + auto ext = file_path.c_str() + file_path.size() - 1; + for (; file_path.c_str() < ext && *ext != '.' && *ext != '/'; --ext) + ; + if (*ext == '.') { + ++ext; + + const auto &mime_types = hd->get_config()->mime_types; + auto content_type_itr = mime_types.find(ext); + if (content_type_itr != std::end(mime_types)) { + content_type = &(*content_type_itr).second; + } + } + + file_ent = sessions->cache_fd( + file_path, FileEntry(file_path, buf.st_size, buf.st_mtime, file, + content_type, std::chrono::steady_clock::now())); + } + + stream->file_ent = file_ent; + + if (last_mod_found && file_ent->mtime <= last_mod) { + hd->submit_response(StringRef::from_lit("304"), stream->stream_id, nullptr); + + return; + } + + auto method = stream->header.method; + if (method == StringRef::from_lit("HEAD")) { + hd->submit_file_response(StringRef::from_lit("200"), stream, + file_ent->mtime, file_ent->length, + file_ent->content_type, nullptr); + return; + } + + stream->body_length = file_ent->length; + + nghttp2_data_provider data_prd; + + data_prd.source.fd = file_ent->fd; + data_prd.read_callback = file_read_callback; + + hd->submit_file_response(StringRef::from_lit("200"), stream, file_ent->mtime, + file_ent->length, file_ent->content_type, &data_prd); +} +} // namespace + +namespace { +int on_header_callback2(nghttp2_session *session, const nghttp2_frame *frame, + nghttp2_rcbuf *name, nghttp2_rcbuf *value, + uint8_t flags, void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + + auto namebuf = nghttp2_rcbuf_get_buf(name); + auto valuebuf = nghttp2_rcbuf_get_buf(value); + + if (hd->get_config()->verbose) { + print_session_id(hd->session_id()); + verbose_on_header_callback(session, frame, namebuf.base, namebuf.len, + valuebuf.base, valuebuf.len, flags, user_data); + } + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + auto stream = hd->get_stream(frame->hd.stream_id); + if (!stream) { + return 0; + } + + if (stream->header_buffer_size + namebuf.len + valuebuf.len > 64_k) { + hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); + return 0; + } + + stream->header_buffer_size += namebuf.len + valuebuf.len; + + auto token = http2::lookup_token(namebuf.base, namebuf.len); + + auto &header = stream->header; + + switch (token) { + case http2::HD__METHOD: + header.method = StringRef{valuebuf.base, valuebuf.len}; + header.rcbuf.method = value; + nghttp2_rcbuf_incref(value); + break; + case http2::HD__SCHEME: + header.scheme = StringRef{valuebuf.base, valuebuf.len}; + header.rcbuf.scheme = value; + nghttp2_rcbuf_incref(value); + break; + case http2::HD__AUTHORITY: + header.authority = StringRef{valuebuf.base, valuebuf.len}; + header.rcbuf.authority = value; + nghttp2_rcbuf_incref(value); + break; + case http2::HD_HOST: + header.host = StringRef{valuebuf.base, valuebuf.len}; + header.rcbuf.host = value; + nghttp2_rcbuf_incref(value); + break; + case http2::HD__PATH: + header.path = StringRef{valuebuf.base, valuebuf.len}; + header.rcbuf.path = value; + nghttp2_rcbuf_incref(value); + break; + case http2::HD_IF_MODIFIED_SINCE: + header.ims = StringRef{valuebuf.base, valuebuf.len}; + header.rcbuf.ims = value; + nghttp2_rcbuf_incref(value); + break; + case http2::HD_EXPECT: + header.expect = StringRef{valuebuf.base, valuebuf.len}; + header.rcbuf.expect = value; + nghttp2_rcbuf_incref(value); + break; + } + + return 0; +} +} // namespace + +namespace { +int on_begin_headers_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + + auto stream = std::make_unique<Stream>(hd, frame->hd.stream_id); + + add_stream_read_timeout(stream.get()); + + hd->add_stream(frame->hd.stream_id, std::move(stream)); + + return 0; +} +} // namespace + +namespace { +int hd_on_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + if (hd->get_config()->verbose) { + print_session_id(hd->session_id()); + verbose_on_frame_recv_callback(session, frame, user_data); + } + switch (frame->hd.type) { + case NGHTTP2_DATA: { + // TODO Handle POST + auto stream = hd->get_stream(frame->hd.stream_id); + if (!stream) { + return 0; + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + remove_stream_read_timeout(stream); + if (stream->echo_upload || !hd->get_config()->early_response) { + prepare_response(stream, hd); + } + } else { + add_stream_read_timeout(stream); + } + + break; + } + case NGHTTP2_HEADERS: { + auto stream = hd->get_stream(frame->hd.stream_id); + if (!stream) { + return 0; + } + + if (frame->headers.cat == NGHTTP2_HCAT_REQUEST) { + + auto expect100 = stream->header.expect; + + if (util::strieq_l("100-continue", expect100)) { + hd->submit_non_final_response("100", frame->hd.stream_id); + } + + auto method = stream->header.method; + if (hd->get_config()->echo_upload && + (method == StringRef::from_lit("POST") || + method == StringRef::from_lit("PUT"))) { + if (!prepare_upload_temp_store(stream, hd)) { + hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); + return 0; + } + } else if (hd->get_config()->early_response) { + prepare_response(stream, hd); + } + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + remove_stream_read_timeout(stream); + if (stream->echo_upload || !hd->get_config()->early_response) { + prepare_response(stream, hd); + } + } else { + add_stream_read_timeout(stream); + } + + break; + } + case NGHTTP2_SETTINGS: + if (frame->hd.flags & NGHTTP2_FLAG_ACK) { + hd->remove_settings_timer(); + } + break; + default: + break; + } + return 0; +} +} // namespace + +namespace { +int hd_on_frame_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + + if (hd->get_config()->verbose) { + print_session_id(hd->session_id()); + verbose_on_frame_send_callback(session, frame, user_data); + } + + switch (frame->hd.type) { + case NGHTTP2_DATA: + case NGHTTP2_HEADERS: { + auto stream = hd->get_stream(frame->hd.stream_id); + + if (!stream) { + return 0; + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + remove_stream_write_timeout(stream); + } else if (std::min(nghttp2_session_get_stream_remote_window_size( + session, frame->hd.stream_id), + nghttp2_session_get_remote_window_size(session)) <= 0) { + // If stream is blocked by flow control, enable write timeout. + add_stream_read_timeout_if_pending(stream); + add_stream_write_timeout(stream); + } else { + add_stream_read_timeout_if_pending(stream); + remove_stream_write_timeout(stream); + } + + break; + } + case NGHTTP2_SETTINGS: { + if (frame->hd.flags & NGHTTP2_FLAG_ACK) { + return 0; + } + + hd->start_settings_timer(); + + break; + } + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + auto promised_stream = hd->get_stream(promised_stream_id); + auto stream = hd->get_stream(frame->hd.stream_id); + + if (!stream || !promised_stream) { + return 0; + } + + add_stream_read_timeout_if_pending(stream); + add_stream_write_timeout(stream); + + prepare_response(promised_stream, hd, /*allow_push */ false); + } + } + return 0; +} +} // namespace + +namespace { +int send_data_callback(nghttp2_session *session, nghttp2_frame *frame, + const uint8_t *framehd, size_t length, + nghttp2_data_source *source, void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + auto wb = hd->get_wb(); + auto padlen = frame->data.padlen; + auto stream = hd->get_stream(frame->hd.stream_id); + + if (wb->wleft() < 9 + length + padlen) { + return NGHTTP2_ERR_WOULDBLOCK; + } + + int fd = source->fd; + + auto p = wb->last; + + p = std::copy_n(framehd, 9, p); + + if (padlen) { + *p++ = padlen - 1; + } + + while (length) { + ssize_t nread; + while ((nread = pread(fd, p, length, stream->body_offset)) == -1 && + errno == EINTR) + ; + + if (nread == -1) { + remove_stream_read_timeout(stream); + remove_stream_write_timeout(stream); + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + stream->body_offset += nread; + length -= nread; + p += nread; + } + + if (padlen) { + std::fill(p, p + padlen - 1, 0); + p += padlen - 1; + } + + wb->last = p; + + return 0; +} +} // namespace + +namespace { +ssize_t select_padding_callback(nghttp2_session *session, + const nghttp2_frame *frame, size_t max_payload, + void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + return std::min(max_payload, frame->hd.length + hd->get_config()->padding); +} +} // namespace + +namespace { +int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + auto stream = hd->get_stream(stream_id); + + if (!stream) { + return 0; + } + + if (stream->echo_upload) { + assert(stream->file_ent); + while (len) { + ssize_t n; + while ((n = write(stream->file_ent->fd, data, len)) == -1 && + errno == EINTR) + ; + if (n == -1) { + hd->submit_rst_stream(stream, NGHTTP2_INTERNAL_ERROR); + return 0; + } + len -= n; + data += n; + } + } + // TODO Handle POST + + add_stream_read_timeout(stream); + + return 0; +} +} // namespace + +namespace { +int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *user_data) { + auto hd = static_cast<Http2Handler *>(user_data); + hd->remove_stream(stream_id); + if (hd->get_config()->verbose) { + print_session_id(hd->session_id()); + print_timer(); + printf(" stream_id=%d closed\n", stream_id); + fflush(stdout); + } + return 0; +} +} // namespace + +namespace { +void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config) { + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_frame_recv_callback( + callbacks, hd_on_frame_recv_callback); + + nghttp2_session_callbacks_set_on_frame_send_callback( + callbacks, hd_on_frame_send_callback); + + if (config->verbose) { + nghttp2_session_callbacks_set_on_invalid_frame_recv_callback( + callbacks, verbose_on_invalid_frame_recv_callback); + + nghttp2_session_callbacks_set_error_callback2(callbacks, + verbose_error_callback); + } + + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); + + nghttp2_session_callbacks_set_on_header_callback2(callbacks, + on_header_callback2); + + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, on_begin_headers_callback); + + nghttp2_session_callbacks_set_send_data_callback(callbacks, + send_data_callback); + + if (config->padding) { + nghttp2_session_callbacks_set_select_padding_callback( + callbacks, select_padding_callback); + } +} +} // namespace + +struct ClientInfo { + int fd; +}; + +struct Worker { + std::unique_ptr<Sessions> sessions; + ev_async w; + // protects q + std::mutex m; + std::deque<ClientInfo> q; +}; + +namespace { +void worker_acceptcb(struct ev_loop *loop, ev_async *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + auto &sessions = worker->sessions; + + std::deque<ClientInfo> q; + { + std::lock_guard<std::mutex> lock(worker->m); + q.swap(worker->q); + } + + for (const auto &c : q) { + sessions->accept_connection(c.fd); + } +} +} // namespace + +namespace { +void run_worker(Worker *worker) { + auto loop = worker->sessions->get_loop(); + + ev_run(loop, 0); +} +} // namespace + +namespace { +int get_ev_loop_flags() { + if (ev_supported_backends() & ~ev_recommended_backends() & EVBACKEND_KQUEUE) { + return ev_recommended_backends() | EVBACKEND_KQUEUE; + } + + return 0; +} +} // namespace + +class AcceptHandler { +public: + AcceptHandler(HttpServer *sv, Sessions *sessions, const Config *config) + : sessions_(sessions), config_(config), next_worker_(0) { + if (config_->num_worker == 1) { + return; + } + for (size_t i = 0; i < config_->num_worker; ++i) { + if (config_->verbose) { + std::cerr << "spawning thread #" << i << std::endl; + } + auto worker = std::make_unique<Worker>(); + auto loop = ev_loop_new(get_ev_loop_flags()); + worker->sessions = std::make_unique<Sessions>(sv, loop, config_, + sessions_->get_ssl_ctx()); + ev_async_init(&worker->w, worker_acceptcb); + worker->w.data = worker.get(); + ev_async_start(loop, &worker->w); + + auto t = std::thread(run_worker, worker.get()); + t.detach(); + workers_.push_back(std::move(worker)); + } + } + void accept_connection(int fd) { + if (config_->num_worker == 1) { + sessions_->accept_connection(fd); + return; + } + + // Dispatch client to the one of the worker threads, in a round + // robin manner. + auto &worker = workers_[next_worker_]; + if (next_worker_ == config_->num_worker - 1) { + next_worker_ = 0; + } else { + ++next_worker_; + } + { + std::lock_guard<std::mutex> lock(worker->m); + worker->q.push_back({fd}); + } + ev_async_send(worker->sessions->get_loop(), &worker->w); + } + +private: + std::vector<std::unique_ptr<Worker>> workers_; + Sessions *sessions_; + const Config *config_; + // In multi threading mode, this points to the next thread that + // client will be dispatched. + size_t next_worker_; +}; + +namespace { +void acceptcb(struct ev_loop *loop, ev_io *w, int revents); +} // namespace + +class ListenEventHandler { +public: + ListenEventHandler(Sessions *sessions, int fd, + std::shared_ptr<AcceptHandler> acceptor) + : acceptor_(std::move(acceptor)), sessions_(sessions), fd_(fd) { + ev_io_init(&w_, acceptcb, fd, EV_READ); + w_.data = this; + ev_io_start(sessions_->get_loop(), &w_); + } + void accept_connection() { + for (;;) { +#ifdef HAVE_ACCEPT4 + auto fd = accept4(fd_, nullptr, nullptr, SOCK_NONBLOCK); +#else // !HAVE_ACCEPT4 + auto fd = accept(fd_, nullptr, nullptr); +#endif // !HAVE_ACCEPT4 + if (fd == -1) { + break; + } +#ifndef HAVE_ACCEPT4 + util::make_socket_nonblocking(fd); +#endif // !HAVE_ACCEPT4 + acceptor_->accept_connection(fd); + } + } + +private: + ev_io w_; + std::shared_ptr<AcceptHandler> acceptor_; + Sessions *sessions_; + int fd_; +}; + +namespace { +void acceptcb(struct ev_loop *loop, ev_io *w, int revents) { + auto handler = static_cast<ListenEventHandler *>(w->data); + handler->accept_connection(); +} +} // namespace + +namespace { +FileEntry make_status_body(int status, uint16_t port) { + BlockAllocator balloc(1024, 1024); + + auto status_string = http2::stringify_status(balloc, status); + auto reason_pharase = http2::get_reason_phrase(status); + + std::string body; + body = "<html><head><title>"; + body += status_string; + body += ' '; + body += reason_pharase; + body += "</title></head><body><h1>"; + body += status_string; + body += ' '; + body += reason_pharase; + body += "</h1><hr><address>"; + body += NGHTTPD_SERVER; + body += " at port "; + body += util::utos(port); + body += "</address>"; + body += "</body></html>"; + + char tempfn[] = "/tmp/nghttpd.temp.XXXXXX"; + int fd = mkstemp(tempfn); + if (fd == -1) { + auto error = errno; + std::cerr << "Could not open status response body file: errno=" << error; + assert(0); + } + unlink(tempfn); + ssize_t nwrite; + while ((nwrite = write(fd, body.c_str(), body.size())) == -1 && + errno == EINTR) + ; + if (nwrite == -1) { + auto error = errno; + std::cerr << "Could not write status response body into file: errno=" + << error; + assert(0); + } + + return FileEntry(util::utos(status), nwrite, 0, fd, nullptr, {}); +} +} // namespace + +// index into HttpServer::status_pages_ +enum { + IDX_200, + IDX_301, + IDX_400, + IDX_404, + IDX_405, +}; + +HttpServer::HttpServer(const Config *config) : config_(config) { + status_pages_ = std::vector<StatusPage>{ + {"200", make_status_body(200, config_->port)}, + {"301", make_status_body(301, config_->port)}, + {"400", make_status_body(400, config_->port)}, + {"404", make_status_body(404, config_->port)}, + {"405", make_status_body(405, config_->port)}, + }; +} + +namespace { +int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { + // We don't verify the client certificate. Just request it for the + // testing purpose. + return 1; +} +} // namespace + +namespace { +int start_listen(HttpServer *sv, struct ev_loop *loop, Sessions *sessions, + const Config *config) { + int r; + bool ok = false; + const char *addr = nullptr; + + std::shared_ptr<AcceptHandler> acceptor; + auto service = util::utos(config->port); + + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; +#ifdef AI_ADDRCONFIG + hints.ai_flags |= AI_ADDRCONFIG; +#endif // AI_ADDRCONFIG + + if (!config->address.empty()) { + addr = config->address.c_str(); + } + + addrinfo *res, *rp; + r = getaddrinfo(addr, service.c_str(), &hints, &res); + if (r != 0) { + std::cerr << "getaddrinfo() failed: " << gai_strerror(r) << std::endl; + return -1; + } + + for (rp = res; rp; rp = rp->ai_next) { + int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + continue; + } + int val = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + (void)util::make_socket_nonblocking(fd); +#ifdef IPV6_V6ONLY + if (rp->ai_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + close(fd); + continue; + } + } +#endif // IPV6_V6ONLY + if (bind(fd, rp->ai_addr, rp->ai_addrlen) == 0 && listen(fd, 1000) == 0) { + if (!acceptor) { + acceptor = std::make_shared<AcceptHandler>(sv, sessions, config); + } + new ListenEventHandler(sessions, fd, acceptor); + + if (config->verbose) { + std::string s = util::numeric_name(rp->ai_addr, rp->ai_addrlen); + std::cout << (rp->ai_family == AF_INET ? "IPv4" : "IPv6") << ": listen " + << s << ":" << config->port << std::endl; + } + ok = true; + continue; + } else { + std::cerr << strerror(errno) << std::endl; + } + close(fd); + } + freeaddrinfo(res); + + if (!ok) { + return -1; + } + return 0; +} +} // namespace + +namespace { +int alpn_select_proto_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + auto config = static_cast<HttpServer *>(arg)->get_config(); + if (config->verbose) { + std::cout << "[ALPN] client offers:" << std::endl; + } + if (config->verbose) { + for (unsigned int i = 0; i < inlen; i += in[i] + 1) { + std::cout << " * "; + std::cout.write(reinterpret_cast<const char *>(&in[i + 1]), in[i]); + std::cout << std::endl; + } + } + if (!util::select_h2(out, outlen, in, inlen)) { + return SSL_TLSEXT_ERR_NOACK; + } + return SSL_TLSEXT_ERR_OK; +} +} // namespace + +int HttpServer::run() { + SSL_CTX *ssl_ctx = nullptr; + std::vector<unsigned char> next_proto; + + if (!config_->no_tls) { + ssl_ctx = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx) { + std::cerr << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION | + SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_TICKET | + SSL_OP_CIPHER_SERVER_PREFERENCE; + +#ifdef SSL_OP_ENABLE_KTLS + if (config_->ktls) { + ssl_opts |= SSL_OP_ENABLE_KTLS; + } +#endif // SSL_OP_ENABLE_KTLS + + SSL_CTX_set_options(ssl_ctx, ssl_opts); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + if (nghttp2::tls::ssl_ctx_set_proto_versions( + ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, + nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { + std::cerr << "Could not set TLS versions" << std::endl; + return -1; + } + + if (SSL_CTX_set_cipher_list(ssl_ctx, tls::DEFAULT_CIPHER_LIST) == 0) { + std::cerr << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + const unsigned char sid_ctx[] = "nghttpd"; + SSL_CTX_set_session_id_context(ssl_ctx, sid_ctx, sizeof(sid_ctx) - 1); + SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER); + +#ifndef OPENSSL_NO_EC + if (SSL_CTX_set1_curves_list(ssl_ctx, "P-256") != 1) { + std::cerr << "SSL_CTX_set1_curves_list failed: " + << ERR_error_string(ERR_get_error(), nullptr); + return -1; + } +#endif // OPENSSL_NO_EC + + if (!config_->dh_param_file.empty()) { + // Read DH parameters from file + auto bio = BIO_new_file(config_->dh_param_file.c_str(), "rb"); + if (bio == nullptr) { + std::cerr << "BIO_new_file() failed: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + +#if OPENSSL_3_0_0_API + EVP_PKEY *dh = nullptr; + auto dctx = OSSL_DECODER_CTX_new_for_pkey( + &dh, "PEM", nullptr, "DH", OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, + nullptr, nullptr); + + if (!OSSL_DECODER_from_bio(dctx, bio)) { + std::cerr << "OSSL_DECODER_from_bio() failed: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + if (SSL_CTX_set0_tmp_dh_pkey(ssl_ctx, dh) != 1) { + std::cerr << "SSL_CTX_set0_tmp_dh_pkey failed: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } +#else // !OPENSSL_3_0_0_API + auto dh = PEM_read_bio_DHparams(bio, nullptr, nullptr, nullptr); + + if (dh == nullptr) { + std::cerr << "PEM_read_bio_DHparams() failed: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + SSL_CTX_set_tmp_dh(ssl_ctx, dh); + DH_free(dh); +#endif // !OPENSSL_3_0_0_API + BIO_free(bio); + } + + if (SSL_CTX_use_PrivateKey_file(ssl_ctx, config_->private_key_file.c_str(), + SSL_FILETYPE_PEM) != 1) { + std::cerr << "SSL_CTX_use_PrivateKey_file failed." << std::endl; + return -1; + } + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, + config_->cert_file.c_str()) != 1) { + std::cerr << "SSL_CTX_use_certificate_file failed." << std::endl; + return -1; + } + if (SSL_CTX_check_private_key(ssl_ctx) != 1) { + std::cerr << "SSL_CTX_check_private_key failed." << std::endl; + return -1; + } + if (config_->verify_client) { + SSL_CTX_set_verify(ssl_ctx, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_callback); + } + + next_proto = util::get_default_alpn(); + + // ALPN selection callback + SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, this); + } + + auto loop = EV_DEFAULT; + + Sessions sessions(this, loop, config_, ssl_ctx); + if (start_listen(this, loop, &sessions, config_) != 0) { + std::cerr << "Could not listen" << std::endl; + if (ssl_ctx) { + SSL_CTX_free(ssl_ctx); + } + return -1; + } + + ev_run(loop, 0); + + SSL_CTX_free(ssl_ctx); + + return 0; +} + +const Config *HttpServer::get_config() const { return config_; } + +const StatusPage *HttpServer::get_status_page(int status) const { + switch (status) { + case 200: + return &status_pages_[IDX_200]; + case 301: + return &status_pages_[IDX_301]; + case 400: + return &status_pages_[IDX_400]; + case 404: + return &status_pages_[IDX_404]; + case 405: + return &status_pages_[IDX_405]; + default: + assert(0); + } + return nullptr; +} + +} // namespace nghttp2 diff --git a/src/HttpServer.h b/src/HttpServer.h new file mode 100644 index 0000000..949bd1f --- /dev/null +++ b/src/HttpServer.h @@ -0,0 +1,253 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP_SERVER_H +#define HTTP_SERVER_H + +#include "nghttp2_config.h" + +#include <sys/types.h> + +#include <cinttypes> +#include <cstdlib> + +#include <string> +#include <vector> +#include <map> +#include <memory> + +#include <openssl/ssl.h> + +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#include "http2.h" +#include "buffer.h" +#include "template.h" +#include "allocator.h" + +namespace nghttp2 { + +struct Config { + std::map<std::string, std::vector<std::string>> push; + std::map<std::string, std::string> mime_types; + Headers trailer; + std::string trailer_names; + std::string htdocs; + std::string host; + std::string private_key_file; + std::string cert_file; + std::string dh_param_file; + std::string address; + std::string mime_types_file; + ev_tstamp stream_read_timeout; + ev_tstamp stream_write_timeout; + void *data_ptr; + size_t padding; + size_t num_worker; + size_t max_concurrent_streams; + ssize_t header_table_size; + ssize_t encoder_header_table_size; + int window_bits; + int connection_window_bits; + uint16_t port; + bool verbose; + bool daemon; + bool verify_client; + bool no_tls; + bool error_gzip; + bool early_response; + bool hexdump; + bool echo_upload; + bool no_content_length; + bool ktls; + bool no_rfc7540_pri; + Config(); + ~Config(); +}; + +class Http2Handler; + +struct FileEntry { + FileEntry(std::string path, int64_t length, int64_t mtime, int fd, + const std::string *content_type, + const std::chrono::steady_clock::time_point &last_valid, + bool stale = false) + : path(std::move(path)), + length(length), + mtime(mtime), + last_valid(last_valid), + content_type(content_type), + dlnext(nullptr), + dlprev(nullptr), + fd(fd), + usecount(1), + stale(stale) {} + std::string path; + std::multimap<std::string, std::unique_ptr<FileEntry>>::iterator it; + int64_t length; + int64_t mtime; + std::chrono::steady_clock::time_point last_valid; + const std::string *content_type; + FileEntry *dlnext, *dlprev; + int fd; + int usecount; + bool stale; +}; + +struct RequestHeader { + StringRef method; + StringRef scheme; + StringRef authority; + StringRef host; + StringRef path; + StringRef ims; + StringRef expect; + + struct { + nghttp2_rcbuf *method; + nghttp2_rcbuf *scheme; + nghttp2_rcbuf *authority; + nghttp2_rcbuf *host; + nghttp2_rcbuf *path; + nghttp2_rcbuf *ims; + nghttp2_rcbuf *expect; + } rcbuf; +}; + +struct Stream { + BlockAllocator balloc; + RequestHeader header; + Http2Handler *handler; + FileEntry *file_ent; + ev_timer rtimer; + ev_timer wtimer; + int64_t body_length; + int64_t body_offset; + // Total amount of bytes (sum of name and value length) used in + // headers. + size_t header_buffer_size; + int32_t stream_id; + bool echo_upload; + Stream(Http2Handler *handler, int32_t stream_id); + ~Stream(); +}; + +class Sessions; + +class Http2Handler { +public: + Http2Handler(Sessions *sessions, int fd, SSL *ssl, int64_t session_id); + ~Http2Handler(); + + void remove_self(); + void start_settings_timer(); + int on_read(); + int on_write(); + int connection_made(); + int verify_alpn_result(); + + int submit_file_response(const StringRef &status, Stream *stream, + time_t last_modified, off_t file_length, + const std::string *content_type, + nghttp2_data_provider *data_prd); + + int submit_response(const StringRef &status, int32_t stream_id, + nghttp2_data_provider *data_prd); + + int submit_response(const StringRef &status, int32_t stream_id, + const HeaderRefs &headers, + nghttp2_data_provider *data_prd); + + int submit_non_final_response(const std::string &status, int32_t stream_id); + + int submit_push_promise(Stream *stream, const StringRef &push_path); + + int submit_rst_stream(Stream *stream, uint32_t error_code); + + void add_stream(int32_t stream_id, std::unique_ptr<Stream> stream); + void remove_stream(int32_t stream_id); + Stream *get_stream(int32_t stream_id); + int64_t session_id() const; + Sessions *get_sessions() const; + const Config *get_config() const; + void remove_settings_timer(); + void terminate_session(uint32_t error_code); + + int fill_wb(); + + int read_clear(); + int write_clear(); + int tls_handshake(); + int read_tls(); + int write_tls(); + + struct ev_loop *get_loop() const; + + using WriteBuf = Buffer<64_k>; + + WriteBuf *get_wb(); + +private: + ev_io wev_; + ev_io rev_; + ev_timer settings_timerev_; + std::map<int32_t, std::unique_ptr<Stream>> id2stream_; + WriteBuf wb_; + std::function<int(Http2Handler &)> read_, write_; + int64_t session_id_; + nghttp2_session *session_; + Sessions *sessions_; + SSL *ssl_; + const uint8_t *data_pending_; + size_t data_pendinglen_; + int fd_; +}; + +struct StatusPage { + std::string status; + FileEntry file_ent; +}; + +class HttpServer { +public: + HttpServer(const Config *config); + int listen(); + int run(); + const Config *get_config() const; + const StatusPage *get_status_page(int status) const; + +private: + std::vector<StatusPage> status_pages_; + const Config *config_; +}; + +ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, + uint8_t *buf, size_t length, uint32_t *data_flags, + nghttp2_data_source *source, void *user_data); + +} // namespace nghttp2 + +#endif // HTTP_SERVER_H diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..f112ac2 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,257 @@ +# nghttp2 - HTTP/2 C Library + +# Copyright (c) 2012 Tatsuhiro Tsujikawa + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +SUBDIRS = testdata + +EXTRA_DIST = \ + CMakeLists.txt \ + test.example.com.pem \ + test.nghttp2.org.pem + +bin_PROGRAMS = +check_PROGRAMS = +TESTS = + +AM_CFLAGS = $(WARNCFLAGS) +AM_CXXFLAGS = $(WARNCXXFLAGS) $(CXX1XCXXFLAGS) +AM_CPPFLAGS = \ + -DPKGDATADIR='"$(pkgdatadir)"' \ + -DPKGLIBDIR='"$(pkglibdir)"' \ + -I$(top_srcdir)/lib/includes \ + -I$(top_builddir)/lib/includes \ + -I$(top_srcdir)/lib \ + -I$(top_srcdir)/third-party \ + -I$(top_srcdir)/third-party/llhttp/include \ + @JEMALLOC_CFLAGS@ \ + @LIBXML2_CFLAGS@ \ + @LIBEV_CFLAGS@ \ + @LIBNGHTTP3_CFLAGS@ \ + @LIBNGTCP2_CRYPTO_QUICTLS_CFLAGS@ \ + @LIBNGTCP2_CRYPTO_BORINGSSL_CFLAGS@ \ + @LIBNGTCP2_CFLAGS@ \ + @OPENSSL_CFLAGS@ \ + @LIBCARES_CFLAGS@ \ + @JANSSON_CFLAGS@ \ + @LIBBPF_CFLAGS@ \ + @ZLIB_CFLAGS@ \ + @EXTRA_DEFS@ \ + @DEFS@ +AM_LDFLAGS = @LIBTOOL_LDFLAGS@ + +LDADD = $(top_builddir)/lib/libnghttp2.la \ + $(top_builddir)/third-party/liburl-parser.la \ + $(top_builddir)/third-party/libllhttp.la \ + @JEMALLOC_LIBS@ \ + @LIBXML2_LIBS@ \ + @LIBEV_LIBS@ \ + @LIBNGHTTP3_LIBS@ \ + @LIBNGTCP2_CRYPTO_QUICTLS_LIBS@ \ + @LIBNGTCP2_CRYPTO_BORINGSSL_LIBS@ \ + @LIBNGTCP2_LIBS@ \ + @OPENSSL_LIBS@ \ + @LIBCARES_LIBS@ \ + @SYSTEMD_LIBS@ \ + @JANSSON_LIBS@ \ + @LIBBPF_LIBS@ \ + @ZLIB_LIBS@ \ + @APPLDFLAGS@ + +if ENABLE_APP + +bin_PROGRAMS += nghttp nghttpd nghttpx + +HELPER_OBJECTS = util.cc \ + http2.cc timegm.c app_helper.cc nghttp2_gzip.c +HELPER_HFILES = util.h \ + http2.h timegm.h app_helper.h nghttp2_config.h \ + nghttp2_gzip.h network.h + +HTML_PARSER_OBJECTS = +HTML_PARSER_HFILES = HtmlParser.h + +if HAVE_LIBXML2 +HTML_PARSER_OBJECTS += HtmlParser.cc +endif # HAVE_LIBXML2 + +nghttp_SOURCES = ${HELPER_OBJECTS} ${HELPER_HFILES} nghttp.cc nghttp.h \ + ${HTML_PARSER_OBJECTS} ${HTML_PARSER_HFILES} \ + tls.cc tls.h ssl_compat.h + +nghttpd_SOURCES = ${HELPER_OBJECTS} ${HELPER_HFILES} nghttpd.cc \ + tls.cc tls.h ssl_compat.h \ + HttpServer.cc HttpServer.h + +bin_PROGRAMS += h2load + +h2load_SOURCES = util.cc util.h \ + http2.cc http2.h h2load.cc h2load.h \ + timegm.c timegm.h \ + tls.cc tls.h ssl_compat.h \ + h2load_session.h \ + h2load_http2_session.cc h2load_http2_session.h \ + h2load_http1_session.cc h2load_http1_session.h + +if ENABLE_HTTP3 +h2load_SOURCES += \ + h2load_http3_session.cc h2load_http3_session.h \ + h2load_quic.cc h2load_quic.h \ + quic.cc quic.h +endif # ENABLE_HTTP3 + +NGHTTPX_SRCS = \ + util.cc util.h http2.cc http2.h timegm.c timegm.h base64.h \ + app_helper.cc app_helper.h \ + tls.cc tls.h ssl_compat.h \ + shrpx_config.cc shrpx_config.h \ + shrpx_error.h \ + shrpx_accept_handler.cc shrpx_accept_handler.h \ + shrpx_connection_handler.cc shrpx_connection_handler.h \ + shrpx_client_handler.cc shrpx_client_handler.h \ + shrpx_upstream.h \ + shrpx_http2_upstream.cc shrpx_http2_upstream.h \ + shrpx_https_upstream.cc shrpx_https_upstream.h \ + shrpx_downstream.cc shrpx_downstream.h \ + shrpx_downstream_connection.cc shrpx_downstream_connection.h \ + shrpx_http_downstream_connection.cc shrpx_http_downstream_connection.h \ + shrpx_http2_downstream_connection.cc shrpx_http2_downstream_connection.h \ + shrpx_http2_session.cc shrpx_http2_session.h \ + shrpx_downstream_queue.cc shrpx_downstream_queue.h \ + shrpx_log.cc shrpx_log.h \ + shrpx_http.cc shrpx_http.h \ + shrpx_io_control.cc shrpx_io_control.h \ + shrpx_tls.cc shrpx_tls.h \ + shrpx_worker.cc shrpx_worker.h \ + shrpx_log_config.cc shrpx_log_config.h \ + shrpx_connect_blocker.cc shrpx_connect_blocker.h \ + shrpx_live_check.cc shrpx_live_check.h \ + shrpx_downstream_connection_pool.cc shrpx_downstream_connection_pool.h \ + shrpx_rate_limit.cc shrpx_rate_limit.h \ + shrpx_connection.cc shrpx_connection.h \ + shrpx_memcached_dispatcher.cc shrpx_memcached_dispatcher.h \ + shrpx_memcached_connection.cc shrpx_memcached_connection.h \ + shrpx_memcached_request.h \ + shrpx_memcached_result.h \ + shrpx_worker_process.cc shrpx_worker_process.h \ + shrpx_process.h \ + shrpx_signal.cc shrpx_signal.h \ + shrpx_router.cc shrpx_router.h \ + shrpx_api_downstream_connection.cc shrpx_api_downstream_connection.h \ + shrpx_health_monitor_downstream_connection.cc \ + shrpx_health_monitor_downstream_connection.h \ + shrpx_null_downstream_connection.cc shrpx_null_downstream_connection.h \ + shrpx_exec.cc shrpx_exec.h \ + shrpx_dns_resolver.cc shrpx_dns_resolver.h \ + shrpx_dual_dns_resolver.cc shrpx_dual_dns_resolver.h \ + shrpx_dns_tracker.cc shrpx_dns_tracker.h \ + buffer.h memchunk.h template.h allocator.h \ + xsi_strerror.c xsi_strerror.h + +if HAVE_MRUBY +NGHTTPX_SRCS += \ + shrpx_mruby.cc shrpx_mruby.h \ + shrpx_mruby_module.cc shrpx_mruby_module.h \ + shrpx_mruby_module_env.cc shrpx_mruby_module_env.h \ + shrpx_mruby_module_request.cc shrpx_mruby_module_request.h \ + shrpx_mruby_module_response.cc shrpx_mruby_module_response.h +endif # HAVE_MRUBY + +if ENABLE_HTTP3 +NGHTTPX_SRCS += \ + shrpx_quic.cc shrpx_quic.h \ + shrpx_quic_listener.cc shrpx_quic_listener.h \ + shrpx_quic_connection_handler.cc shrpx_quic_connection_handler.h \ + shrpx_http3_upstream.cc shrpx_http3_upstream.h \ + http3.cc http3.h \ + quic.cc quic.h +endif # ENABLE_HTTP3 + +noinst_LIBRARIES = libnghttpx.a +libnghttpx_a_SOURCES = ${NGHTTPX_SRCS} +libnghttpx_a_CPPFLAGS = ${AM_CPPFLAGS} + +nghttpx_SOURCES = shrpx.cc shrpx.h +nghttpx_CPPFLAGS = ${libnghttpx_a_CPPFLAGS} +nghttpx_LDADD = libnghttpx.a ${LDADD} + +if HAVE_MRUBY +libnghttpx_a_CPPFLAGS += \ + -I${top_srcdir}/third-party/mruby/include @LIBMRUBY_CFLAGS@ +nghttpx_LDADD += -L${top_builddir}/third-party/mruby/build/lib @LIBMRUBY_LIBS@ +endif # HAVE_MRUBY + +if HAVE_NEVERBLEED +libnghttpx_a_CPPFLAGS += -I${top_srcdir}/third-party/neverbleed +nghttpx_LDADD += ${top_builddir}/third-party/libneverbleed.la +endif # HAVE_NEVERBLEED + +if HAVE_CUNIT +check_PROGRAMS += nghttpx-unittest +nghttpx_unittest_SOURCES = shrpx-unittest.cc \ + shrpx_tls_test.cc shrpx_tls_test.h \ + shrpx_downstream_test.cc shrpx_downstream_test.h \ + shrpx_config_test.cc shrpx_config_test.h \ + shrpx_worker_test.cc shrpx_worker_test.h \ + shrpx_http_test.cc shrpx_http_test.h \ + shrpx_router_test.cc shrpx_router_test.h \ + http2_test.cc http2_test.h \ + util_test.cc util_test.h \ + nghttp2_gzip_test.c nghttp2_gzip_test.h \ + nghttp2_gzip.c nghttp2_gzip.h \ + buffer_test.cc buffer_test.h \ + memchunk_test.cc memchunk_test.h \ + template_test.cc template_test.h \ + base64_test.cc base64_test.h +nghttpx_unittest_CPPFLAGS = ${AM_CPPFLAGS} \ + -DNGHTTP2_SRC_DIR=\"$(top_srcdir)/src\" +nghttpx_unittest_LDADD = libnghttpx.a ${LDADD} @CUNIT_LIBS@ @TESTLDADD@ + +if HAVE_MRUBY +nghttpx_unittest_CPPFLAGS += \ + -I${top_srcdir}/third-party/mruby/include @LIBMRUBY_CFLAGS@ +nghttpx_unittest_LDADD += \ + -L${top_builddir}/third-party/mruby/build/lib @LIBMRUBY_LIBS@ +endif # HAVE_MRUBY + +if HAVE_NEVERBLEED +nghttpx_unittest_CPPFLAGS += -I${top_srcdir}/third-party/neverbleed +nghttpx_unittest_LDADD += ${top_builddir}/third-party/libneverbleed.la +endif # HAVE_NEVERBLEED + +TESTS += nghttpx-unittest +endif # HAVE_CUNIT + +endif # ENABLE_APP + +if ENABLE_HPACK_TOOLS + +bin_PROGRAMS += inflatehd deflatehd + +HPACK_TOOLS_COMMON_SRCS = \ + comp_helper.c comp_helper.h \ + util.cc util.h \ + timegm.c timegm.h + +inflatehd_SOURCES = inflatehd.cc $(HPACK_TOOLS_COMMON_SRCS) + +deflatehd_SOURCES = deflatehd.cc $(HPACK_TOOLS_COMMON_SRCS) + +endif # ENABLE_HPACK_TOOLS diff --git a/src/allocator.h b/src/allocator.h new file mode 100644 index 0000000..97b9a41 --- /dev/null +++ b/src/allocator.h @@ -0,0 +1,273 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef ALLOCATOR_H +#define ALLOCATOR_H + +#include "nghttp2_config.h" + +#ifndef _WIN32 +# include <sys/uio.h> +#endif // !_WIN32 + +#include <cassert> +#include <utility> + +#include "template.h" + +namespace nghttp2 { + +struct MemBlock { + // The next MemBlock to chain them. This is for book keeping + // purpose to free them later. + MemBlock *next; + // begin is the pointer to the beginning of buffer. last is the + // location of next write. end is the one beyond of the end of the + // buffer. + uint8_t *begin, *last, *end; +}; + +// BlockAllocator allocates memory block with given size at once, and +// cuts the region from it when allocation is requested. If the +// requested size is larger than given threshold (plus small internal +// overhead), it will be allocated in a distinct buffer on demand. +// The |isolation_threshold| must be less than or equal to +// |block_size|. +struct BlockAllocator { + BlockAllocator(size_t block_size, size_t isolation_threshold) + : retain(nullptr), + head(nullptr), + block_size(block_size), + isolation_threshold(std::min(block_size, isolation_threshold)) { + assert(isolation_threshold <= block_size); + } + + ~BlockAllocator() { reset(); } + + BlockAllocator(BlockAllocator &&other) noexcept + : retain{std::exchange(other.retain, nullptr)}, + head{std::exchange(other.head, nullptr)}, + block_size(other.block_size), + isolation_threshold(other.isolation_threshold) {} + + BlockAllocator &operator=(BlockAllocator &&other) noexcept { + reset(); + + retain = std::exchange(other.retain, nullptr); + head = std::exchange(other.head, nullptr); + block_size = other.block_size; + isolation_threshold = other.isolation_threshold; + + return *this; + } + + BlockAllocator(const BlockAllocator &) = delete; + BlockAllocator &operator=(const BlockAllocator &) = delete; + + void reset() { + for (auto mb = retain; mb;) { + auto next = mb->next; + delete[] reinterpret_cast<uint8_t *>(mb); + mb = next; + } + + retain = nullptr; + head = nullptr; + } + + MemBlock *alloc_mem_block(size_t size) { + auto block = new uint8_t[sizeof(MemBlock) + size]; + auto mb = reinterpret_cast<MemBlock *>(block); + + mb->next = retain; + mb->begin = mb->last = block + sizeof(MemBlock); + mb->end = mb->begin + size; + retain = mb; + return mb; + } + + void *alloc(size_t size) { + if (size + sizeof(size_t) >= isolation_threshold) { + auto len = std::max(static_cast<size_t>(16), size); + // We will store the allocated size in size_t field. + auto mb = alloc_mem_block(len + sizeof(size_t)); + auto sp = reinterpret_cast<size_t *>(mb->begin); + *sp = len; + mb->last = mb->end; + return mb->begin + sizeof(size_t); + } + + if (!head || + head->end - head->last < static_cast<ssize_t>(size + sizeof(size_t))) { + head = alloc_mem_block(block_size); + } + + // We will store the allocated size in size_t field. + auto res = head->last + sizeof(size_t); + auto sp = reinterpret_cast<size_t *>(head->last); + *sp = size; + + head->last = reinterpret_cast<uint8_t *>( + (reinterpret_cast<intptr_t>(res + size) + 0xf) & ~0xf); + + return res; + } + + // Returns allocated size for memory pointed by |ptr|. We assume + // that |ptr| was returned from alloc() or realloc(). + size_t get_alloc_length(void *ptr) { + return *reinterpret_cast<size_t *>(static_cast<uint8_t *>(ptr) - + sizeof(size_t)); + } + + // Allocates memory of at least |size| bytes. If |ptr| is nullptr, + // this is equivalent to alloc(size). If |ptr| is not nullptr, + // obtain the allocated size for |ptr|, assuming that |ptr| was + // returned from alloc() or realloc(). If the allocated size is + // greater than or equal to size, |ptr| is returned. Otherwise, + // allocates at least |size| bytes of memory, and the original + // content pointed by |ptr| is copied to the newly allocated memory. + void *realloc(void *ptr, size_t size) { + if (!ptr) { + return alloc(size); + } + auto alloclen = get_alloc_length(ptr); + auto p = reinterpret_cast<uint8_t *>(ptr); + if (size <= alloclen) { + return ptr; + } + + auto nalloclen = std::max(size + 1, alloclen * 2); + + auto res = alloc(nalloclen); + std::copy_n(p, alloclen, static_cast<uint8_t *>(res)); + + return res; + } + + // This holds live memory block to free them in dtor. + MemBlock *retain; + // Current memory block to use. + MemBlock *head; + // size of single memory block + size_t block_size; + // if allocation greater or equal to isolation_threshold bytes is + // requested, allocate dedicated block. + size_t isolation_threshold; +}; + +// Makes a copy of |src|. The resulting string will be +// NULL-terminated. +template <typename BlockAllocator> +StringRef make_string_ref(BlockAllocator &alloc, const StringRef &src) { + auto dst = static_cast<uint8_t *>(alloc.alloc(src.size() + 1)); + auto p = dst; + p = std::copy(std::begin(src), std::end(src), p); + *p = '\0'; + return StringRef{dst, src.size()}; +} + +// private function used in concat_string_ref. this is the base +// function of concat_string_ref_count(). +inline constexpr size_t concat_string_ref_count(size_t acc) { return acc; } + +// private function used in concat_string_ref. This function counts +// the sum of length of given arguments. The calculated length is +// accumulated, and passed to the next function. +template <typename... Args> +constexpr size_t concat_string_ref_count(size_t acc, const StringRef &value, + Args &&...args) { + return concat_string_ref_count(acc + value.size(), + std::forward<Args>(args)...); +} + +// private function used in concat_string_ref. this is the base +// function of concat_string_ref_copy(). +inline uint8_t *concat_string_ref_copy(uint8_t *p) { return p; } + +// private function used in concat_string_ref. This function copies +// given strings into |p|. |p| is incremented by the copied length, +// and returned. In the end, return value points to the location one +// beyond the last byte written. +template <typename... Args> +uint8_t *concat_string_ref_copy(uint8_t *p, const StringRef &value, + Args &&...args) { + p = std::copy(std::begin(value), std::end(value), p); + return concat_string_ref_copy(p, std::forward<Args>(args)...); +} + +// Returns the string which is the concatenation of |args| in the +// given order. The resulting string will be NULL-terminated. +template <typename BlockAllocator, typename... Args> +StringRef concat_string_ref(BlockAllocator &alloc, Args &&...args) { + size_t len = concat_string_ref_count(0, std::forward<Args>(args)...); + auto dst = static_cast<uint8_t *>(alloc.alloc(len + 1)); + auto p = dst; + p = concat_string_ref_copy(p, std::forward<Args>(args)...); + *p = '\0'; + return StringRef{dst, len}; +} + +// Returns the string which is the concatenation of |value| and |args| +// in the given order. The resulting string will be NULL-terminated. +// This function assumes that the pointer value value.c_str() was +// obtained from alloc.alloc() or alloc.realloc(), and attempts to use +// unused memory region by using alloc.realloc(). If value is empty, +// then just call concat_string_ref(). +template <typename BlockAllocator, typename... Args> +StringRef realloc_concat_string_ref(BlockAllocator &alloc, + const StringRef &value, Args &&...args) { + if (value.empty()) { + return concat_string_ref(alloc, std::forward<Args>(args)...); + } + + auto len = + value.size() + concat_string_ref_count(0, std::forward<Args>(args)...); + auto dst = static_cast<uint8_t *>( + alloc.realloc(const_cast<uint8_t *>(value.byte()), len + 1)); + auto p = dst + value.size(); + p = concat_string_ref_copy(p, std::forward<Args>(args)...); + *p = '\0'; + + return StringRef{dst, len}; +} + +struct ByteRef { + // The pointer to the beginning of the buffer. + uint8_t *base; + // The length of the buffer. + size_t len; +}; + +// Makes a buffer with given size. The resulting byte string might +// not be NULL-terminated. +template <typename BlockAllocator> +ByteRef make_byte_ref(BlockAllocator &alloc, size_t size) { + auto dst = static_cast<uint8_t *>(alloc.alloc(size)); + return {dst, size}; +} + +} // namespace nghttp2 + +#endif // ALLOCATOR_H diff --git a/src/app_helper.cc b/src/app_helper.cc new file mode 100644 index 0000000..ef92762 --- /dev/null +++ b/src/app_helper.cc @@ -0,0 +1,518 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#include <netinet/tcp.h> +#include <poll.h> + +#include <cassert> +#include <cstdio> +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <string> +#include <iostream> +#include <string> +#include <set> +#include <iomanip> +#include <fstream> + +#include <zlib.h> + +#include "app_helper.h" +#include "util.h" +#include "http2.h" +#include "template.h" + +namespace nghttp2 { + +namespace { +const char *strsettingsid(int32_t id) { + switch (id) { + case NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: + return "SETTINGS_HEADER_TABLE_SIZE"; + case NGHTTP2_SETTINGS_ENABLE_PUSH: + return "SETTINGS_ENABLE_PUSH"; + case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: + return "SETTINGS_MAX_CONCURRENT_STREAMS"; + case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: + return "SETTINGS_INITIAL_WINDOW_SIZE"; + case NGHTTP2_SETTINGS_MAX_FRAME_SIZE: + return "SETTINGS_MAX_FRAME_SIZE"; + case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: + return "SETTINGS_MAX_HEADER_LIST_SIZE"; + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: + return "SETTINGS_ENABLE_CONNECT_PROTOCOL"; + case NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES: + return "SETTINGS_NO_RFC7540_PRIORITIES"; + default: + return "UNKNOWN"; + } +} +} // namespace + +namespace { +std::string strframetype(uint8_t type) { + switch (type) { + case NGHTTP2_DATA: + return "DATA"; + case NGHTTP2_HEADERS: + return "HEADERS"; + case NGHTTP2_PRIORITY: + return "PRIORITY"; + case NGHTTP2_RST_STREAM: + return "RST_STREAM"; + case NGHTTP2_SETTINGS: + return "SETTINGS"; + case NGHTTP2_PUSH_PROMISE: + return "PUSH_PROMISE"; + case NGHTTP2_PING: + return "PING"; + case NGHTTP2_GOAWAY: + return "GOAWAY"; + case NGHTTP2_WINDOW_UPDATE: + return "WINDOW_UPDATE"; + case NGHTTP2_ALTSVC: + return "ALTSVC"; + case NGHTTP2_ORIGIN: + return "ORIGIN"; + case NGHTTP2_PRIORITY_UPDATE: + return "PRIORITY_UPDATE"; + } + + std::string s = "extension(0x"; + s += util::format_hex(&type, 1); + s += ')'; + + return s; +}; +} // namespace + +namespace { +bool color_output = false; +} // namespace + +void set_color_output(bool f) { color_output = f; } + +namespace { +FILE *outfile = stdout; +} // namespace + +void set_output(FILE *file) { outfile = file; } + +namespace { +void print_frame_attr_indent() { fprintf(outfile, " "); } +} // namespace + +namespace { +const char *ansi_esc(const char *code) { return color_output ? code : ""; } +} // namespace + +namespace { +const char *ansi_escend() { return color_output ? "\033[0m" : ""; } +} // namespace + +namespace { +void print_nv(nghttp2_nv *nv) { + fprintf(outfile, "%s%s%s: %s\n", ansi_esc("\033[1;34m"), nv->name, + ansi_escend(), nv->value); +} +} // namespace +namespace { +void print_nv(nghttp2_nv *nva, size_t nvlen) { + auto end = nva + nvlen; + for (; nva != end; ++nva) { + print_frame_attr_indent(); + + print_nv(nva); + } +} +} // namespace + +void print_timer() { + auto millis = get_timer(); + fprintf(outfile, "%s[%3ld.%03ld]%s", ansi_esc("\033[33m"), + (long int)(millis.count() / 1000), (long int)(millis.count() % 1000), + ansi_escend()); +} + +namespace { +void print_frame_hd(const nghttp2_frame_hd &hd) { + fprintf(outfile, "<length=%zu, flags=0x%02x, stream_id=%d>\n", hd.length, + hd.flags, hd.stream_id); +} +} // namespace + +namespace { +void print_flags(const nghttp2_frame_hd &hd) { + std::string s; + switch (hd.type) { + case NGHTTP2_DATA: + if (hd.flags & NGHTTP2_FLAG_END_STREAM) { + s += "END_STREAM"; + } + if (hd.flags & NGHTTP2_FLAG_PADDED) { + if (!s.empty()) { + s += " | "; + } + s += "PADDED"; + } + break; + case NGHTTP2_HEADERS: + if (hd.flags & NGHTTP2_FLAG_END_STREAM) { + s += "END_STREAM"; + } + if (hd.flags & NGHTTP2_FLAG_END_HEADERS) { + if (!s.empty()) { + s += " | "; + } + s += "END_HEADERS"; + } + if (hd.flags & NGHTTP2_FLAG_PADDED) { + if (!s.empty()) { + s += " | "; + } + s += "PADDED"; + } + if (hd.flags & NGHTTP2_FLAG_PRIORITY) { + if (!s.empty()) { + s += " | "; + } + s += "PRIORITY"; + } + + break; + case NGHTTP2_PRIORITY: + break; + case NGHTTP2_SETTINGS: + if (hd.flags & NGHTTP2_FLAG_ACK) { + s += "ACK"; + } + break; + case NGHTTP2_PUSH_PROMISE: + if (hd.flags & NGHTTP2_FLAG_END_HEADERS) { + s += "END_HEADERS"; + } + if (hd.flags & NGHTTP2_FLAG_PADDED) { + if (!s.empty()) { + s += " | "; + } + s += "PADDED"; + } + break; + case NGHTTP2_PING: + if (hd.flags & NGHTTP2_FLAG_ACK) { + s += "ACK"; + } + break; + } + fprintf(outfile, "; %s\n", s.c_str()); +} +} // namespace + +enum print_type { PRINT_SEND, PRINT_RECV }; + +namespace { +const char *frame_name_ansi_esc(print_type ptype) { + return ansi_esc(ptype == PRINT_SEND ? "\033[1;35m" : "\033[1;36m"); +} +} // namespace + +namespace { +void print_frame(print_type ptype, const nghttp2_frame *frame) { + fprintf(outfile, "%s%s%s frame ", frame_name_ansi_esc(ptype), + strframetype(frame->hd.type).c_str(), ansi_escend()); + print_frame_hd(frame->hd); + if (frame->hd.flags) { + print_frame_attr_indent(); + print_flags(frame->hd); + } + switch (frame->hd.type) { + case NGHTTP2_DATA: + if (frame->data.padlen > 0) { + print_frame_attr_indent(); + fprintf(outfile, "(padlen=%zu)\n", frame->data.padlen); + } + break; + case NGHTTP2_HEADERS: + print_frame_attr_indent(); + fprintf(outfile, "(padlen=%zu", frame->headers.padlen); + if (frame->hd.flags & NGHTTP2_FLAG_PRIORITY) { + fprintf(outfile, ", dep_stream_id=%d, weight=%u, exclusive=%d", + frame->headers.pri_spec.stream_id, frame->headers.pri_spec.weight, + frame->headers.pri_spec.exclusive); + } + fprintf(outfile, ")\n"); + switch (frame->headers.cat) { + case NGHTTP2_HCAT_REQUEST: + print_frame_attr_indent(); + fprintf(outfile, "; Open new stream\n"); + break; + case NGHTTP2_HCAT_RESPONSE: + print_frame_attr_indent(); + fprintf(outfile, "; First response header\n"); + break; + case NGHTTP2_HCAT_PUSH_RESPONSE: + print_frame_attr_indent(); + fprintf(outfile, "; First push response header\n"); + break; + default: + break; + } + print_nv(frame->headers.nva, frame->headers.nvlen); + break; + case NGHTTP2_PRIORITY: + print_frame_attr_indent(); + + fprintf(outfile, "(dep_stream_id=%d, weight=%u, exclusive=%d)\n", + frame->priority.pri_spec.stream_id, frame->priority.pri_spec.weight, + frame->priority.pri_spec.exclusive); + + break; + case NGHTTP2_RST_STREAM: + print_frame_attr_indent(); + fprintf(outfile, "(error_code=%s(0x%02x))\n", + nghttp2_http2_strerror(frame->rst_stream.error_code), + frame->rst_stream.error_code); + break; + case NGHTTP2_SETTINGS: + print_frame_attr_indent(); + fprintf(outfile, "(niv=%lu)\n", + static_cast<unsigned long>(frame->settings.niv)); + for (size_t i = 0; i < frame->settings.niv; ++i) { + print_frame_attr_indent(); + fprintf(outfile, "[%s(0x%02x):%u]\n", + strsettingsid(frame->settings.iv[i].settings_id), + frame->settings.iv[i].settings_id, frame->settings.iv[i].value); + } + break; + case NGHTTP2_PUSH_PROMISE: + print_frame_attr_indent(); + fprintf(outfile, "(padlen=%zu, promised_stream_id=%d)\n", + frame->push_promise.padlen, frame->push_promise.promised_stream_id); + print_nv(frame->push_promise.nva, frame->push_promise.nvlen); + break; + case NGHTTP2_PING: + print_frame_attr_indent(); + fprintf(outfile, "(opaque_data=%s)\n", + util::format_hex(frame->ping.opaque_data, 8).c_str()); + break; + case NGHTTP2_GOAWAY: + print_frame_attr_indent(); + fprintf(outfile, + "(last_stream_id=%d, error_code=%s(0x%02x), " + "opaque_data(%u)=[%s])\n", + frame->goaway.last_stream_id, + nghttp2_http2_strerror(frame->goaway.error_code), + frame->goaway.error_code, + static_cast<unsigned int>(frame->goaway.opaque_data_len), + util::ascii_dump(frame->goaway.opaque_data, + frame->goaway.opaque_data_len) + .c_str()); + break; + case NGHTTP2_WINDOW_UPDATE: + print_frame_attr_indent(); + fprintf(outfile, "(window_size_increment=%d)\n", + frame->window_update.window_size_increment); + break; + case NGHTTP2_ALTSVC: { + auto altsvc = static_cast<nghttp2_ext_altsvc *>(frame->ext.payload); + print_frame_attr_indent(); + fprintf(outfile, "(origin=[%.*s], altsvc_field_value=[%.*s])\n", + static_cast<int>(altsvc->origin_len), altsvc->origin, + static_cast<int>(altsvc->field_value_len), altsvc->field_value); + break; + } + case NGHTTP2_ORIGIN: { + auto origin = static_cast<nghttp2_ext_origin *>(frame->ext.payload); + for (size_t i = 0; i < origin->nov; ++i) { + auto ent = &origin->ov[i]; + print_frame_attr_indent(); + fprintf(outfile, "[%.*s]\n", (int)ent->origin_len, ent->origin); + } + break; + } + case NGHTTP2_PRIORITY_UPDATE: { + auto priority_update = + static_cast<nghttp2_ext_priority_update *>(frame->ext.payload); + print_frame_attr_indent(); + fprintf(outfile, + "(prioritized_stream_id=%d, priority_field_value=[%.*s])\n", + priority_update->stream_id, + static_cast<int>(priority_update->field_value_len), + priority_update->field_value); + break; + } + default: + break; + } +} +} // namespace + +int verbose_on_header_callback(nghttp2_session *session, + const nghttp2_frame *frame, const uint8_t *name, + size_t namelen, const uint8_t *value, + size_t valuelen, uint8_t flags, + void *user_data) { + nghttp2_nv nv = {const_cast<uint8_t *>(name), const_cast<uint8_t *>(value), + namelen, valuelen}; + + print_timer(); + fprintf(outfile, " recv (stream_id=%d", frame->hd.stream_id); + if (flags & NGHTTP2_NV_FLAG_NO_INDEX) { + fprintf(outfile, ", sensitive"); + } + fprintf(outfile, ") "); + + print_nv(&nv); + fflush(outfile); + + return 0; +} + +int verbose_on_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, + void *user_data) { + print_timer(); + fprintf(outfile, " recv "); + print_frame(PRINT_RECV, frame); + fflush(outfile); + return 0; +} + +int verbose_on_invalid_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, + int lib_error_code, + void *user_data) { + print_timer(); + fprintf(outfile, " [INVALID; error=%s] recv ", + nghttp2_strerror(lib_error_code)); + print_frame(PRINT_RECV, frame); + fflush(outfile); + return 0; +} + +int verbose_on_frame_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, + void *user_data) { + print_timer(); + fprintf(outfile, " send "); + print_frame(PRINT_SEND, frame); + fflush(outfile); + return 0; +} + +int verbose_on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data) { + print_timer(); + auto srecv = + nghttp2_session_get_stream_effective_recv_data_length(session, stream_id); + auto crecv = nghttp2_session_get_effective_recv_data_length(session); + + fprintf(outfile, + " recv (stream_id=%d, length=%zu, srecv=%d, crecv=%d) DATA\n", + stream_id, len, srecv, crecv); + fflush(outfile); + + return 0; +} + +int verbose_error_callback(nghttp2_session *session, int lib_error_code, + const char *msg, size_t len, void *user_data) { + print_timer(); + fprintf(outfile, " [ERROR] %.*s\n", (int)len, msg); + fflush(outfile); + + return 0; +} + +namespace { +std::chrono::steady_clock::time_point base_tv; +} // namespace + +void reset_timer() { base_tv = std::chrono::steady_clock::now(); } + +std::chrono::milliseconds get_timer() { + return time_delta(std::chrono::steady_clock::now(), base_tv); +} + +std::chrono::steady_clock::time_point get_time() { + return std::chrono::steady_clock::now(); +} + +ssize_t deflate_data(uint8_t *out, size_t outlen, const uint8_t *in, + size_t inlen) { + int rv; + z_stream zst{}; + uint8_t temp_out[8_k]; + auto temp_outlen = sizeof(temp_out); + + rv = deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 9, + Z_DEFAULT_STRATEGY); + + if (rv != Z_OK) { + return -1; + } + + zst.avail_in = inlen; + zst.next_in = (uint8_t *)in; + zst.avail_out = temp_outlen; + zst.next_out = temp_out; + + rv = deflate(&zst, Z_FINISH); + + deflateEnd(&zst); + + if (rv != Z_STREAM_END) { + return -1; + } + + temp_outlen -= zst.avail_out; + + if (temp_outlen > outlen) { + return -1; + } + + memcpy(out, temp_out, temp_outlen); + + return temp_outlen; +} + +} // namespace nghttp2 diff --git a/src/app_helper.h b/src/app_helper.h new file mode 100644 index 0000000..5424054 --- /dev/null +++ b/src/app_helper.h @@ -0,0 +1,98 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef APP_HELPER_H +#define APP_HELPER_H + +#include "nghttp2_config.h" + +#include <cinttypes> +#include <cstdlib> +#ifdef HAVE_SYS_TIME_H +# include <sys/time.h> +#endif // HAVE_SYS_TIME_H +#include <poll.h> + +#include <map> +#include <chrono> + +#include <nghttp2/nghttp2.h> + +namespace nghttp2 { + +int verbose_on_header_callback(nghttp2_session *session, + const nghttp2_frame *frame, const uint8_t *name, + size_t namelen, const uint8_t *value, + size_t valuelen, uint8_t flags, void *user_data); + +int verbose_on_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data); + +int verbose_on_invalid_frame_recv_callback(nghttp2_session *session, + const nghttp2_frame *frame, + int lib_error_code, void *user_data); + +int verbose_on_frame_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data); + +int verbose_on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data); + +int verbose_error_callback(nghttp2_session *session, int lib_error_code, + const char *msg, size_t len, void *user_data); + +// Returns difference between |a| and |b| in milliseconds, assuming +// |a| is more recent than |b|. +template <typename TimePoint> +std::chrono::milliseconds time_delta(const TimePoint &a, const TimePoint &b) { + return std::chrono::duration_cast<std::chrono::milliseconds>(a - b); +} + +// Resets timer +void reset_timer(); + +// Returns the duration since timer reset. +std::chrono::milliseconds get_timer(); + +// Returns current time point. +std::chrono::steady_clock::time_point get_time(); + +void print_timer(); + +// Setting true will print characters with ANSI color escape codes +// when printing HTTP2 frames. This function changes a static +// variable. +void set_color_output(bool f); + +// Set output file when printing HTTP2 frames. By default, stdout is +// used. +void set_output(FILE *file); + +ssize_t deflate_data(uint8_t *out, size_t outlen, const uint8_t *in, + size_t inlen); + +} // namespace nghttp2 + +#endif // APP_HELPER_H diff --git a/src/base64.h b/src/base64.h new file mode 100644 index 0000000..1bd51af --- /dev/null +++ b/src/base64.h @@ -0,0 +1,225 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef BASE64_H +#define BASE64_H + +#include "nghttp2_config.h" + +#include <string> + +#include "template.h" +#include "allocator.h" + +namespace nghttp2 { + +namespace base64 { + +namespace { +constexpr char B64_CHARS[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', +}; +} // namespace + +template <typename InputIt> std::string encode(InputIt first, InputIt last) { + std::string res; + size_t len = last - first; + if (len == 0) { + return res; + } + size_t r = len % 3; + res.resize((len + 2) / 3 * 4); + auto j = last - r; + auto p = std::begin(res); + while (first != j) { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + n += static_cast<uint8_t>(*first++) << 8; + n += static_cast<uint8_t>(*first++); + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = B64_CHARS[(n >> 6) & 0x3fu]; + *p++ = B64_CHARS[n & 0x3fu]; + } + + if (r == 2) { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + n += static_cast<uint8_t>(*first++) << 8; + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = B64_CHARS[(n >> 6) & 0x3fu]; + *p++ = '='; + } else if (r == 1) { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = '='; + *p++ = '='; + } + return res; +} + +constexpr size_t encode_length(size_t n) { return (n + 2) / 3 * 4; } + +template <typename InputIt, typename OutputIt> +OutputIt encode(InputIt first, InputIt last, OutputIt d_first) { + size_t len = last - first; + if (len == 0) { + return d_first; + } + auto r = len % 3; + auto j = last - r; + auto p = d_first; + while (first != j) { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + n += static_cast<uint8_t>(*first++) << 8; + n += static_cast<uint8_t>(*first++); + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = B64_CHARS[(n >> 6) & 0x3fu]; + *p++ = B64_CHARS[n & 0x3fu]; + } + + switch (r) { + case 2: { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + n += static_cast<uint8_t>(*first++) << 8; + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = B64_CHARS[(n >> 6) & 0x3fu]; + *p++ = '='; + break; + } + case 1: { + uint32_t n = static_cast<uint8_t>(*first++) << 16; + *p++ = B64_CHARS[n >> 18]; + *p++ = B64_CHARS[(n >> 12) & 0x3fu]; + *p++ = '='; + *p++ = '='; + break; + } + } + return p; +} + +template <typename InputIt> +InputIt next_decode_input(InputIt first, InputIt last, const int *tbl) { + for (; first != last; ++first) { + if (tbl[static_cast<size_t>(*first)] != -1 || *first == '=') { + break; + } + } + return first; +} + +template <typename InputIt, typename OutputIt> +OutputIt decode(InputIt first, InputIt last, OutputIt d_first) { + static constexpr int INDEX_TABLE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1}; + assert(std::distance(first, last) % 4 == 0); + auto p = d_first; + for (; first != last;) { + uint32_t n = 0; + for (int i = 1; i <= 4; ++i, ++first) { + auto idx = INDEX_TABLE[static_cast<size_t>(*first)]; + if (idx == -1) { + if (i <= 2) { + return d_first; + } + if (i == 3) { + if (*first == '=' && *(first + 1) == '=' && first + 2 == last) { + *p++ = n >> 16; + return p; + } + return d_first; + } + if (*first == '=' && first + 1 == last) { + *p++ = n >> 16; + *p++ = n >> 8 & 0xffu; + return p; + } + return d_first; + } + + n += idx << (24 - i * 6); + } + + *p++ = n >> 16; + *p++ = n >> 8 & 0xffu; + *p++ = n & 0xffu; + } + + return p; +} + +template <typename InputIt> std::string decode(InputIt first, InputIt last) { + auto len = std::distance(first, last); + if (len % 4 != 0) { + return ""; + } + std::string res; + res.resize(len / 4 * 3); + + res.erase(decode(first, last, std::begin(res)), std::end(res)); + + return res; +} + +template <typename InputIt> +StringRef decode(BlockAllocator &balloc, InputIt first, InputIt last) { + auto len = std::distance(first, last); + if (len % 4 != 0) { + return StringRef::from_lit(""); + } + auto iov = make_byte_ref(balloc, len / 4 * 3 + 1); + auto p = iov.base; + + p = decode(first, last, p); + *p = '\0'; + + return StringRef{iov.base, p}; +} + +} // namespace base64 + +} // namespace nghttp2 + +#endif // BASE64_H diff --git a/src/base64_test.cc b/src/base64_test.cc new file mode 100644 index 0000000..4324bd7 --- /dev/null +++ b/src/base64_test.cc @@ -0,0 +1,121 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "base64_test.h" + +#include <cstring> +#include <iostream> + +#include <CUnit/CUnit.h> + +#include <nghttp2/nghttp2.h> + +#include "base64.h" + +namespace nghttp2 { + +void test_base64_encode(void) { + { + std::string in = "\xff"; + auto out = base64::encode(std::begin(in), std::end(in)); + CU_ASSERT("/w==" == out); + } + { + std::string in = "\xff\xfe"; + auto out = base64::encode(std::begin(in), std::end(in)); + CU_ASSERT("//4=" == out); + } + { + std::string in = "\xff\xfe\xfd"; + auto out = base64::encode(std::begin(in), std::end(in)); + CU_ASSERT("//79" == out); + } + { + std::string in = "\xff\xfe\xfd\xfc"; + auto out = base64::encode(std::begin(in), std::end(in)); + CU_ASSERT("//79/A==" == out); + } +} + +void test_base64_decode(void) { + BlockAllocator balloc(4096, 4096); + { + std::string in = "/w=="; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("\xff" == out); + CU_ASSERT("\xff" == base64::decode(balloc, std::begin(in), std::end(in))); + } + { + std::string in = "//4="; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("\xff\xfe" == out); + CU_ASSERT("\xff\xfe" == + base64::decode(balloc, std::begin(in), std::end(in))); + } + { + std::string in = "//79"; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("\xff\xfe\xfd" == out); + CU_ASSERT("\xff\xfe\xfd" == + base64::decode(balloc, std::begin(in), std::end(in))); + } + { + std::string in = "//79/A=="; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("\xff\xfe\xfd\xfc" == out); + CU_ASSERT("\xff\xfe\xfd\xfc" == + base64::decode(balloc, std::begin(in), std::end(in))); + } + { + // we check the number of valid input must be multiples of 4 + std::string in = "//79="; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("" == out); + CU_ASSERT("" == base64::decode(balloc, std::begin(in), std::end(in))); + } + { + // ending invalid character at the boundary of multiples of 4 is + // bad + std::string in = "bmdodHRw\n"; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("" == out); + CU_ASSERT("" == base64::decode(balloc, std::begin(in), std::end(in))); + } + { + // after seeing '=', subsequent input must be also '='. + std::string in = "//79/A=A"; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("" == out); + CU_ASSERT("" == base64::decode(balloc, std::begin(in), std::end(in))); + } + { + // additional '=' at the end is bad + std::string in = "//79/A======"; + auto out = base64::decode(std::begin(in), std::end(in)); + CU_ASSERT("" == out); + CU_ASSERT("" == base64::decode(balloc, std::begin(in), std::end(in))); + } +} + +} // namespace nghttp2 diff --git a/src/base64_test.h b/src/base64_test.h new file mode 100644 index 0000000..8bdb84f --- /dev/null +++ b/src/base64_test.h @@ -0,0 +1,39 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef BASE64_TEST_H +#define BASE64_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace nghttp2 { + +void test_base64_encode(void); +void test_base64_decode(void); + +} // namespace nghttp2 + +#endif // BASE64_TEST_H diff --git a/src/buffer.h b/src/buffer.h new file mode 100644 index 0000000..1921edf --- /dev/null +++ b/src/buffer.h @@ -0,0 +1,78 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef BUFFER_H +#define BUFFER_H + +#include "nghttp2_config.h" + +#include <cstring> +#include <algorithm> +#include <array> + +namespace nghttp2 { + +template <size_t N> struct Buffer { + Buffer() : pos(std::begin(buf)), last(pos) {} + // Returns the number of bytes to read. + size_t rleft() const { return last - pos; } + // Returns the number of bytes this buffer can store. + size_t wleft() const { return std::end(buf) - last; } + // Writes up to min(wleft(), |count|) bytes from buffer pointed by + // |src|. Returns number of bytes written. + size_t write(const void *src, size_t count) { + count = std::min(count, wleft()); + auto p = static_cast<const uint8_t *>(src); + last = std::copy_n(p, count, last); + return count; + } + size_t write(size_t count) { + count = std::min(count, wleft()); + last += count; + return count; + } + // Drains min(rleft(), |count|) bytes from start of the buffer. + size_t drain(size_t count) { + count = std::min(count, rleft()); + pos += count; + return count; + } + size_t drain_reset(size_t count) { + count = std::min(count, rleft()); + std::copy(pos + count, last, std::begin(buf)); + last = std::begin(buf) + (last - (pos + count)); + pos = std::begin(buf); + return count; + } + void reset() { pos = last = std::begin(buf); } + uint8_t *begin() { return std::begin(buf); } + uint8_t &operator[](size_t n) { return buf[n]; } + const uint8_t &operator[](size_t n) const { return buf[n]; } + std::array<uint8_t, N> buf; + uint8_t *pos, *last; +}; + +} // namespace nghttp2 + +#endif // BUFFER_H diff --git a/src/buffer_test.cc b/src/buffer_test.cc new file mode 100644 index 0000000..38688ed --- /dev/null +++ b/src/buffer_test.cc @@ -0,0 +1,78 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "buffer_test.h" + +#include <cstring> +#include <iostream> +#include <tuple> + +#include <CUnit/CUnit.h> + +#include <nghttp2/nghttp2.h> + +#include "buffer.h" + +namespace nghttp2 { + +void test_buffer_write(void) { + Buffer<16> b; + CU_ASSERT(0 == b.rleft()); + CU_ASSERT(16 == b.wleft()); + + b.write("012", 3); + + CU_ASSERT(3 == b.rleft()); + CU_ASSERT(13 == b.wleft()); + CU_ASSERT(b.pos == std::begin(b.buf)); + + b.drain(3); + + CU_ASSERT(0 == b.rleft()); + CU_ASSERT(13 == b.wleft()); + CU_ASSERT(3 == b.pos - std::begin(b.buf)); + + auto n = b.write("0123456789ABCDEF", 16); + + CU_ASSERT(n == 13); + + CU_ASSERT(13 == b.rleft()); + CU_ASSERT(0 == b.wleft()); + CU_ASSERT(3 == b.pos - std::begin(b.buf)); + CU_ASSERT(0 == memcmp(b.pos, "0123456789ABC", 13)); + + b.reset(); + + CU_ASSERT(0 == b.rleft()); + CU_ASSERT(16 == b.wleft()); + CU_ASSERT(b.pos == std::begin(b.buf)); + + b.write(5); + + CU_ASSERT(5 == b.rleft()); + CU_ASSERT(11 == b.wleft()); + CU_ASSERT(b.pos == std::begin(b.buf)); +} + +} // namespace nghttp2 diff --git a/src/buffer_test.h b/src/buffer_test.h new file mode 100644 index 0000000..6789aa3 --- /dev/null +++ b/src/buffer_test.h @@ -0,0 +1,38 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef BUFFER_TEST_H +#define BUFFER_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace nghttp2 { + +void test_buffer_write(void); + +} // namespace nghttp2 + +#endif // BUFFER_TEST_H diff --git a/src/ca-config.json b/src/ca-config.json new file mode 100644 index 0000000..5788d07 --- /dev/null +++ b/src/ca-config.json @@ -0,0 +1,17 @@ +{ + "signing": { + "default": { + "expiry": "87600h" + }, + "profiles": { + "server": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + } + } + } +} diff --git a/src/ca.nghttp2.org-key.pem b/src/ca.nghttp2.org-key.pem new file mode 100644 index 0000000..6ce8707 --- /dev/null +++ b/src/ca.nghttp2.org-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA1kVkF8QSUwW/HV9EFRPSMoiOVmYwB8vqKDtT0d6MFiKAM8/Y +JFUq2uKlUydgT4IPE7PATvVcIj3GtL9XzPhscqYO/S0Y7scyTE2VAPmtz+StPWf2 +wZ1IQR09HrnDTc44KvYGZpefBZkD9UjbmJ9a1ZmJjJiMr3hTnKE/sxZ2+dMsnMZX +N822cfaHyTN+T0+Tyw5vBBboCDsZzxmf+9FFIDJNs3NL34cR8EZRhpfaegapH8bt +OJ+D+RZ2kg7E/YYkGcS6NodvTjSUFCFHpWjHCfTFhn/owBIAooCdWorh6dc8Q72l +AodwNLXS8uuPgPqM5s4Cz57m7Zgs4OilNmIdawIDAQABAoIBAQCwqLtygLye6KD+ +RXorapEmCsJX5553/x6Klwdvg+25ni5XCWjp47IWj0DBQzi7tL5bfxrxvod8z7QR +d6SbIMLA77px8Ima7G7CzEAqcrBkM+TFOP8P+G4HCWVH/N5SOtDCUt9KHH4Grna9 +95jdx5yreRAX8/oh/bHp9GRBcicbpwYMVWOnjTE2seEUYQOpdpYdP4bOPUvAju0l +mwmy2/dDGmbibktN3sdHEhDodKu+Znv7nFZo0jzhlyoXse653WcvaQeZZYuojvSe +Sr92DvPp7UaYrb4KvT7ujXiPavSV2m/4EmGtyqevUf2dZ6sfMXZjmXsjWz9txhWp +4BgbHyHRAoGBAPqyuNj2CDD3FE7N3Hxyba8d+ZtsVUNawjq2gwOvT9NLsMstOGyH +OCc1v4W6Sq4w1wo4nIJyY8kNZwtReaTHOPZlDgBhVvk/x8eLBu+QTMRyocRt1LoD +8HyKxWSAnYTtCh/GUEQ37amIqvOJ5GNL+25WDzevLa5kMYWG743uxEupAoGBANrN +c/fVxepvP0GISlLpL3aZCFGAjMrq3xUYcf/w4wPoMq6AdpIPeRVBmJ1/Uqw1FkV8 +NRKJNPE2YcMuv8iMeQlacoPd34KT9ob80EYVlMwAkeC0NK+FfiM/UteR0wB49gmi +ugX9YlJytOP9aUgPvEGT6l+XtgGC44W1TQWe62zzAoGBAKZenNU+0UjNb6isbToZ +Jjkkh1Vhm2PLg0I7hM6ZNTxf6r+rDtrXEajTvnocmxrmRo796r+W8immv09/jl6P +53l8rsIJ1xIqBYai+MNa29cyy6/zw0x++MVtwnlj8SUZubJEhVgAVbRAglKEnBBZ +iE48xnSJyKMG0uZuGePzJEmhAoGBAIOHJcNBumum3DuklikpC+MbMyjrQbdpYRjp +TP4x7AWZO34ysxQyQPNKL1feBfCHKRA0DiNKX4zwx+vw2lDQQKIiwNwMMCPqljOn +HfxDVOMdJJQTP+iTMrQ1iLMVceXC0QQR0glvu/8b/SlgWD19WAmDxUwZgst9xw/F +YLuUQKmJAoGAREeTugd4hc0U/YV/BQQjSCLhl11EtVry/oQMHj8KZpIJhP7tj8lw +hSE0+z04oMhiTeq55PYKQkTo5l6V4PW0zfpEwlKEEm0erab1G9Ddh7us47XFcKLl +Rmk192EVZ0lQuzftsYv7dzRLiAR7yDFXwD1ELIK/uPkwBtu7wtHlq+M= +-----END RSA PRIVATE KEY----- diff --git a/src/ca.nghttp2.org.csr b/src/ca.nghttp2.org.csr new file mode 100644 index 0000000..37ee560 --- /dev/null +++ b/src/ca.nghttp2.org.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICwjCCAaoCAQAwXjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx +ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEXMBUGA1UEAxMOY2Eu +bmdodHRwMi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWRWQX +xBJTBb8dX0QVE9IyiI5WZjAHy+ooO1PR3owWIoAzz9gkVSra4qVTJ2BPgg8Ts8BO +9VwiPca0v1fM+Gxypg79LRjuxzJMTZUA+a3P5K09Z/bBnUhBHT0eucNNzjgq9gZm +l58FmQP1SNuYn1rVmYmMmIyveFOcoT+zFnb50yycxlc3zbZx9ofJM35PT5PLDm8E +FugIOxnPGZ/70UUgMk2zc0vfhxHwRlGGl9p6Bqkfxu04n4P5FnaSDsT9hiQZxLo2 +h29ONJQUIUelaMcJ9MWGf+jAEgCigJ1aiuHp1zxDvaUCh3A0tdLy64+A+ozmzgLP +nubtmCzg6KU2Yh1rAgMBAAGgHzAdBgkqhkiG9w0BCQ4xEDAOMAwGA1UdEwQFMAMB +Af8wDQYJKoZIhvcNAQELBQADggEBACI5v8GbOXKv38h9/tuGEwJ9uxpYEljgGt8h +QL5lwfEifh/7A8b39b9JEzWk5hnMRCOb8J6Jc3/6nmVgtKkQ+Mceupqpwsp1gT/v +uUoAkJE03Iuja9zLhHmy74oZ7LWOQrZ1T7Z0eGQ+5u+LBZiPKnKxmkLCQoUPTbc4 +NQ9BbKhr8OaoJ4DDvJnszcL7to6kih7SkdoNZsq4zB0/ai/cPhvoVgkYfbLH2++D +Tcs7TqU2L7gKzqXUtHeAKM2y81ewL7QTrcYzgiW86s3NmquxZG5pq0mjD+P4BYLc +MOdnCxKbBuE/1R29pa6+JKgc46jOa2yRgv5+8rXkkpu53Ke3FGc= +-----END CERTIFICATE REQUEST----- diff --git a/src/ca.nghttp2.org.csr.json b/src/ca.nghttp2.org.csr.json new file mode 100644 index 0000000..69d9c6a --- /dev/null +++ b/src/ca.nghttp2.org.csr.json @@ -0,0 +1,17 @@ +{ + "CN": "ca.nghttp2.org", + "key": { + "algo": "rsa", + "size": 2048 + }, + "ca": { + "expiry": "87600h" + }, + "names": [ + { + "C": "AU", + "ST": "Some-State", + "O": "Internet Widgits Pty Ltd" + } + ] +} diff --git a/src/ca.nghttp2.org.pem b/src/ca.nghttp2.org.pem new file mode 100644 index 0000000..e50acfc --- /dev/null +++ b/src/ca.nghttp2.org.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrTCCApWgAwIBAgIUe4dvx8haIjsT3ZpNCMrl62Xk6E0wDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoT +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEXMBUGA1UEAxMOY2EubmdodHRwMi5v +cmcwHhcNMTYwNjI1MDkzMzAwWhcNMjYwNjIzMDkzMzAwWjBeMQswCQYDVQQGEwJB +VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRcwFQYDVQQDEw5jYS5uZ2h0dHAyLm9yZzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANZFZBfEElMFvx1fRBUT0jKIjlZmMAfL6ig7U9He +jBYigDPP2CRVKtripVMnYE+CDxOzwE71XCI9xrS/V8z4bHKmDv0tGO7HMkxNlQD5 +rc/krT1n9sGdSEEdPR65w03OOCr2BmaXnwWZA/VI25ifWtWZiYyYjK94U5yhP7MW +dvnTLJzGVzfNtnH2h8kzfk9Pk8sObwQW6Ag7Gc8Zn/vRRSAyTbNzS9+HEfBGUYaX +2noGqR/G7Tifg/kWdpIOxP2GJBnEujaHb040lBQhR6Voxwn0xYZ/6MASAKKAnVqK +4enXPEO9pQKHcDS10vLrj4D6jObOAs+e5u2YLODopTZiHWsCAwEAAaNjMGEwDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNA5xVR1Zcax +RJL9VC6pzuLmvduGMB8GA1UdIwQYMBaAFNA5xVR1ZcaxRJL9VC6pzuLmvduGMA0G +CSqGSIb3DQEBCwUAA4IBAQCmdVfn/hUyEdvkKG7svg5d8o6BENOj8695KtWmzJjK +zxH8J5Vy3mn89XrHQ+BOYXCDPyhs0aDS8aq3Z+HY0n9z1oAicyGzlVwZQQNX3YId +Y2vcf7qu/2ATm/1S+mebE1/EXMUlWISKKUYXjggCwFgjDhH87Ai+A8MKScVdmqgL +Hf+fRSzH3ToW7BCXlRl5bPAq2g+v1ALYc8wU9cT1MYm4dqAXh870LGFyUpaSWmFr +TtX1DXBTgLp62syNlDthAvGigYFDtCa4cDM2vdTD9wpec2V9EKpfVqiRDDuYjUVX +UXl27MvkNWnEBKCIoNv5abWXpZVG2zQdEMmUOkVuAXUC +-----END CERTIFICATE----- diff --git a/src/comp_helper.c b/src/comp_helper.c new file mode 100644 index 0000000..98db08a --- /dev/null +++ b/src/comp_helper.c @@ -0,0 +1,133 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "comp_helper.h" +#include <string.h> + +static void dump_val(json_t *jent, const char *key, uint8_t *val, size_t len) { + json_object_set_new(jent, key, json_pack("s#", val, len)); +} + +#define NGHTTP2_HD_ENTRY_OVERHEAD 32 + +json_t *dump_deflate_header_table(nghttp2_hd_deflater *deflater) { + json_t *obj, *entries; + size_t i; + size_t len = nghttp2_hd_deflate_get_num_table_entries(deflater); + + obj = json_object(); + entries = json_array(); + /* The first index of dynamic table is 62 */ + for (i = 62; i <= len; ++i) { + const nghttp2_nv *nv = nghttp2_hd_deflate_get_table_entry(deflater, i); + json_t *outent = json_object(); + json_object_set_new(outent, "index", json_integer((json_int_t)i)); + dump_val(outent, "name", nv->name, nv->namelen); + dump_val(outent, "value", nv->value, nv->valuelen); + json_object_set_new(outent, "size", + json_integer((json_int_t)(nv->namelen + nv->valuelen + + NGHTTP2_HD_ENTRY_OVERHEAD))); + json_array_append_new(entries, outent); + } + json_object_set_new(obj, "entries", entries); + json_object_set_new( + obj, "size", + json_integer( + (json_int_t)nghttp2_hd_deflate_get_dynamic_table_size(deflater))); + json_object_set_new( + obj, "max_size", + json_integer( + (json_int_t)nghttp2_hd_deflate_get_max_dynamic_table_size(deflater))); + + return obj; +} + +json_t *dump_inflate_header_table(nghttp2_hd_inflater *inflater) { + json_t *obj, *entries; + size_t i; + size_t len = nghttp2_hd_inflate_get_num_table_entries(inflater); + + obj = json_object(); + entries = json_array(); + /* The first index of dynamic table is 62 */ + for (i = 62; i <= len; ++i) { + const nghttp2_nv *nv = nghttp2_hd_inflate_get_table_entry(inflater, i); + json_t *outent = json_object(); + json_object_set_new(outent, "index", json_integer((json_int_t)i)); + dump_val(outent, "name", nv->name, nv->namelen); + dump_val(outent, "value", nv->value, nv->valuelen); + json_object_set_new(outent, "size", + json_integer((json_int_t)(nv->namelen + nv->valuelen + + NGHTTP2_HD_ENTRY_OVERHEAD))); + json_array_append_new(entries, outent); + } + json_object_set_new(obj, "entries", entries); + json_object_set_new( + obj, "size", + json_integer( + (json_int_t)nghttp2_hd_inflate_get_dynamic_table_size(inflater))); + json_object_set_new( + obj, "max_size", + json_integer( + (json_int_t)nghttp2_hd_inflate_get_max_dynamic_table_size(inflater))); + + return obj; +} + +json_t *dump_header(const uint8_t *name, size_t namelen, const uint8_t *value, + size_t valuelen) { + json_t *nv_pair = json_object(); + char *cname = malloc(namelen + 1); + if (cname == NULL) { + return NULL; + } + memcpy(cname, name, namelen); + cname[namelen] = '\0'; + json_object_set_new(nv_pair, cname, json_pack("s#", value, valuelen)); + free(cname); + return nv_pair; +} + +json_t *dump_headers(const nghttp2_nv *nva, size_t nvlen) { + json_t *headers; + size_t i; + + headers = json_array(); + for (i = 0; i < nvlen; ++i) { + json_array_append_new(headers, dump_header(nva[i].name, nva[i].namelen, + nva[i].value, nva[i].valuelen)); + } + return headers; +} + +void output_json_header(void) { + printf("{\n" + " \"cases\":\n" + " [\n"); +} + +void output_json_footer(void) { + printf(" ]\n" + "}\n"); +} diff --git a/src/comp_helper.h b/src/comp_helper.h new file mode 100644 index 0000000..131ed21 --- /dev/null +++ b/src/comp_helper.h @@ -0,0 +1,57 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP2_COMP_HELPER_H +#define NGHTTP2_COMP_HELPER_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <jansson.h> + +#include <nghttp2/nghttp2.h> + +#ifdef __cplusplus +extern "C" { +#endif + +json_t *dump_deflate_header_table(nghttp2_hd_deflater *deflater); + +json_t *dump_inflate_header_table(nghttp2_hd_inflater *inflater); + +json_t *dump_header(const uint8_t *name, size_t namelen, const uint8_t *value, + size_t vlauelen); + +json_t *dump_headers(const nghttp2_nv *nva, size_t nvlen); + +void output_json_header(void); + +void output_json_footer(void); + +#ifdef __cplusplus +} +#endif + +#endif /* NGHTTP2_COMP_HELPER_H */ diff --git a/src/deflatehd.cc b/src/deflatehd.cc new file mode 100644 index 0000000..7dcfccf --- /dev/null +++ b/src/deflatehd.cc @@ -0,0 +1,450 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <getopt.h> + +#include <cstdio> +#include <cstring> +#include <cassert> +#include <cerrno> +#include <cstdlib> +#include <vector> +#include <iostream> + +#include <jansson.h> + +#include <nghttp2/nghttp2.h> + +#include "template.h" +#include "comp_helper.h" +#include "util.h" + +namespace nghttp2 { + +typedef struct { + size_t table_size; + size_t deflate_table_size; + int http1text; + int dump_header_table; +} deflate_config; + +static deflate_config config; + +static size_t input_sum; +static size_t output_sum; + +static char to_hex_digit(uint8_t n) { + if (n > 9) { + return n - 10 + 'a'; + } + return n + '0'; +} + +static void to_hex(char *dest, const uint8_t *src, size_t len) { + size_t i; + for (i = 0; i < len; ++i) { + *dest++ = to_hex_digit(src[i] >> 4); + *dest++ = to_hex_digit(src[i] & 0xf); + } +} + +static void output_to_json(nghttp2_hd_deflater *deflater, const uint8_t *buf, + size_t buflen, size_t inputlen, + const std::vector<nghttp2_nv> &nva, int seq) { + auto hex = std::vector<char>(buflen * 2); + auto obj = json_object(); + auto comp_ratio = inputlen == 0 ? 0.0 : (double)buflen / inputlen * 100; + + json_object_set_new(obj, "seq", json_integer(seq)); + json_object_set_new(obj, "input_length", json_integer(inputlen)); + json_object_set_new(obj, "output_length", json_integer(buflen)); + json_object_set_new(obj, "percentage_of_original_size", + json_real(comp_ratio)); + + if (buflen == 0) { + json_object_set_new(obj, "wire", json_string("")); + } else { + to_hex(hex.data(), buf, buflen); + json_object_set_new(obj, "wire", json_pack("s#", hex.data(), hex.size())); + } + json_object_set_new(obj, "headers", dump_headers(nva.data(), nva.size())); + if (seq == 0) { + // We only change the header table size only once at the beginning + json_object_set_new(obj, "header_table_size", + json_integer(config.table_size)); + } + if (config.dump_header_table) { + json_object_set_new(obj, "header_table", + dump_deflate_header_table(deflater)); + } + json_dumpf(obj, stdout, JSON_PRESERVE_ORDER | JSON_INDENT(2)); + printf("\n"); + json_decref(obj); +} + +static void deflate_hd(nghttp2_hd_deflater *deflater, + const std::vector<nghttp2_nv> &nva, size_t inputlen, + int seq) { + ssize_t rv; + std::array<uint8_t, 64_k> buf; + + rv = nghttp2_hd_deflate_hd(deflater, buf.data(), buf.size(), + (nghttp2_nv *)nva.data(), nva.size()); + if (rv < 0) { + fprintf(stderr, "deflate failed with error code %zd at %d\n", rv, seq); + exit(EXIT_FAILURE); + } + + input_sum += inputlen; + output_sum += rv; + + output_to_json(deflater, buf.data(), rv, inputlen, nva, seq); +} + +static int deflate_hd_json(json_t *obj, nghttp2_hd_deflater *deflater, + int seq) { + size_t inputlen = 0; + + auto js = json_object_get(obj, "headers"); + if (js == nullptr) { + fprintf(stderr, "'headers' key is missing at %d\n", seq); + return -1; + } + if (!json_is_array(js)) { + fprintf(stderr, "The value of 'headers' key must be an array at %d\n", seq); + return -1; + } + + auto len = json_array_size(js); + auto nva = std::vector<nghttp2_nv>(len); + + for (size_t i = 0; i < len; ++i) { + auto nv_pair = json_array_get(js, i); + const char *name; + json_t *value; + + if (!json_is_object(nv_pair) || json_object_size(nv_pair) != 1) { + fprintf(stderr, "bad formatted name/value pair object at %d\n", seq); + return -1; + } + + json_object_foreach(nv_pair, name, value) { + nva[i].name = (uint8_t *)name; + nva[i].namelen = strlen(name); + + if (!json_is_string(value)) { + fprintf(stderr, "value is not string at %d\n", seq); + return -1; + } + + nva[i].value = (uint8_t *)json_string_value(value); + nva[i].valuelen = strlen(json_string_value(value)); + + nva[i].flags = NGHTTP2_NV_FLAG_NONE; + } + + inputlen += nva[i].namelen + nva[i].valuelen; + } + + deflate_hd(deflater, nva, inputlen, seq); + + return 0; +} + +static nghttp2_hd_deflater *init_deflater() { + nghttp2_hd_deflater *deflater; + nghttp2_hd_deflate_new(&deflater, config.deflate_table_size); + if (config.table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { + nghttp2_hd_deflate_change_table_size(deflater, config.table_size); + } + return deflater; +} + +static void deinit_deflater(nghttp2_hd_deflater *deflater) { + nghttp2_hd_deflate_del(deflater); +} + +static int perform(void) { + json_error_t error; + + auto json = json_loadf(stdin, 0, &error); + + if (json == nullptr) { + fprintf(stderr, "JSON loading failed\n"); + exit(EXIT_FAILURE); + } + + auto cases = json_object_get(json, "cases"); + + if (cases == nullptr) { + fprintf(stderr, "Missing 'cases' key in root object\n"); + exit(EXIT_FAILURE); + } + + if (!json_is_array(cases)) { + fprintf(stderr, "'cases' must be JSON array\n"); + exit(EXIT_FAILURE); + } + + auto deflater = init_deflater(); + output_json_header(); + auto len = json_array_size(cases); + + for (size_t i = 0; i < len; ++i) { + auto obj = json_array_get(cases, i); + if (!json_is_object(obj)) { + fprintf(stderr, "Unexpected JSON type at %zu. It should be object.\n", i); + continue; + } + if (deflate_hd_json(obj, deflater, i) != 0) { + continue; + } + if (i + 1 < len) { + printf(",\n"); + } + } + output_json_footer(); + deinit_deflater(deflater); + json_decref(json); + return 0; +} + +static int perform_from_http1text(void) { + char line[1 << 14]; + int seq = 0; + + auto deflater = init_deflater(); + output_json_header(); + for (;;) { + std::vector<nghttp2_nv> nva; + int end = 0; + size_t inputlen = 0; + + for (;;) { + char *rv = fgets(line, sizeof(line), stdin); + char *val, *val_end; + if (rv == nullptr) { + end = 1; + break; + } else if (line[0] == '\n') { + break; + } + + nva.emplace_back(); + auto &nv = nva.back(); + + val = strchr(line + 1, ':'); + if (val == nullptr) { + fprintf(stderr, "Bad HTTP/1 header field format at %d.\n", seq); + exit(EXIT_FAILURE); + } + *val = '\0'; + ++val; + for (; *val && (*val == ' ' || *val == '\t'); ++val) + ; + for (val_end = val; *val_end && (*val_end != '\r' && *val_end != '\n'); + ++val_end) + ; + *val_end = '\0'; + + nv.namelen = strlen(line); + nv.valuelen = strlen(val); + nv.name = (uint8_t *)strdup(line); + nv.value = (uint8_t *)strdup(val); + nv.flags = NGHTTP2_NV_FLAG_NONE; + + inputlen += nv.namelen + nv.valuelen; + } + + if (!end) { + if (seq > 0) { + printf(",\n"); + } + deflate_hd(deflater, nva, inputlen, seq); + } + + for (auto &nv : nva) { + free(nv.name); + free(nv.value); + } + + if (end) + break; + ++seq; + } + output_json_footer(); + deinit_deflater(deflater); + return 0; +} + +static void print_help(void) { + std::cout << R"(HPACK HTTP/2 header encoder +Usage: deflatehd [OPTIONS] < INPUT + +Reads JSON data or HTTP/1-style header fields from stdin and outputs +deflated header block in JSON array. + +For the JSON input, the root JSON object must contain "context" key, +which indicates which compression context is used. If it is +"request", request compression context is used. Otherwise, response +compression context is used. The value of "cases" key contains the +sequence of input header set. They share the same compression context +and are processed in the order they appear. Each item in the sequence +is a JSON object and it must have at least "headers" key. Its value +is an array of a JSON object containing exactly one name/value pair. + +Example: +{ + "context": "request", + "cases": + [ + { + "headers": [ + { ":method": "GET" }, + { ":path": "/" } + ] + }, + { + "headers": [ + { ":method": "POST" }, + { ":path": "/" } + ] + } + ] +} + +With -t option, the program can accept more familiar HTTP/1 style +header field block. Each header set must be followed by one empty +line: + +Example: + +:method: GET +:scheme: https +:path: / + +:method: POST +user-agent: nghttp2 + +The output of this program can be used as input for inflatehd. + +OPTIONS: + -t, --http1text Use HTTP/1 style header field text as input. + Each header set is delimited by single empty + line. + -s, --table-size=<N> + Set dynamic table size. In the HPACK + specification, this value is denoted by + SETTINGS_HEADER_TABLE_SIZE. + Default: 4096 + -S, --deflate-table-size=<N> + Use first N bytes of dynamic header table + buffer. + Default: 4096 + -d, --dump-header-table + Output dynamic header table.)" + << std::endl; +} + +constexpr static struct option long_options[] = { + {"http1text", no_argument, nullptr, 't'}, + {"table-size", required_argument, nullptr, 's'}, + {"deflate-table-size", required_argument, nullptr, 'S'}, + {"dump-header-table", no_argument, nullptr, 'd'}, + {nullptr, 0, nullptr, 0}}; + +int main(int argc, char **argv) { + config.table_size = 4_k; + config.deflate_table_size = 4_k; + config.http1text = 0; + config.dump_header_table = 0; + while (1) { + int option_index = 0; + int c = getopt_long(argc, argv, "S:dhs:t", long_options, &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'h': + print_help(); + exit(EXIT_SUCCESS); + case 't': + // --http1text + config.http1text = 1; + break; + case 's': { + // --table-size + auto n = util::parse_uint(optarg); + if (n == -1) { + fprintf(stderr, "-s: Bad option value\n"); + exit(EXIT_FAILURE); + } + config.table_size = n; + break; + } + case 'S': { + // --deflate-table-size + auto n = util::parse_uint(optarg); + if (n == -1) { + fprintf(stderr, "-S: Bad option value\n"); + exit(EXIT_FAILURE); + } + config.deflate_table_size = n; + break; + } + case 'd': + // --dump-header-table + config.dump_header_table = 1; + break; + case '?': + exit(EXIT_FAILURE); + default: + break; + } + } + if (config.http1text) { + perform_from_http1text(); + } else { + perform(); + } + + auto comp_ratio = input_sum == 0 ? 0.0 : (double)output_sum / input_sum; + + fprintf(stderr, "Overall: input=%zu output=%zu ratio=%.02f\n", input_sum, + output_sum, comp_ratio); + return 0; +} + +} // namespace nghttp2 + +int main(int argc, char **argv) { + return nghttp2::run_app(nghttp2::main, argc, argv); +} diff --git a/src/h2load.cc b/src/h2load.cc new file mode 100644 index 0000000..0f07610 --- /dev/null +++ b/src/h2load.cc @@ -0,0 +1,3292 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load.h" + +#include <getopt.h> +#include <signal.h> +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#include <netinet/tcp.h> +#include <sys/stat.h> +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#include <sys/mman.h> +#include <netinet/udp.h> + +#include <cstdio> +#include <cassert> +#include <cstdlib> +#include <iostream> +#include <iomanip> +#include <fstream> +#include <chrono> +#include <thread> +#include <future> +#include <random> + +#include <openssl/err.h> + +#ifdef ENABLE_HTTP3 +# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# include <ngtcp2/ngtcp2_crypto_quictls.h> +# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +# include <ngtcp2/ngtcp2_crypto_boringssl.h> +# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +#endif // ENABLE_HTTP3 + +#include "url-parser/url_parser.h" + +#include "h2load_http1_session.h" +#include "h2load_http2_session.h" +#ifdef ENABLE_HTTP3 +# include "h2load_http3_session.h" +# include "h2load_quic.h" +#endif // ENABLE_HTTP3 +#include "tls.h" +#include "http2.h" +#include "util.h" +#include "template.h" +#include "ssl_compat.h" + +#ifndef O_BINARY +# define O_BINARY (0) +#endif // O_BINARY + +using namespace nghttp2; + +namespace h2load { + +namespace { +bool recorded(const std::chrono::steady_clock::time_point &t) { + return std::chrono::steady_clock::duration::zero() != t.time_since_epoch(); +} +} // namespace + +namespace { +std::ofstream keylog_file; +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +Config::Config() + : ciphers(tls::DEFAULT_CIPHER_LIST), + tls13_ciphers("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_" + "CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256"), + groups("X25519:P-256:P-384:P-521"), + data_length(-1), + data(nullptr), + addrs(nullptr), + nreqs(1), + nclients(1), + nthreads(1), + max_concurrent_streams(1), + window_bits(30), + connection_window_bits(30), + max_frame_size(16_k), + rate(0), + rate_period(1.0), + duration(0.0), + warm_up_time(0.0), + conn_active_timeout(0.), + conn_inactivity_timeout(0.), + no_tls_proto(PROTO_HTTP2), + header_table_size(4_k), + encoder_header_table_size(4_k), + data_fd(-1), + log_fd(-1), + qlog_file_base(), + port(0), + default_port(0), + connect_to_port(0), + verbose(false), + timing_script(false), + base_uri_unix(false), + unix_addr{}, + rps(0.), + no_udp_gso(false), + max_udp_payload_size(0), + ktls(false) {} + +Config::~Config() { + if (addrs) { + if (base_uri_unix) { + delete addrs; + } else { + freeaddrinfo(addrs); + } + } + + if (data_fd != -1) { + close(data_fd); + } +} + +bool Config::is_rate_mode() const { return (this->rate != 0); } +bool Config::is_timing_based_mode() const { return (this->duration > 0); } +bool Config::has_base_uri() const { return (!this->base_uri.empty()); } +bool Config::rps_enabled() const { return this->rps > 0.0; } +bool Config::is_quic() const { +#ifdef ENABLE_HTTP3 + return !alpn_list.empty() && + (alpn_list[0] == NGHTTP3_ALPN_H3 || alpn_list[0] == "\x5h3-29"); +#else // !ENABLE_HTTP3 + return false; +#endif // !ENABLE_HTTP3 +} +Config config; + +namespace { +constexpr size_t MAX_SAMPLES = 1000000; +} // namespace + +Stats::Stats(size_t req_todo, size_t nclients) + : req_todo(req_todo), + req_started(0), + req_done(0), + req_success(0), + req_status_success(0), + req_failed(0), + req_error(0), + req_timedout(0), + bytes_total(0), + bytes_head(0), + bytes_head_decomp(0), + bytes_body(0), + status(), + udp_dgram_recv(0), + udp_dgram_sent(0) {} + +Stream::Stream() : req_stat{}, status_success(-1) {} + +namespace { +std::random_device rd; +} // namespace + +namespace { +std::mt19937 gen(rd()); +} // namespace + +namespace { +void sampling_init(Sampling &smp, size_t max_samples) { + smp.n = 0; + smp.max_samples = max_samples; +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto client = static_cast<Client *>(w->data); + client->restart_timeout(); + auto rv = client->do_write(); + if (rv == Client::ERR_CONNECT_FAIL) { + client->disconnect(); + // Try next address + client->current_addr = nullptr; + rv = client->connect(); + if (rv != 0) { + client->fail(); + client->worker->free_client(client); + delete client; + return; + } + return; + } + if (rv != 0) { + client->fail(); + client->worker->free_client(client); + delete client; + } +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto client = static_cast<Client *>(w->data); + client->restart_timeout(); + if (client->do_read() != 0) { + if (client->try_again_or_fail() == 0) { + return; + } + client->worker->free_client(client); + delete client; + return; + } + client->signal_write(); +} +} // namespace + +namespace { +// Called every rate_period when rate mode is being used +void rate_period_timeout_w_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + auto nclients_per_second = worker->rate; + auto conns_remaining = worker->nclients - worker->nconns_made; + auto nclients = std::min(nclients_per_second, conns_remaining); + + for (size_t i = 0; i < nclients; ++i) { + auto req_todo = worker->nreqs_per_client; + if (worker->nreqs_rem > 0) { + ++req_todo; + --worker->nreqs_rem; + } + auto client = + std::make_unique<Client>(worker->next_client_id++, worker, req_todo); + + ++worker->nconns_made; + + if (client->connect() != 0) { + std::cerr << "client could not connect to host" << std::endl; + client->fail(); + } else { + if (worker->config->is_timing_based_mode()) { + worker->clients.push_back(client.release()); + } else { + client.release(); + } + } + worker->report_rate_progress(); + } + if (!worker->config->is_timing_based_mode()) { + if (worker->nconns_made >= worker->nclients) { + ev_timer_stop(worker->loop, w); + } + } else { + // To check whether all created clients are pushed correctly + assert(worker->nclients == worker->clients.size()); + } +} +} // namespace + +namespace { +// Called when the duration for infinite number of requests are over +void duration_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + + worker->current_phase = Phase::DURATION_OVER; + + std::cout << "Main benchmark duration is over for thread #" << worker->id + << ". Stopping all clients." << std::endl; + worker->stop_all_clients(); + std::cout << "Stopped all clients for thread #" << worker->id << std::endl; +} +} // namespace + +namespace { +// Called when the warmup duration for infinite number of requests are over +void warmup_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + + std::cout << "Warm-up phase is over for thread #" << worker->id << "." + << std::endl; + std::cout << "Main benchmark duration is started for thread #" << worker->id + << "." << std::endl; + assert(worker->stats.req_started == 0); + assert(worker->stats.req_done == 0); + + for (auto client : worker->clients) { + if (client) { + assert(client->req_todo == 0); + assert(client->req_left == 1); + assert(client->req_inflight == 0); + assert(client->req_started == 0); + assert(client->req_done == 0); + + client->record_client_start_time(); + client->clear_connect_times(); + client->record_connect_start_time(); + } + } + + worker->current_phase = Phase::MAIN_DURATION; + + ev_timer_start(worker->loop, &worker->duration_watcher); +} +} // namespace + +namespace { +void rps_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast<Client *>(w->data); + auto &session = client->session; + + assert(!config.timing_script); + + if (client->req_left == 0) { + ev_timer_stop(loop, w); + return; + } + + auto now = std::chrono::steady_clock::now(); + auto d = now - client->rps_duration_started; + auto n = static_cast<size_t>( + round(std::chrono::duration<double>(d).count() * config.rps)); + client->rps_req_pending += n; + client->rps_duration_started += + util::duration_from(static_cast<double>(n) / config.rps); + + if (client->rps_req_pending == 0) { + return; + } + + auto nreq = session->max_concurrent_streams() - client->rps_req_inflight; + if (nreq == 0) { + return; + } + + nreq = config.is_timing_based_mode() ? std::max(nreq, client->req_left) + : std::min(nreq, client->req_left); + nreq = std::min(nreq, client->rps_req_pending); + + client->rps_req_inflight += nreq; + client->rps_req_pending -= nreq; + + for (; nreq > 0; --nreq) { + if (client->submit_request() != 0) { + client->process_request_failure(); + break; + } + } + + client->signal_write(); +} +} // namespace + +namespace { +// Called when an a connection has been inactive for a set period of time +// or a fixed amount of time after all requests have been made on a +// connection +void conn_timeout_cb(EV_P_ ev_timer *w, int revents) { + auto client = static_cast<Client *>(w->data); + + ev_timer_stop(client->worker->loop, &client->conn_inactivity_watcher); + ev_timer_stop(client->worker->loop, &client->conn_active_watcher); + + if (util::check_socket_connected(client->fd)) { + client->timeout(); + } +} +} // namespace + +namespace { +bool check_stop_client_request_timeout(Client *client, ev_timer *w) { + if (client->req_left == 0) { + // no more requests to make, stop timer + ev_timer_stop(client->worker->loop, w); + return true; + } + + return false; +} +} // namespace + +namespace { +void client_request_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast<Client *>(w->data); + + if (client->streams.size() >= (size_t)config.max_concurrent_streams) { + ev_timer_stop(client->worker->loop, w); + return; + } + + if (client->submit_request() != 0) { + ev_timer_stop(client->worker->loop, w); + client->process_request_failure(); + return; + } + client->signal_write(); + + if (check_stop_client_request_timeout(client, w)) { + return; + } + + auto duration = + config.timings[client->reqidx] - config.timings[client->reqidx - 1]; + + while (duration < std::chrono::duration<double>(1e-9)) { + if (client->submit_request() != 0) { + ev_timer_stop(client->worker->loop, w); + client->process_request_failure(); + return; + } + client->signal_write(); + if (check_stop_client_request_timeout(client, w)) { + return; + } + + duration = + config.timings[client->reqidx] - config.timings[client->reqidx - 1]; + } + + client->request_timeout_watcher.repeat = util::ev_tstamp_from(duration); + ev_timer_again(client->worker->loop, &client->request_timeout_watcher); +} +} // namespace + +Client::Client(uint32_t id, Worker *worker, size_t req_todo) + : wb(&worker->mcpool), + cstat{}, + worker(worker), + ssl(nullptr), +#ifdef ENABLE_HTTP3 + quic{}, +#endif // ENABLE_HTTP3 + next_addr(config.addrs), + current_addr(nullptr), + reqidx(0), + state(CLIENT_IDLE), + req_todo(req_todo), + req_left(req_todo), + req_inflight(0), + req_started(0), + req_done(0), + id(id), + fd(-1), + local_addr{}, + new_connection_requested(false), + final(false), + rps_req_pending(0), + rps_req_inflight(0) { + if (req_todo == 0) { // this means infinite number of requests are to be made + // This ensures that number of requests are unbounded + // Just a positive number is fine, we chose the first positive number + req_left = 1; + } + ev_io_init(&wev, writecb, 0, EV_WRITE); + ev_io_init(&rev, readcb, 0, EV_READ); + + wev.data = this; + rev.data = this; + + ev_timer_init(&conn_inactivity_watcher, conn_timeout_cb, 0., + worker->config->conn_inactivity_timeout); + conn_inactivity_watcher.data = this; + + ev_timer_init(&conn_active_watcher, conn_timeout_cb, + worker->config->conn_active_timeout, 0.); + conn_active_watcher.data = this; + + ev_timer_init(&request_timeout_watcher, client_request_timeout_cb, 0., 0.); + request_timeout_watcher.data = this; + + ev_timer_init(&rps_watcher, rps_cb, 0., 0.); + rps_watcher.data = this; + +#ifdef ENABLE_HTTP3 + ev_timer_init(&quic.pkt_timer, quic_pkt_timeout_cb, 0., 0.); + quic.pkt_timer.data = this; + + if (config.is_quic()) { + quic.tx.data = std::make_unique<uint8_t[]>(64_k); + } + + ngtcp2_ccerr_default(&quic.last_error); +#endif // ENABLE_HTTP3 +} + +Client::~Client() { + disconnect(); + +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + quic_free(); + } +#endif // ENABLE_HTTP3 + + if (ssl) { + SSL_free(ssl); + } + + worker->sample_client_stat(&cstat); + ++worker->client_smp.n; +} + +int Client::do_read() { return readfn(*this); } +int Client::do_write() { return writefn(*this); } + +int Client::make_socket(addrinfo *addr) { + int rv; + + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 + fd = util::create_nonblock_udp_socket(addr->ai_family); + if (fd == -1) { + return -1; + } + +# ifdef UDP_GRO + int val = 1; + if (setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, sizeof(val)) != 0) { + std::cerr << "setsockopt UDP_GRO failed" << std::endl; + return -1; + } +# endif // UDP_GRO + + rv = util::bind_any_addr_udp(fd, addr->ai_family); + if (rv != 0) { + close(fd); + fd = -1; + return -1; + } + + socklen_t addrlen = sizeof(local_addr.su.storage); + rv = getsockname(fd, &local_addr.su.sa, &addrlen); + if (rv == -1) { + return -1; + } + local_addr.len = addrlen; + + if (quic_init(&local_addr.su.sa, local_addr.len, addr->ai_addr, + addr->ai_addrlen) != 0) { + std::cerr << "quic_init failed" << std::endl; + return -1; + } +#endif // ENABLE_HTTP3 + } else { + fd = util::create_nonblock_socket(addr->ai_family); + if (fd == -1) { + return -1; + } + if (config.scheme == "https") { + if (!ssl) { + ssl = SSL_new(worker->ssl_ctx); + } + + SSL_set_connect_state(ssl); + } + } + + if (ssl && !util::numeric_host(config.host.c_str())) { + SSL_set_tlsext_host_name(ssl, config.host.c_str()); + } + + if (config.is_quic()) { + return 0; + } + + rv = ::connect(fd, addr->ai_addr, addr->ai_addrlen); + if (rv != 0 && errno != EINPROGRESS) { + if (ssl) { + SSL_free(ssl); + ssl = nullptr; + } + close(fd); + fd = -1; + return -1; + } + return 0; +} + +int Client::connect() { + int rv; + + if (!worker->config->is_timing_based_mode() || + worker->current_phase == Phase::MAIN_DURATION) { + record_client_start_time(); + clear_connect_times(); + record_connect_start_time(); + } else if (worker->current_phase == Phase::INITIAL_IDLE) { + worker->current_phase = Phase::WARM_UP; + std::cout << "Warm-up started for thread #" << worker->id << "." + << std::endl; + ev_timer_start(worker->loop, &worker->warmup_watcher); + } + + if (worker->config->conn_inactivity_timeout > 0.) { + ev_timer_again(worker->loop, &conn_inactivity_watcher); + } + + if (current_addr) { + rv = make_socket(current_addr); + if (rv == -1) { + return -1; + } + } else { + addrinfo *addr = nullptr; + while (next_addr) { + addr = next_addr; + next_addr = next_addr->ai_next; + rv = make_socket(addr); + if (rv == 0) { + break; + } + } + + if (fd == -1) { + return -1; + } + + assert(addr); + + current_addr = addr; + } + + ev_io_set(&rev, fd, EV_READ); + ev_io_set(&wev, fd, EV_WRITE); + + ev_io_start(worker->loop, &wev); + + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 + ev_io_start(worker->loop, &rev); + + readfn = &Client::read_quic; + writefn = &Client::write_quic; +#endif // ENABLE_HTTP3 + } else { + writefn = &Client::connected; + } + + return 0; +} + +void Client::timeout() { + process_timedout_streams(); + + disconnect(); +} + +void Client::restart_timeout() { + if (worker->config->conn_inactivity_timeout > 0.) { + ev_timer_again(worker->loop, &conn_inactivity_watcher); + } +} + +int Client::try_again_or_fail() { + disconnect(); + + if (new_connection_requested) { + new_connection_requested = false; + + if (req_left) { + + if (worker->current_phase == Phase::MAIN_DURATION) { + // At the moment, we don't have a facility to re-start request + // already in in-flight. Make them fail. + worker->stats.req_failed += req_inflight; + worker->stats.req_error += req_inflight; + + req_inflight = 0; + } + + // Keep using current address + if (connect() == 0) { + return 0; + } + std::cerr << "client could not connect to host" << std::endl; + } + } + + process_abandoned_streams(); + + return -1; +} + +void Client::fail() { + disconnect(); + + process_abandoned_streams(); +} + +void Client::disconnect() { + record_client_end_time(); + +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + quic_close_connection(); + } +#endif // ENABLE_HTTP3 + +#ifdef ENABLE_HTTP3 + ev_timer_stop(worker->loop, &quic.pkt_timer); +#endif // ENABLE_HTTP3 + ev_timer_stop(worker->loop, &conn_inactivity_watcher); + ev_timer_stop(worker->loop, &conn_active_watcher); + ev_timer_stop(worker->loop, &rps_watcher); + ev_timer_stop(worker->loop, &request_timeout_watcher); + streams.clear(); + session.reset(); + wb.reset(); + state = CLIENT_IDLE; + ev_io_stop(worker->loop, &wev); + ev_io_stop(worker->loop, &rev); + if (ssl) { + if (config.is_quic()) { + SSL_free(ssl); + ssl = nullptr; + } else { + SSL_set_shutdown(ssl, SSL_get_shutdown(ssl) | SSL_RECEIVED_SHUTDOWN); + ERR_clear_error(); + + if (SSL_shutdown(ssl) != 1) { + SSL_free(ssl); + ssl = nullptr; + } + } + } + if (fd != -1) { + shutdown(fd, SHUT_WR); + close(fd); + fd = -1; + } + + final = false; +} + +int Client::submit_request() { + if (session->submit_request() != 0) { + return -1; + } + + if (worker->current_phase != Phase::MAIN_DURATION) { + return 0; + } + + ++worker->stats.req_started; + ++req_started; + ++req_inflight; + if (!worker->config->is_timing_based_mode()) { + --req_left; + } + // if an active timeout is set and this is the last request to be submitted + // on this connection, start the active timeout. + if (worker->config->conn_active_timeout > 0. && req_left == 0) { + ev_timer_start(worker->loop, &conn_active_watcher); + } + + return 0; +} + +void Client::process_timedout_streams() { + if (worker->current_phase != Phase::MAIN_DURATION) { + return; + } + + for (auto &p : streams) { + auto &req_stat = p.second.req_stat; + if (!req_stat.completed) { + req_stat.stream_close_time = std::chrono::steady_clock::now(); + } + } + + worker->stats.req_timedout += req_inflight; + + process_abandoned_streams(); +} + +void Client::process_abandoned_streams() { + if (worker->current_phase != Phase::MAIN_DURATION) { + return; + } + + auto req_abandoned = req_inflight + req_left; + + worker->stats.req_failed += req_abandoned; + worker->stats.req_error += req_abandoned; + + req_inflight = 0; + req_left = 0; +} + +void Client::process_request_failure() { + if (worker->current_phase != Phase::MAIN_DURATION) { + return; + } + + worker->stats.req_failed += req_left; + worker->stats.req_error += req_left; + + req_left = 0; + + if (req_inflight == 0) { + terminate_session(); + } + std::cout << "Process Request Failure:" << worker->stats.req_failed + << std::endl; +} + +namespace { +void print_server_tmp_key(SSL *ssl) { + EVP_PKEY *key; + + if (!SSL_get_server_tmp_key(ssl, &key)) { + return; + } + + auto key_del = defer(EVP_PKEY_free, key); + + std::cout << "Server Temp Key: "; + + auto pkey_id = EVP_PKEY_id(key); + switch (pkey_id) { + case EVP_PKEY_RSA: + std::cout << "RSA " << EVP_PKEY_bits(key) << " bits" << std::endl; + break; + case EVP_PKEY_DH: + std::cout << "DH " << EVP_PKEY_bits(key) << " bits" << std::endl; + break; + case EVP_PKEY_EC: { +#if OPENSSL_3_0_0_API + std::array<char, 64> curve_name; + const char *cname; + if (!EVP_PKEY_get_utf8_string_param(key, "group", curve_name.data(), + curve_name.size(), nullptr)) { + cname = "<unknown>"; + } else { + cname = curve_name.data(); + } +#else // !OPENSSL_3_0_0_API + auto ec = EVP_PKEY_get1_EC_KEY(key); + auto ec_del = defer(EC_KEY_free, ec); + auto nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); + auto cname = EC_curve_nid2nist(nid); + if (!cname) { + cname = OBJ_nid2sn(nid); + } +#endif // !OPENSSL_3_0_0_API + + std::cout << "ECDH " << cname << " " << EVP_PKEY_bits(key) << " bits" + << std::endl; + break; + } + default: + std::cout << OBJ_nid2sn(pkey_id) << " " << EVP_PKEY_bits(key) << " bits" + << std::endl; + break; + } +} +} // namespace + +void Client::report_tls_info() { + if (worker->id == 0 && !worker->tls_info_report_done) { + worker->tls_info_report_done = true; + auto cipher = SSL_get_current_cipher(ssl); + std::cout << "TLS Protocol: " << tls::get_tls_protocol(ssl) << "\n" + << "Cipher: " << SSL_CIPHER_get_name(cipher) << std::endl; + print_server_tmp_key(ssl); + } +} + +void Client::report_app_info() { + if (worker->id == 0 && !worker->app_info_report_done) { + worker->app_info_report_done = true; + std::cout << "Application protocol: " << selected_proto << std::endl; + } +} + +void Client::terminate_session() { +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + quic.close_requested = true; + } +#endif // ENABLE_HTTP3 + if (session) { + session->terminate(); + } + // http1 session needs writecb to tear down session. + signal_write(); +} + +void Client::on_request(int32_t stream_id) { streams[stream_id] = Stream(); } + +void Client::on_header(int32_t stream_id, const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen) { + auto itr = streams.find(stream_id); + if (itr == std::end(streams)) { + return; + } + auto &stream = (*itr).second; + + if (worker->current_phase != Phase::MAIN_DURATION) { + // If the stream is for warm-up phase, then mark as a success + // But we do not update the count for 2xx, 3xx, etc status codes + // Same has been done in on_status_code function + stream.status_success = 1; + return; + } + + if (stream.status_success == -1 && namelen == 7 && + util::streq_l(":status", name, namelen)) { + int status = 0; + for (size_t i = 0; i < valuelen; ++i) { + if ('0' <= value[i] && value[i] <= '9') { + status *= 10; + status += value[i] - '0'; + if (status > 999) { + stream.status_success = 0; + return; + } + } else { + break; + } + } + + if (status < 200) { + return; + } + + stream.req_stat.status = status; + if (status >= 200 && status < 300) { + ++worker->stats.status[2]; + stream.status_success = 1; + } else if (status < 400) { + ++worker->stats.status[3]; + stream.status_success = 1; + } else if (status < 600) { + ++worker->stats.status[status / 100]; + stream.status_success = 0; + } else { + stream.status_success = 0; + } + } +} + +void Client::on_status_code(int32_t stream_id, uint16_t status) { + auto itr = streams.find(stream_id); + if (itr == std::end(streams)) { + return; + } + auto &stream = (*itr).second; + + if (worker->current_phase != Phase::MAIN_DURATION) { + stream.status_success = 1; + return; + } + + stream.req_stat.status = status; + if (status >= 200 && status < 300) { + ++worker->stats.status[2]; + stream.status_success = 1; + } else if (status < 400) { + ++worker->stats.status[3]; + stream.status_success = 1; + } else if (status < 600) { + ++worker->stats.status[status / 100]; + stream.status_success = 0; + } else { + stream.status_success = 0; + } +} + +void Client::on_stream_close(int32_t stream_id, bool success, bool final) { + if (worker->current_phase == Phase::MAIN_DURATION) { + if (req_inflight > 0) { + --req_inflight; + } + auto req_stat = get_req_stat(stream_id); + if (!req_stat) { + return; + } + + req_stat->stream_close_time = std::chrono::steady_clock::now(); + if (success) { + req_stat->completed = true; + ++worker->stats.req_success; + ++cstat.req_success; + + if (streams[stream_id].status_success == 1) { + ++worker->stats.req_status_success; + } else { + ++worker->stats.req_failed; + } + + worker->sample_req_stat(req_stat); + + // Count up in successful cases only + ++worker->request_times_smp.n; + } else { + ++worker->stats.req_failed; + ++worker->stats.req_error; + } + ++worker->stats.req_done; + ++req_done; + + if (worker->config->log_fd != -1) { + auto start = std::chrono::duration_cast<std::chrono::microseconds>( + req_stat->request_wall_time.time_since_epoch()); + auto delta = std::chrono::duration_cast<std::chrono::microseconds>( + req_stat->stream_close_time - req_stat->request_time); + + std::array<uint8_t, 256> buf; + auto p = std::begin(buf); + p = util::utos(p, start.count()); + *p++ = '\t'; + if (success) { + p = util::utos(p, req_stat->status); + } else { + *p++ = '-'; + *p++ = '1'; + } + *p++ = '\t'; + p = util::utos(p, delta.count()); + *p++ = '\n'; + + auto nwrite = static_cast<size_t>(std::distance(std::begin(buf), p)); + assert(nwrite <= buf.size()); + while (write(worker->config->log_fd, buf.data(), nwrite) == -1 && + errno == EINTR) + ; + } + } + + worker->report_progress(); + streams.erase(stream_id); + if (req_left == 0 && req_inflight == 0) { + terminate_session(); + return; + } + + if (!final && req_left > 0) { + if (config.timing_script) { + if (!ev_is_active(&request_timeout_watcher)) { + ev_feed_event(worker->loop, &request_timeout_watcher, EV_TIMER); + } + } else if (!config.rps_enabled()) { + if (submit_request() != 0) { + process_request_failure(); + } + } else if (rps_req_pending) { + --rps_req_pending; + if (submit_request() != 0) { + process_request_failure(); + } + } else { + assert(rps_req_inflight); + --rps_req_inflight; + } + } +} + +RequestStat *Client::get_req_stat(int32_t stream_id) { + auto it = streams.find(stream_id); + if (it == std::end(streams)) { + return nullptr; + } + + return &(*it).second.req_stat; +} + +int Client::connection_made() { + if (ssl) { + report_tls_info(); + + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len; + + SSL_get0_alpn_selected(ssl, &next_proto, &next_proto_len); + + if (next_proto) { + auto proto = StringRef{next_proto, next_proto_len}; + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 + assert(session); + if (!util::streq(StringRef{&NGHTTP3_ALPN_H3[1]}, proto) && + !util::streq_l("h3-29", proto)) { + return -1; + } +#endif // ENABLE_HTTP3 + } else if (util::check_h2_is_selected(proto)) { + session = std::make_unique<Http2Session>(this); + } else if (util::streq(NGHTTP2_H1_1, proto)) { + session = std::make_unique<Http1Session>(this); + } + + // Just assign next_proto to selected_proto anyway to show the + // negotiation result. + selected_proto = proto.str(); + } else if (config.is_quic()) { + std::cerr << "QUIC requires ALPN negotiation" << std::endl; + return -1; + } else { + std::cout << "No protocol negotiated. Fallback behaviour may be activated" + << std::endl; + + for (const auto &proto : config.alpn_list) { + if (util::streq(NGHTTP2_H1_1_ALPN, StringRef{proto})) { + std::cout << "Server does not support ALPN. Falling back to HTTP/1.1." + << std::endl; + session = std::make_unique<Http1Session>(this); + selected_proto = NGHTTP2_H1_1.str(); + break; + } + } + } + + if (!selected_proto.empty()) { + report_app_info(); + } + + if (!session) { + std::cout + << "No supported protocol was negotiated. Supported protocols were:" + << std::endl; + for (const auto &proto : config.alpn_list) { + std::cout << proto.substr(1) << std::endl; + } + disconnect(); + return -1; + } + } else { + switch (config.no_tls_proto) { + case Config::PROTO_HTTP2: + session = std::make_unique<Http2Session>(this); + selected_proto = NGHTTP2_CLEARTEXT_PROTO_VERSION_ID; + break; + case Config::PROTO_HTTP1_1: + session = std::make_unique<Http1Session>(this); + selected_proto = NGHTTP2_H1_1.str(); + break; + default: + // unreachable + assert(0); + } + + report_app_info(); + } + + state = CLIENT_CONNECTED; + + session->on_connect(); + + record_connect_time(); + + if (config.rps_enabled()) { + rps_watcher.repeat = std::max(0.01, 1. / config.rps); + ev_timer_again(worker->loop, &rps_watcher); + rps_duration_started = std::chrono::steady_clock::now(); + } + + if (config.rps_enabled()) { + assert(req_left); + + ++rps_req_inflight; + + if (submit_request() != 0) { + process_request_failure(); + } + } else if (!config.timing_script) { + auto nreq = config.is_timing_based_mode() + ? std::max(req_left, session->max_concurrent_streams()) + : std::min(req_left, session->max_concurrent_streams()); + + for (; nreq > 0; --nreq) { + if (submit_request() != 0) { + process_request_failure(); + break; + } + } + } else { + + auto duration = config.timings[reqidx]; + + while (duration < std::chrono::duration<double>(1e-9)) { + if (submit_request() != 0) { + process_request_failure(); + break; + } + duration = config.timings[reqidx]; + if (reqidx == 0) { + // if reqidx wraps around back to 0, we uses up all lines and + // should break + break; + } + } + + if (duration >= std::chrono::duration<double>(1e-9)) { + // double check since we may have break due to reqidx wraps + // around back to 0 + request_timeout_watcher.repeat = util::ev_tstamp_from(duration); + ev_timer_again(worker->loop, &request_timeout_watcher); + } + } + signal_write(); + + return 0; +} + +int Client::on_read(const uint8_t *data, size_t len) { + auto rv = session->on_read(data, len); + if (rv != 0) { + return -1; + } + if (worker->current_phase == Phase::MAIN_DURATION) { + worker->stats.bytes_total += len; + } + signal_write(); + return 0; +} + +int Client::on_write() { + if (wb.rleft() >= BACKOFF_WRITE_BUFFER_THRES) { + return 0; + } + + if (session->on_write() != 0) { + return -1; + } + return 0; +} + +int Client::read_clear() { + uint8_t buf[8_k]; + + for (;;) { + ssize_t nread; + while ((nread = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR) + ; + if (nread == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return -1; + } + + if (nread == 0) { + return -1; + } + + if (on_read(buf, nread) != 0) { + return -1; + } + } + + return 0; +} + +int Client::write_clear() { + std::array<struct iovec, 2> iov; + + for (;;) { + if (on_write() != 0) { + return -1; + } + + auto iovcnt = wb.riovec(iov.data(), iov.size()); + + if (iovcnt == 0) { + break; + } + + ssize_t nwrite; + while ((nwrite = writev(fd, iov.data(), iovcnt)) == -1 && errno == EINTR) + ; + + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + ev_io_start(worker->loop, &wev); + return 0; + } + return -1; + } + + wb.drain(nwrite); + } + + ev_io_stop(worker->loop, &wev); + + return 0; +} + +int Client::connected() { + if (!util::check_socket_connected(fd)) { + return ERR_CONNECT_FAIL; + } + ev_io_start(worker->loop, &rev); + ev_io_stop(worker->loop, &wev); + + if (ssl) { + SSL_set_fd(ssl, fd); + + readfn = &Client::tls_handshake; + writefn = &Client::tls_handshake; + + return do_write(); + } + + readfn = &Client::read_clear; + writefn = &Client::write_clear; + + if (connection_made() != 0) { + return -1; + } + + return 0; +} + +int Client::tls_handshake() { + ERR_clear_error(); + + auto rv = SSL_do_handshake(ssl); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + ev_io_stop(worker->loop, &wev); + return 0; + case SSL_ERROR_WANT_WRITE: + ev_io_start(worker->loop, &wev); + return 0; + default: + return -1; + } + } + + ev_io_stop(worker->loop, &wev); + + readfn = &Client::read_tls; + writefn = &Client::write_tls; + + if (connection_made() != 0) { + return -1; + } + + return 0; +} + +int Client::read_tls() { + uint8_t buf[8_k]; + + ERR_clear_error(); + + for (;;) { + auto rv = SSL_read(ssl, buf, sizeof(buf)); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + return 0; + case SSL_ERROR_WANT_WRITE: + // renegotiation started + return -1; + default: + return -1; + } + } + + if (on_read(buf, rv) != 0) { + return -1; + } + } +} + +int Client::write_tls() { + ERR_clear_error(); + + struct iovec iov; + + for (;;) { + if (on_write() != 0) { + return -1; + } + + auto iovcnt = wb.riovec(&iov, 1); + + if (iovcnt == 0) { + break; + } + + auto rv = SSL_write(ssl, iov.iov_base, iov.iov_len); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + // renegotiation started + return -1; + case SSL_ERROR_WANT_WRITE: + ev_io_start(worker->loop, &wev); + return 0; + default: + return -1; + } + } + + wb.drain(rv); + } + + ev_io_stop(worker->loop, &wev); + + return 0; +} + +#ifdef ENABLE_HTTP3 +// Returns 1 if sendmsg is blocked. +int Client::write_udp(const sockaddr *addr, socklen_t addrlen, + const uint8_t *data, size_t datalen, size_t gso_size) { + iovec msg_iov; + msg_iov.iov_base = const_cast<uint8_t *>(data); + msg_iov.iov_len = datalen; + + msghdr msg{}; + msg.msg_name = const_cast<sockaddr *>(addr); + msg.msg_namelen = addrlen; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + +# ifdef UDP_SEGMENT + std::array<uint8_t, CMSG_SPACE(sizeof(uint16_t))> msg_ctrl{}; + if (gso_size && datalen > gso_size) { + msg.msg_control = msg_ctrl.data(); + msg.msg_controllen = msg_ctrl.size(); + + auto cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + uint16_t n = gso_size; + memcpy(CMSG_DATA(cm), &n, sizeof(n)); + } +# endif // UDP_SEGMENT + + auto nwrite = sendmsg(fd, &msg, 0); + if (nwrite < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 1; + } + + std::cerr << "sendmsg: errno=" << errno << std::endl; + } else { + ++worker->stats.udp_dgram_sent; + } + + ev_io_stop(worker->loop, &wev); + + return 0; +} +#endif // ENABLE_HTTP3 + +void Client::record_request_time(RequestStat *req_stat) { + req_stat->request_time = std::chrono::steady_clock::now(); + req_stat->request_wall_time = std::chrono::system_clock::now(); +} + +void Client::record_connect_start_time() { + cstat.connect_start_time = std::chrono::steady_clock::now(); +} + +void Client::record_connect_time() { + cstat.connect_time = std::chrono::steady_clock::now(); +} + +void Client::record_ttfb() { + if (recorded(cstat.ttfb)) { + return; + } + + cstat.ttfb = std::chrono::steady_clock::now(); +} + +void Client::clear_connect_times() { + cstat.connect_start_time = std::chrono::steady_clock::time_point(); + cstat.connect_time = std::chrono::steady_clock::time_point(); + cstat.ttfb = std::chrono::steady_clock::time_point(); +} + +void Client::record_client_start_time() { + // Record start time only once at the very first connection is going + // to be made. + if (recorded(cstat.client_start_time)) { + return; + } + + cstat.client_start_time = std::chrono::steady_clock::now(); +} + +void Client::record_client_end_time() { + // Unlike client_start_time, we overwrite client_end_time. This + // handles multiple connect/disconnect for HTTP/1.1 benchmark. + cstat.client_end_time = std::chrono::steady_clock::now(); +} + +void Client::signal_write() { ev_io_start(worker->loop, &wev); } + +void Client::try_new_connection() { new_connection_requested = true; } + +namespace { +int get_ev_loop_flags() { + if (ev_supported_backends() & ~ev_recommended_backends() & EVBACKEND_KQUEUE) { + return ev_recommended_backends() | EVBACKEND_KQUEUE; + } + + return 0; +} +} // namespace + +Worker::Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t req_todo, size_t nclients, + size_t rate, size_t max_samples, Config *config) + : randgen(util::make_mt19937()), + stats(req_todo, nclients), + loop(ev_loop_new(get_ev_loop_flags())), + ssl_ctx(ssl_ctx), + config(config), + id(id), + tls_info_report_done(false), + app_info_report_done(false), + nconns_made(0), + nclients(nclients), + nreqs_per_client(req_todo / nclients), + nreqs_rem(req_todo % nclients), + rate(rate), + max_samples(max_samples), + next_client_id(0) { + if (!config->is_rate_mode() && !config->is_timing_based_mode()) { + progress_interval = std::max(static_cast<size_t>(1), req_todo / 10); + } else { + progress_interval = std::max(static_cast<size_t>(1), nclients / 10); + } + + // Below timeout is not needed in case of timing-based benchmarking + // create timer that will go off every rate_period + ev_timer_init(&timeout_watcher, rate_period_timeout_w_cb, 0., + config->rate_period); + timeout_watcher.data = this; + + if (config->is_timing_based_mode()) { + stats.req_stats.reserve(std::max(req_todo, max_samples)); + stats.client_stats.reserve(std::max(nclients, max_samples)); + } else { + stats.req_stats.reserve(std::min(req_todo, max_samples)); + stats.client_stats.reserve(std::min(nclients, max_samples)); + } + + sampling_init(request_times_smp, max_samples); + sampling_init(client_smp, max_samples); + + ev_timer_init(&duration_watcher, duration_timeout_cb, config->duration, 0.); + duration_watcher.data = this; + + ev_timer_init(&warmup_watcher, warmup_timeout_cb, config->warm_up_time, 0.); + warmup_watcher.data = this; + + if (config->is_timing_based_mode()) { + current_phase = Phase::INITIAL_IDLE; + } else { + current_phase = Phase::MAIN_DURATION; + } +} + +Worker::~Worker() { + ev_timer_stop(loop, &timeout_watcher); + ev_timer_stop(loop, &duration_watcher); + ev_timer_stop(loop, &warmup_watcher); + ev_loop_destroy(loop); +} + +void Worker::stop_all_clients() { + for (auto client : clients) { + if (client) { + client->terminate_session(); + } + } +} + +void Worker::free_client(Client *deleted_client) { + for (auto &client : clients) { + if (client == deleted_client) { + client->req_todo = client->req_done; + stats.req_todo += client->req_todo; + auto index = &client - &clients[0]; + clients[index] = nullptr; + return; + } + } +} + +void Worker::run() { + if (!config->is_rate_mode() && !config->is_timing_based_mode()) { + for (size_t i = 0; i < nclients; ++i) { + auto req_todo = nreqs_per_client; + if (nreqs_rem > 0) { + ++req_todo; + --nreqs_rem; + } + + auto client = std::make_unique<Client>(next_client_id++, this, req_todo); + if (client->connect() != 0) { + std::cerr << "client could not connect to host" << std::endl; + client->fail(); + } else { + client.release(); + } + } + } else if (config->is_rate_mode()) { + ev_timer_again(loop, &timeout_watcher); + + // call callback so that we don't waste the first rate_period + rate_period_timeout_w_cb(loop, &timeout_watcher, 0); + } else { + // call the callback to start for one single time + rate_period_timeout_w_cb(loop, &timeout_watcher, 0); + } + ev_run(loop, 0); +} + +namespace { +template <typename Stats, typename Stat> +void sample(Sampling &smp, Stats &stats, Stat *s) { + ++smp.n; + if (stats.size() < smp.max_samples) { + stats.push_back(*s); + return; + } + auto d = std::uniform_int_distribution<unsigned long>(0, smp.n - 1); + auto i = d(gen); + if (i < smp.max_samples) { + stats[i] = *s; + } +} +} // namespace + +void Worker::sample_req_stat(RequestStat *req_stat) { + sample(request_times_smp, stats.req_stats, req_stat); +} + +void Worker::sample_client_stat(ClientStat *cstat) { + sample(client_smp, stats.client_stats, cstat); +} + +void Worker::report_progress() { + if (id != 0 || config->is_rate_mode() || stats.req_done % progress_interval || + config->is_timing_based_mode()) { + return; + } + + std::cout << "progress: " << stats.req_done * 100 / stats.req_todo << "% done" + << std::endl; +} + +void Worker::report_rate_progress() { + if (id != 0 || nconns_made % progress_interval) { + return; + } + + std::cout << "progress: " << nconns_made * 100 / nclients + << "% of clients started" << std::endl; +} + +namespace { +// Returns percentage of number of samples within mean +/- sd. +double within_sd(const std::vector<double> &samples, double mean, double sd) { + if (samples.size() == 0) { + return 0.0; + } + auto lower = mean - sd; + auto upper = mean + sd; + auto m = std::count_if( + std::begin(samples), std::end(samples), + [&lower, &upper](double t) { return lower <= t && t <= upper; }); + return (m / static_cast<double>(samples.size())) * 100; +} +} // namespace + +namespace { +// Computes statistics using |samples|. The min, max, mean, sd, and +// percentage of number of samples within mean +/- sd are computed. +// If |sampling| is true, this computes sample variance. Otherwise, +// population variance. +SDStat compute_time_stat(const std::vector<double> &samples, + bool sampling = false) { + if (samples.empty()) { + return {0.0, 0.0, 0.0, 0.0, 0.0}; + } + // standard deviation calculated using Rapid calculation method: + // https://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods + double a = 0, q = 0; + size_t n = 0; + double sum = 0; + auto res = SDStat{std::numeric_limits<double>::max(), + std::numeric_limits<double>::min()}; + for (const auto &t : samples) { + ++n; + res.min = std::min(res.min, t); + res.max = std::max(res.max, t); + sum += t; + + auto na = a + (t - a) / n; + q += (t - a) * (t - na); + a = na; + } + + assert(n > 0); + res.mean = sum / n; + res.sd = sqrt(q / (sampling && n > 1 ? n - 1 : n)); + res.within_sd = within_sd(samples, res.mean, res.sd); + + return res; +} +} // namespace + +namespace { +SDStats +process_time_stats(const std::vector<std::unique_ptr<Worker>> &workers) { + auto request_times_sampling = false; + auto client_times_sampling = false; + size_t nrequest_times = 0; + size_t nclient_times = 0; + for (const auto &w : workers) { + nrequest_times += w->stats.req_stats.size(); + request_times_sampling = w->request_times_smp.n > w->stats.req_stats.size(); + + nclient_times += w->stats.client_stats.size(); + client_times_sampling = w->client_smp.n > w->stats.client_stats.size(); + } + + std::vector<double> request_times; + request_times.reserve(nrequest_times); + + std::vector<double> connect_times, ttfb_times, rps_values; + connect_times.reserve(nclient_times); + ttfb_times.reserve(nclient_times); + rps_values.reserve(nclient_times); + + for (const auto &w : workers) { + for (const auto &req_stat : w->stats.req_stats) { + if (!req_stat.completed) { + continue; + } + request_times.push_back( + std::chrono::duration_cast<std::chrono::duration<double>>( + req_stat.stream_close_time - req_stat.request_time) + .count()); + } + + const auto &stat = w->stats; + + for (const auto &cstat : stat.client_stats) { + if (recorded(cstat.client_start_time) && + recorded(cstat.client_end_time)) { + auto t = std::chrono::duration_cast<std::chrono::duration<double>>( + cstat.client_end_time - cstat.client_start_time) + .count(); + if (t > 1e-9) { + rps_values.push_back(cstat.req_success / t); + } + } + + // We will get connect event before FFTB. + if (!recorded(cstat.connect_start_time) || + !recorded(cstat.connect_time)) { + continue; + } + + connect_times.push_back( + std::chrono::duration_cast<std::chrono::duration<double>>( + cstat.connect_time - cstat.connect_start_time) + .count()); + + if (!recorded(cstat.ttfb)) { + continue; + } + + ttfb_times.push_back( + std::chrono::duration_cast<std::chrono::duration<double>>( + cstat.ttfb - cstat.connect_start_time) + .count()); + } + } + + return {compute_time_stat(request_times, request_times_sampling), + compute_time_stat(connect_times, client_times_sampling), + compute_time_stat(ttfb_times, client_times_sampling), + compute_time_stat(rps_values, client_times_sampling)}; +} +} // namespace + +namespace { +void resolve_host() { + if (config.base_uri_unix) { + auto res = std::make_unique<addrinfo>(); + res->ai_family = config.unix_addr.sun_family; + res->ai_socktype = SOCK_STREAM; + res->ai_addrlen = sizeof(config.unix_addr); + res->ai_addr = + static_cast<struct sockaddr *>(static_cast<void *>(&config.unix_addr)); + + config.addrs = res.release(); + return; + }; + + int rv; + addrinfo hints{}, *res; + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + hints.ai_flags = AI_ADDRCONFIG; + + const auto &resolve_host = + config.connect_to_host.empty() ? config.host : config.connect_to_host; + auto port = + config.connect_to_port == 0 ? config.port : config.connect_to_port; + + rv = + getaddrinfo(resolve_host.c_str(), util::utos(port).c_str(), &hints, &res); + if (rv != 0) { + std::cerr << "getaddrinfo() failed: " << gai_strerror(rv) << std::endl; + exit(EXIT_FAILURE); + } + if (res == nullptr) { + std::cerr << "No address returned" << std::endl; + exit(EXIT_FAILURE); + } + config.addrs = res; +} +} // namespace + +namespace { +std::string get_reqline(const char *uri, const http_parser_url &u) { + std::string reqline; + + if (util::has_uri_field(u, UF_PATH)) { + reqline = util::get_uri_field(uri, u, UF_PATH).str(); + } else { + reqline = "/"; + } + + if (util::has_uri_field(u, UF_QUERY)) { + reqline += '?'; + reqline += util::get_uri_field(uri, u, UF_QUERY); + } + + return reqline; +} +} // namespace + +namespace { +constexpr char UNIX_PATH_PREFIX[] = "unix:"; +} // namespace + +namespace { +bool parse_base_uri(const StringRef &base_uri) { + http_parser_url u{}; + if (http_parser_parse_url(base_uri.c_str(), base_uri.size(), 0, &u) != 0 || + !util::has_uri_field(u, UF_SCHEMA) || !util::has_uri_field(u, UF_HOST)) { + return false; + } + + config.scheme = util::get_uri_field(base_uri.c_str(), u, UF_SCHEMA).str(); + config.host = util::get_uri_field(base_uri.c_str(), u, UF_HOST).str(); + config.default_port = util::get_default_port(base_uri.c_str(), u); + if (util::has_uri_field(u, UF_PORT)) { + config.port = u.port; + } else { + config.port = config.default_port; + } + + return true; +} +} // namespace +namespace { +// Use std::vector<std::string>::iterator explicitly, without that, +// http_parser_url u{} fails with clang-3.4. +std::vector<std::string> parse_uris(std::vector<std::string>::iterator first, + std::vector<std::string>::iterator last) { + std::vector<std::string> reqlines; + + if (first == last) { + std::cerr << "no URI available" << std::endl; + exit(EXIT_FAILURE); + } + + if (!config.has_base_uri()) { + + if (!parse_base_uri(StringRef{*first})) { + std::cerr << "invalid URI: " << *first << std::endl; + exit(EXIT_FAILURE); + } + + config.base_uri = *first; + } + + for (; first != last; ++first) { + http_parser_url u{}; + + auto uri = (*first).c_str(); + + if (http_parser_parse_url(uri, (*first).size(), 0, &u) != 0) { + std::cerr << "invalid URI: " << uri << std::endl; + exit(EXIT_FAILURE); + } + + reqlines.push_back(get_reqline(uri, u)); + } + + return reqlines; +} +} // namespace + +namespace { +std::vector<std::string> read_uri_from_file(std::istream &infile) { + std::vector<std::string> uris; + std::string line_uri; + while (std::getline(infile, line_uri)) { + uris.push_back(line_uri); + } + + return uris; +} +} // namespace + +namespace { +void read_script_from_file( + std::istream &infile, + std::vector<std::chrono::steady_clock::duration> &timings, + std::vector<std::string> &uris) { + std::string script_line; + int line_count = 0; + while (std::getline(infile, script_line)) { + line_count++; + if (script_line.empty()) { + std::cerr << "Empty line detected at line " << line_count + << ". Ignoring and continuing." << std::endl; + continue; + } + + std::size_t pos = script_line.find("\t"); + if (pos == std::string::npos) { + std::cerr << "Invalid line format detected, no tab character at line " + << line_count << ". \n\t" << script_line << std::endl; + exit(EXIT_FAILURE); + } + + const char *start = script_line.c_str(); + char *end; + auto v = std::strtod(start, &end); + + errno = 0; + if (v < 0.0 || !std::isfinite(v) || end == start || errno != 0) { + auto error = errno; + std::cerr << "Time value error at line " << line_count << ". \n\t" + << "value = " << script_line.substr(0, pos) << std::endl; + if (error != 0) { + std::cerr << "\t" << strerror(error) << std::endl; + } + exit(EXIT_FAILURE); + } + + timings.emplace_back( + std::chrono::duration_cast<std::chrono::steady_clock::duration>( + std::chrono::duration<double, std::milli>(v))); + uris.push_back(script_line.substr(pos + 1, script_line.size())); + } +} +} // namespace + +namespace { +std::unique_ptr<Worker> create_worker(uint32_t id, SSL_CTX *ssl_ctx, + size_t nreqs, size_t nclients, + size_t rate, size_t max_samples) { + std::stringstream rate_report; + if (config.is_rate_mode() && nclients > rate) { + rate_report << "Up to " << rate << " client(s) will be created every " + << util::duration_str(config.rate_period) << " "; + } + + if (config.is_timing_based_mode()) { + std::cout << "spawning thread #" << id << ": " << nclients + << " total client(s). Timing-based test with " + << config.warm_up_time << "s of warm-up time and " + << config.duration << "s of main duration for measurements." + << std::endl; + } else { + std::cout << "spawning thread #" << id << ": " << nclients + << " total client(s). " << rate_report.str() << nreqs + << " total requests" << std::endl; + } + + if (config.is_rate_mode()) { + return std::make_unique<Worker>(id, ssl_ctx, nreqs, nclients, rate, + max_samples, &config); + } else { + // Here rate is same as client because the rate_timeout callback + // will be called only once + return std::make_unique<Worker>(id, ssl_ctx, nreqs, nclients, nclients, + max_samples, &config); + } +} +} // namespace + +namespace { +int parse_header_table_size(uint32_t &dst, const char *opt, + const char *optarg) { + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--" << opt << ": Bad option value: " << optarg << std::endl; + return -1; + } + if (n > std::numeric_limits<uint32_t>::max()) { + std::cerr << "--" << opt + << ": Value too large. It should be less than or equal to " + << std::numeric_limits<uint32_t>::max() << std::endl; + return -1; + } + + dst = n; + + return 0; +} +} // namespace + +namespace { +std::string make_http_authority(const Config &config) { + std::string host; + + if (util::numeric_host(config.host.c_str(), AF_INET6)) { + host += '['; + host += config.host; + host += ']'; + } else { + host = config.host; + } + + if (config.port != config.default_port) { + host += ':'; + host += util::utos(config.port); + } + + return host; +} +} // namespace + +namespace { +void print_version(std::ostream &out) { + out << "h2load nghttp2/" NGHTTP2_VERSION << std::endl; +} +} // namespace + +namespace { +void print_usage(std::ostream &out) { + out << R"(Usage: h2load [OPTIONS]... [URI]... +benchmarking tool for HTTP/2 server)" + << std::endl; +} +} // namespace + +namespace { +constexpr char DEFAULT_ALPN_LIST[] = "h2,h2-16,h2-14,http/1.1"; +} // namespace + +namespace { +void print_help(std::ostream &out) { + print_usage(out); + + auto config = Config(); + + out << R"( + <URI> Specify URI to access. Multiple URIs can be specified. + URIs are used in this order for each client. All URIs + are used, then first URI is used and then 2nd URI, and + so on. The scheme, host and port in the subsequent + URIs, if present, are ignored. Those in the first URI + are used solely. Definition of a base URI overrides all + scheme, host or port values. +Options: + -n, --requests=<N> + Number of requests across all clients. If it is used + with --timing-script-file option, this option specifies + the number of requests each client performs rather than + the number of requests across all clients. This option + is ignored if timing-based benchmarking is enabled (see + --duration option). + Default: )" + << config.nreqs << R"( + -c, --clients=<N> + Number of concurrent clients. With -r option, this + specifies the maximum number of connections to be made. + Default: )" + << config.nclients << R"( + -t, --threads=<N> + Number of native threads. + Default: )" + << config.nthreads << R"( + -i, --input-file=<PATH> + Path of a file with multiple URIs are separated by EOLs. + This option will disable URIs getting from command-line. + If '-' is given as <PATH>, URIs will be read from stdin. + URIs are used in this order for each client. All URIs + are used, then first URI is used and then 2nd URI, and + so on. The scheme, host and port in the subsequent + URIs, if present, are ignored. Those in the first URI + are used solely. Definition of a base URI overrides all + scheme, host or port values. + -m, --max-concurrent-streams=<N> + Max concurrent streams to issue per session. When + http/1.1 is used, this specifies the number of HTTP + pipelining requests in-flight. + Default: 1 + -f, --max-frame-size=<SIZE> + Maximum frame size that the local endpoint is willing to + receive. + Default: )" + << util::utos_unit(config.max_frame_size) << R"( + -w, --window-bits=<N> + Sets the stream level initial window size to (2**<N>)-1. + For QUIC, <N> is capped to 26 (roughly 64MiB). + Default: )" + << config.window_bits << R"( + -W, --connection-window-bits=<N> + Sets the connection level initial window size to + (2**<N>)-1. + Default: )" + << config.connection_window_bits << R"( + -H, --header=<HEADER> + Add/Override a header to the requests. + --ciphers=<SUITE> + Set allowed cipher list for TLSv1.2 or earlier. The + format of the string is described in OpenSSL ciphers(1). + Default: )" + << config.ciphers << R"( + --tls13-ciphers=<SUITE> + Set allowed cipher list for TLSv1.3. The format of the + string is described in OpenSSL ciphers(1). + Default: )" + << config.tls13_ciphers << R"( + -p, --no-tls-proto=<PROTOID> + Specify ALPN identifier of the protocol to be used when + accessing http URI without SSL/TLS. + Available protocols: )" + << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( and )" << NGHTTP2_H1_1 << R"( + Default: )" + << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( + -d, --data=<PATH> + Post FILE to server. The request method is changed to + POST. For http/1.1 connection, if -d is used, the + maximum number of in-flight pipelined requests is set to + 1. + -r, --rate=<N> + Specifies the fixed rate at which connections are + created. The rate must be a positive integer, + representing the number of connections to be made per + rate period. The maximum number of connections to be + made is given in -c option. This rate will be + distributed among threads as evenly as possible. For + example, with -t2 and -r4, each thread gets 2 + connections per period. When the rate is 0, the program + will run as it normally does, creating connections at + whatever variable rate it wants. The default value for + this option is 0. -r and -D are mutually exclusive. + --rate-period=<DURATION> + Specifies the time period between creating connections. + The period must be a positive number, representing the + length of the period in time. This option is ignored if + the rate option is not used. The default value for this + option is 1s. + -D, --duration=<DURATION> + Specifies the main duration for the measurements in case + of timing-based benchmarking. -D and -r are mutually + exclusive. + --warm-up-time=<DURATION> + Specifies the time period before starting the actual + measurements, in case of timing-based benchmarking. + Needs to provided along with -D option. + -T, --connection-active-timeout=<DURATION> + Specifies the maximum time that h2load is willing to + keep a connection open, regardless of the activity on + said connection. <DURATION> must be a positive integer, + specifying the amount of time to wait. When no timeout + value is set (either active or inactive), h2load will + keep a connection open indefinitely, waiting for a + response. + -N, --connection-inactivity-timeout=<DURATION> + Specifies the amount of time that h2load is willing to + wait to see activity on a given connection. <DURATION> + must be a positive integer, specifying the amount of + time to wait. When no timeout value is set (either + active or inactive), h2load will keep a connection open + indefinitely, waiting for a response. + --timing-script-file=<PATH> + Path of a file containing one or more lines separated by + EOLs. Each script line is composed of two tab-separated + fields. The first field represents the time offset from + the start of execution, expressed as a positive value of + milliseconds with microsecond resolution. The second + field represents the URI. This option will disable URIs + getting from command-line. If '-' is given as <PATH>, + script lines will be read from stdin. Script lines are + used in order for each client. If -n is given, it must + be less than or equal to the number of script lines, + larger values are clamped to the number of script lines. + If -n is not given, the number of requests will default + to the number of script lines. The scheme, host and + port defined in the first URI are used solely. Values + contained in other URIs, if present, are ignored. + Definition of a base URI overrides all scheme, host or + port values. --timing-script-file and --rps are + mutually exclusive. + -B, --base-uri=(<URI>|unix:<PATH>) + Specify URI from which the scheme, host and port will be + used for all requests. The base URI overrides all + values defined either at the command line or inside + input files. If argument starts with "unix:", then the + rest of the argument will be treated as UNIX domain + socket path. The connection is made through that path + instead of TCP. In this case, scheme is inferred from + the first URI appeared in the command line or inside + input files as usual. + --alpn-list=<LIST> + Comma delimited list of ALPN protocol identifier sorted + in the order of preference. That means most desirable + protocol comes first. The parameter must be delimited + by a single comma only and any white spaces are treated + as a part of protocol string. + Default: )" + << DEFAULT_ALPN_LIST << R"( + --h1 Short hand for --alpn-list=http/1.1 + --no-tls-proto=http/1.1, which effectively force + http/1.1 for both http and https URI. + --header-table-size=<SIZE> + Specify decoder header table size. + Default: )" + << util::utos_unit(config.header_table_size) << R"( + --encoder-header-table-size=<SIZE> + Specify encoder header table size. The decoder (server) + specifies the maximum dynamic table size it accepts. + Then the negotiated dynamic table size is the minimum of + this option value and the value which server specified. + Default: )" + << util::utos_unit(config.encoder_header_table_size) << R"( + --log-file=<PATH> + Write per-request information to a file as tab-separated + columns: start time as microseconds since epoch; HTTP + status code; microseconds until end of response. More + columns may be added later. Rows are ordered by end-of- + response time when using one worker thread, but may + appear slightly out of order with multiple threads due + to buffering. Status code is -1 for failed streams. + --qlog-file-base=<PATH> + Enable qlog output and specify base file name for qlogs. + Qlog is emitted for each connection. For a given base + name "base", each output file name becomes + "base.M.N.sqlog" where M is worker ID and N is client ID + (e.g. "base.0.3.sqlog"). Only effective in QUIC runs. + --connect-to=<HOST>[:<PORT>] + Host and port to connect instead of using the authority + in <URI>. + --rps=<N> Specify request per second for each client. --rps and + --timing-script-file are mutually exclusive. + --groups=<GROUPS> + Specify the supported groups. + Default: )" + << config.groups << R"( + --no-udp-gso + Disable UDP GSO. + --max-udp-payload-size=<SIZE> + Specify the maximum outgoing UDP datagram payload size. + --ktls Enable ktls. + -v, --verbose + Output debug information. + --version Display version information and exit. + -h, --help Display this help and exit. + +-- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms + (hours, minutes, seconds and milliseconds, respectively). If a unit + is omitted, a second is used as unit.)" + << std::endl; +} +} // namespace + +int main(int argc, char **argv) { + std::string datafile; + std::string logfile; + bool nreqs_set_manually = false; + while (1) { + static int flag = 0; + constexpr static option long_options[] = { + {"requests", required_argument, nullptr, 'n'}, + {"clients", required_argument, nullptr, 'c'}, + {"data", required_argument, nullptr, 'd'}, + {"threads", required_argument, nullptr, 't'}, + {"max-concurrent-streams", required_argument, nullptr, 'm'}, + {"window-bits", required_argument, nullptr, 'w'}, + {"max-frame-size", required_argument, nullptr, 'f'}, + {"connection-window-bits", required_argument, nullptr, 'W'}, + {"input-file", required_argument, nullptr, 'i'}, + {"header", required_argument, nullptr, 'H'}, + {"no-tls-proto", required_argument, nullptr, 'p'}, + {"verbose", no_argument, nullptr, 'v'}, + {"help", no_argument, nullptr, 'h'}, + {"version", no_argument, &flag, 1}, + {"ciphers", required_argument, &flag, 2}, + {"rate", required_argument, nullptr, 'r'}, + {"connection-active-timeout", required_argument, nullptr, 'T'}, + {"connection-inactivity-timeout", required_argument, nullptr, 'N'}, + {"duration", required_argument, nullptr, 'D'}, + {"timing-script-file", required_argument, &flag, 3}, + {"base-uri", required_argument, nullptr, 'B'}, + {"npn-list", required_argument, &flag, 4}, + {"rate-period", required_argument, &flag, 5}, + {"h1", no_argument, &flag, 6}, + {"header-table-size", required_argument, &flag, 7}, + {"encoder-header-table-size", required_argument, &flag, 8}, + {"warm-up-time", required_argument, &flag, 9}, + {"log-file", required_argument, &flag, 10}, + {"connect-to", required_argument, &flag, 11}, + {"rps", required_argument, &flag, 12}, + {"groups", required_argument, &flag, 13}, + {"tls13-ciphers", required_argument, &flag, 14}, + {"no-udp-gso", no_argument, &flag, 15}, + {"qlog-file-base", required_argument, &flag, 16}, + {"max-udp-payload-size", required_argument, &flag, 17}, + {"ktls", no_argument, &flag, 18}, + {"alpn-list", required_argument, &flag, 19}, + {nullptr, 0, nullptr, 0}}; + int option_index = 0; + auto c = getopt_long(argc, argv, + "hvW:c:d:m:n:p:t:w:f:H:i:r:T:N:D:B:", long_options, + &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'n': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-n: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.nreqs = n; + nreqs_set_manually = true; + break; + } + case 'c': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-c: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.nclients = n; + break; + } + case 'd': + datafile = optarg; + break; + case 't': { +#ifdef NOTHREADS + std::cerr << "-t: WARNING: Threading disabled at build time, " + << "no threads created." << std::endl; +#else + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-t: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.nthreads = n; +#endif // NOTHREADS + break; + } + case 'm': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-m: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.max_concurrent_streams = n; + break; + } + case 'w': + case 'W': { + auto n = util::parse_uint(optarg); + if (n == -1 || n > 30) { + std::cerr << "-" << static_cast<char>(c) + << ": specify the integer in the range [0, 30], inclusive" + << std::endl; + exit(EXIT_FAILURE); + } + if (c == 'w') { + config.window_bits = n; + } else { + config.connection_window_bits = n; + } + break; + } + case 'f': { + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--max-frame-size: bad option value: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + if (static_cast<uint64_t>(n) < 16_k) { + std::cerr << "--max-frame-size: minimum 16384" << std::endl; + exit(EXIT_FAILURE); + } + if (static_cast<uint64_t>(n) > 16_m - 1) { + std::cerr << "--max-frame-size: maximum 16777215" << std::endl; + exit(EXIT_FAILURE); + } + config.max_frame_size = n; + break; + } + case 'H': { + char *header = optarg; + // Skip first possible ':' in the header name + char *value = strchr(optarg + 1, ':'); + if (!value || (header[0] == ':' && header + 1 == value)) { + std::cerr << "-H: invalid header: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + *value = 0; + value++; + while (isspace(*value)) { + value++; + } + if (*value == 0) { + // This could also be a valid case for suppressing a header + // similar to curl + std::cerr << "-H: invalid header - value missing: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + // Note that there is no processing currently to handle multiple + // message-header fields with the same field name + config.custom_headers.emplace_back(header, value); + util::inp_strlower(config.custom_headers.back().name); + break; + } + case 'i': + config.ifile = optarg; + break; + case 'p': { + auto proto = StringRef{optarg}; + if (util::strieq(StringRef::from_lit(NGHTTP2_CLEARTEXT_PROTO_VERSION_ID), + proto)) { + config.no_tls_proto = Config::PROTO_HTTP2; + } else if (util::strieq(NGHTTP2_H1_1, proto)) { + config.no_tls_proto = Config::PROTO_HTTP1_1; + } else { + std::cerr << "-p: unsupported protocol " << proto << std::endl; + exit(EXIT_FAILURE); + } + break; + } + case 'r': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-r: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + if (n == 0) { + std::cerr << "-r: the rate at which connections are made " + << "must be positive." << std::endl; + exit(EXIT_FAILURE); + } + config.rate = n; + break; + } + case 'T': + config.conn_active_timeout = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.conn_active_timeout)) { + std::cerr << "-T: bad value for the conn_active_timeout wait time: " + << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 'N': + config.conn_inactivity_timeout = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.conn_inactivity_timeout)) { + std::cerr << "-N: bad value for the conn_inactivity_timeout wait time: " + << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 'B': { + auto arg = StringRef{optarg}; + config.base_uri = ""; + config.base_uri_unix = false; + + if (util::istarts_with_l(arg, UNIX_PATH_PREFIX)) { + // UNIX domain socket path + sockaddr_un un; + + auto path = StringRef{std::begin(arg) + str_size(UNIX_PATH_PREFIX), + std::end(arg)}; + + if (path.size() == 0 || path.size() + 1 > sizeof(un.sun_path)) { + std::cerr << "--base-uri: invalid UNIX domain socket path: " << arg + << std::endl; + exit(EXIT_FAILURE); + } + + config.base_uri_unix = true; + + auto &unix_addr = config.unix_addr; + std::copy(std::begin(path), std::end(path), unix_addr.sun_path); + unix_addr.sun_path[path.size()] = '\0'; + unix_addr.sun_family = AF_UNIX; + + break; + } + + if (!parse_base_uri(arg)) { + std::cerr << "--base-uri: invalid base URI: " << arg << std::endl; + exit(EXIT_FAILURE); + } + + config.base_uri = arg.str(); + break; + } + case 'D': + config.duration = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.duration)) { + std::cerr << "-D: value error " << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 'v': + config.verbose = true; + break; + case 'h': + print_help(std::cout); + exit(EXIT_SUCCESS); + case '?': + util::show_candidates(argv[optind - 1], long_options); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // version option + print_version(std::cout); + exit(EXIT_SUCCESS); + case 2: + // ciphers option + config.ciphers = optarg; + break; + case 3: + // timing-script option + config.ifile = optarg; + config.timing_script = true; + break; + case 5: + // rate-period + config.rate_period = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.rate_period)) { + std::cerr << "--rate-period: value error " << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 6: + // --h1 + config.alpn_list = + util::parse_config_str_list(StringRef::from_lit("http/1.1")); + config.no_tls_proto = Config::PROTO_HTTP1_1; + break; + case 7: + // --header-table-size + if (parse_header_table_size(config.header_table_size, + "header-table-size", optarg) != 0) { + exit(EXIT_FAILURE); + } + break; + case 8: + // --encoder-header-table-size + if (parse_header_table_size(config.encoder_header_table_size, + "encoder-header-table-size", optarg) != 0) { + exit(EXIT_FAILURE); + } + break; + case 9: + // --warm-up-time + config.warm_up_time = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.warm_up_time)) { + std::cerr << "--warm-up-time: value error " << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 10: + // --log-file + logfile = optarg; + break; + case 11: { + // --connect-to + auto p = util::split_hostport(StringRef{optarg}); + int64_t port = 0; + if (p.first.empty() || + (!p.second.empty() && (port = util::parse_uint(p.second)) == -1)) { + std::cerr << "--connect-to: Invalid value " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.connect_to_host = p.first.str(); + config.connect_to_port = port; + break; + } + case 12: { + char *end; + auto v = std::strtod(optarg, &end); + if (end == optarg || *end != '\0' || !std::isfinite(v) || + 1. / v < 1e-6) { + std::cerr << "--rps: Invalid value " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.rps = v; + break; + } + case 13: + // --groups + config.groups = optarg; + break; + case 14: + // --tls13-ciphers + config.tls13_ciphers = optarg; + break; + case 15: + // --no-udp-gso + config.no_udp_gso = true; + break; + case 16: + // --qlog-file-base + config.qlog_file_base = optarg; + break; + case 17: { + // --max-udp-payload-size + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--max-udp-payload-size: bad option value: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + if (static_cast<uint64_t>(n) > 64_k) { + std::cerr << "--max-udp-payload-size: must not exceed 65536" + << std::endl; + exit(EXIT_FAILURE); + } + config.max_udp_payload_size = n; + break; + } + case 18: + // --ktls + config.ktls = true; + break; + case 4: + // npn-list option + std::cerr << "--npn-list: deprecated. Use --alpn-list instead." + << std::endl; + // fall through + case 19: + // alpn-list option + config.alpn_list = util::parse_config_str_list(StringRef{optarg}); + break; + } + break; + default: + break; + } + } + + if (argc == optind) { + if (config.ifile.empty()) { + std::cerr << "no URI or input file given" << std::endl; + exit(EXIT_FAILURE); + } + } + + if (config.nclients == 0) { + std::cerr << "-c: the number of clients must be strictly greater than 0." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.alpn_list.empty()) { + config.alpn_list = + util::parse_config_str_list(StringRef::from_lit(DEFAULT_ALPN_LIST)); + } + + // serialize the APLN tokens + for (auto &proto : config.alpn_list) { + proto.insert(proto.begin(), static_cast<unsigned char>(proto.size())); + } + + std::vector<std::string> reqlines; + + if (config.ifile.empty()) { + std::vector<std::string> uris; + std::copy(&argv[optind], &argv[argc], std::back_inserter(uris)); + reqlines = parse_uris(std::begin(uris), std::end(uris)); + } else { + std::vector<std::string> uris; + if (!config.timing_script) { + if (config.ifile == "-") { + uris = read_uri_from_file(std::cin); + } else { + std::ifstream infile(config.ifile); + if (!infile) { + std::cerr << "cannot read input file: " << config.ifile << std::endl; + exit(EXIT_FAILURE); + } + + uris = read_uri_from_file(infile); + } + } else { + if (config.ifile == "-") { + read_script_from_file(std::cin, config.timings, uris); + } else { + std::ifstream infile(config.ifile); + if (!infile) { + std::cerr << "cannot read input file: " << config.ifile << std::endl; + exit(EXIT_FAILURE); + } + + read_script_from_file(infile, config.timings, uris); + } + + if (nreqs_set_manually) { + if (config.nreqs > uris.size()) { + std::cerr << "-n: the number of requests must be less than or equal " + "to the number of timing script entries. Setting number " + "of requests to " + << uris.size() << std::endl; + + config.nreqs = uris.size(); + } + } else { + config.nreqs = uris.size(); + } + } + + reqlines = parse_uris(std::begin(uris), std::end(uris)); + } + + if (reqlines.empty()) { + std::cerr << "No URI given" << std::endl; + exit(EXIT_FAILURE); + } + + if (config.is_timing_based_mode() && config.is_rate_mode()) { + std::cerr << "-r, -D: they are mutually exclusive." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.timing_script && config.rps_enabled()) { + std::cerr << "--timing-script-file, --rps: they are mutually exclusive." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nreqs == 0 && !config.is_timing_based_mode()) { + std::cerr << "-n: the number of requests must be strictly greater than 0 " + "if timing-based test is not being run." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.max_concurrent_streams == 0) { + std::cerr << "-m: the max concurrent streams must be strictly greater " + << "than 0." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nthreads == 0) { + std::cerr << "-t: the number of threads must be strictly greater than 0." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nthreads > std::thread::hardware_concurrency()) { + std::cerr << "-t: warning: the number of threads is greater than hardware " + << "cores." << std::endl; + } + + // With timing script, we don't distribute config.nreqs to each + // client or thread. + if (!config.timing_script && config.nreqs < config.nclients && + !config.is_timing_based_mode()) { + std::cerr << "-n, -c: the number of requests must be greater than or " + << "equal to the clients." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nclients < config.nthreads) { + std::cerr << "-c, -t: the number of clients must be greater than or equal " + << "to the number of threads." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.is_timing_based_mode()) { + config.nreqs = 0; + } + + if (config.is_rate_mode()) { + if (config.rate < config.nthreads) { + std::cerr << "-r, -t: the connection rate must be greater than or equal " + << "to the number of threads." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.rate > config.nclients) { + std::cerr << "-r, -c: the connection rate must be smaller than or equal " + "to the number of clients." + << std::endl; + exit(EXIT_FAILURE); + } + } + + if (!datafile.empty()) { + config.data_fd = open(datafile.c_str(), O_RDONLY | O_BINARY); + if (config.data_fd == -1) { + std::cerr << "-d: Could not open file " << datafile << std::endl; + exit(EXIT_FAILURE); + } + struct stat data_stat; + if (fstat(config.data_fd, &data_stat) == -1) { + std::cerr << "-d: Could not stat file " << datafile << std::endl; + exit(EXIT_FAILURE); + } + config.data_length = data_stat.st_size; + auto addr = mmap(nullptr, config.data_length, PROT_READ, MAP_SHARED, + config.data_fd, 0); + if (addr == MAP_FAILED) { + std::cerr << "-d: Could not mmap file " << datafile << std::endl; + exit(EXIT_FAILURE); + } + config.data = static_cast<uint8_t *>(addr); + } + + if (!logfile.empty()) { + config.log_fd = open(logfile.c_str(), O_WRONLY | O_CREAT | O_APPEND, + S_IRUSR | S_IWUSR | S_IRGRP); + if (config.log_fd == -1) { + std::cerr << "--log-file: Could not open file " << logfile << std::endl; + exit(EXIT_FAILURE); + } + } + + if (!config.qlog_file_base.empty() && !config.is_quic()) { + std::cerr << "Warning: --qlog-file-base: only effective in quic, ignoring." + << std::endl; + } + + struct sigaction act {}; + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, nullptr); + + auto ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx) { + std::cerr << "Failed to create SSL_CTX: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + exit(EXIT_FAILURE); + } + + auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; + +#ifdef SSL_OP_ENABLE_KTLS + if (config.ktls) { + ssl_opts |= SSL_OP_ENABLE_KTLS; + } +#endif // SSL_OP_ENABLE_KTLS + + SSL_CTX_set_options(ssl_ctx, ssl_opts); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 +# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS + if (ngtcp2_crypto_quictls_configure_client_context(ssl_ctx) != 0) { + std::cerr << "ngtcp2_crypto_quictls_configure_client_context failed" + << std::endl; + exit(EXIT_FAILURE); + } +# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL + if (ngtcp2_crypto_boringssl_configure_client_context(ssl_ctx) != 0) { + std::cerr << "ngtcp2_crypto_boringssl_configure_client_context failed" + << std::endl; + exit(EXIT_FAILURE); + } +# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +#endif // ENABLE_HTTP3 + } else if (nghttp2::tls::ssl_ctx_set_proto_versions( + ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, + nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { + std::cerr << "Could not set TLS versions" << std::endl; + exit(EXIT_FAILURE); + } + + if (SSL_CTX_set_cipher_list(ssl_ctx, config.ciphers.c_str()) == 0) { + std::cerr << "SSL_CTX_set_cipher_list with " << config.ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + exit(EXIT_FAILURE); + } + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_LIBRESSL) + if (SSL_CTX_set_ciphersuites(ssl_ctx, config.tls13_ciphers.c_str()) == 0) { + std::cerr << "SSL_CTX_set_ciphersuites with " << config.tls13_ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + exit(EXIT_FAILURE); + } +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_LIBRESSL + + if (SSL_CTX_set1_groups_list(ssl_ctx, config.groups.c_str()) != 1) { + std::cerr << "SSL_CTX_set1_groups_list failed" << std::endl; + exit(EXIT_FAILURE); + } + + std::vector<unsigned char> proto_list; + for (const auto &proto : config.alpn_list) { + std::copy_n(proto.c_str(), proto.size(), std::back_inserter(proto_list)); + } + + SSL_CTX_set_alpn_protos(ssl_ctx, proto_list.data(), proto_list.size()); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + SSL_CTX_set_keylog_callback(ssl_ctx, keylog_callback); + } + } + + std::string user_agent = "h2load nghttp2/" NGHTTP2_VERSION; + Headers shared_nva; + shared_nva.emplace_back(":scheme", config.scheme); + shared_nva.emplace_back(":authority", make_http_authority(config)); + shared_nva.emplace_back(":method", config.data_fd == -1 ? "GET" : "POST"); + shared_nva.emplace_back("user-agent", user_agent); + + // list header fields that can be overridden. + auto override_hdrs = make_array<std::string>(":authority", ":host", ":method", + ":scheme", "user-agent"); + + for (auto &kv : config.custom_headers) { + if (std::find(std::begin(override_hdrs), std::end(override_hdrs), + kv.name) != std::end(override_hdrs)) { + // override header + for (auto &nv : shared_nva) { + if ((nv.name == ":authority" && kv.name == ":host") || + (nv.name == kv.name)) { + nv.value = kv.value; + } + } + } else { + // add additional headers + shared_nva.push_back(kv); + } + } + + std::string content_length_str; + if (config.data_fd != -1) { + content_length_str = util::utos(config.data_length); + } + + auto method_it = + std::find_if(std::begin(shared_nva), std::end(shared_nva), + [](const Header &nv) { return nv.name == ":method"; }); + assert(method_it != std::end(shared_nva)); + + config.h1reqs.reserve(reqlines.size()); + config.nva.reserve(reqlines.size()); + + for (auto &req : reqlines) { + // For HTTP/1.1 + auto h1req = (*method_it).value; + h1req += ' '; + h1req += req; + h1req += " HTTP/1.1\r\n"; + for (auto &nv : shared_nva) { + if (nv.name == ":authority") { + h1req += "Host: "; + h1req += nv.value; + h1req += "\r\n"; + continue; + } + if (nv.name[0] == ':') { + continue; + } + h1req += nv.name; + h1req += ": "; + h1req += nv.value; + h1req += "\r\n"; + } + + if (!content_length_str.empty()) { + h1req += "Content-Length: "; + h1req += content_length_str; + h1req += "\r\n"; + } + h1req += "\r\n"; + + config.h1reqs.push_back(std::move(h1req)); + + // For nghttp2 + std::vector<nghttp2_nv> nva; + // 2 for :path, and possible content-length + nva.reserve(2 + shared_nva.size()); + + nva.push_back(http2::make_nv_ls(":path", req)); + + for (auto &nv : shared_nva) { + nva.push_back(http2::make_nv(nv.name, nv.value, false)); + } + + if (!content_length_str.empty()) { + nva.push_back(http2::make_nv(StringRef::from_lit("content-length"), + StringRef{content_length_str})); + } + + config.nva.push_back(std::move(nva)); + } + + // Don't DOS our server! + if (config.host == "nghttp2.org") { + std::cerr << "Using h2load against public server " << config.host + << " should be prohibited." << std::endl; + exit(EXIT_FAILURE); + } + + resolve_host(); + + std::cout << "starting benchmark..." << std::endl; + + std::vector<std::unique_ptr<Worker>> workers; + workers.reserve(config.nthreads); + +#ifndef NOTHREADS + size_t nreqs_per_thread = 0; + ssize_t nreqs_rem = 0; + + if (!config.timing_script) { + nreqs_per_thread = config.nreqs / config.nthreads; + nreqs_rem = config.nreqs % config.nthreads; + } + + size_t nclients_per_thread = config.nclients / config.nthreads; + ssize_t nclients_rem = config.nclients % config.nthreads; + + size_t rate_per_thread = config.rate / config.nthreads; + ssize_t rate_per_thread_rem = config.rate % config.nthreads; + + size_t max_samples_per_thread = + std::max(static_cast<size_t>(256), MAX_SAMPLES / config.nthreads); + + std::mutex mu; + std::condition_variable cv; + auto ready = false; + + std::vector<std::future<void>> futures; + for (size_t i = 0; i < config.nthreads; ++i) { + auto rate = rate_per_thread; + if (rate_per_thread_rem > 0) { + --rate_per_thread_rem; + ++rate; + } + auto nclients = nclients_per_thread; + if (nclients_rem > 0) { + --nclients_rem; + ++nclients; + } + + size_t nreqs; + if (config.timing_script) { + // With timing script, each client issues config.nreqs requests. + // We divide nreqs by number of clients in Worker ctor to + // distribute requests to those clients evenly, so multiply + // config.nreqs here by config.nclients. + nreqs = config.nreqs * nclients; + } else { + nreqs = nreqs_per_thread; + if (nreqs_rem > 0) { + --nreqs_rem; + ++nreqs; + } + } + + workers.push_back(create_worker(i, ssl_ctx, nreqs, nclients, rate, + max_samples_per_thread)); + auto &worker = workers.back(); + futures.push_back( + std::async(std::launch::async, [&worker, &mu, &cv, &ready]() { + { + std::unique_lock<std::mutex> ulk(mu); + cv.wait(ulk, [&ready] { return ready; }); + } + worker->run(); + })); + } + + { + std::lock_guard<std::mutex> lg(mu); + ready = true; + cv.notify_all(); + } + + auto start = std::chrono::steady_clock::now(); + + for (auto &fut : futures) { + fut.get(); + } + +#else // NOTHREADS + auto rate = config.rate; + auto nclients = config.nclients; + auto nreqs = + config.timing_script ? config.nreqs * config.nclients : config.nreqs; + + workers.push_back( + create_worker(0, ssl_ctx, nreqs, nclients, rate, MAX_SAMPLES)); + + auto start = std::chrono::steady_clock::now(); + + workers.back()->run(); +#endif // NOTHREADS + + auto end = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::microseconds>(end - start); + + Stats stats(0, 0); + for (const auto &w : workers) { + const auto &s = w->stats; + + stats.req_todo += s.req_todo; + stats.req_started += s.req_started; + stats.req_done += s.req_done; + stats.req_timedout += s.req_timedout; + stats.req_success += s.req_success; + stats.req_status_success += s.req_status_success; + stats.req_failed += s.req_failed; + stats.req_error += s.req_error; + stats.bytes_total += s.bytes_total; + stats.bytes_head += s.bytes_head; + stats.bytes_head_decomp += s.bytes_head_decomp; + stats.bytes_body += s.bytes_body; + stats.udp_dgram_recv += s.udp_dgram_recv; + stats.udp_dgram_sent += s.udp_dgram_sent; + + for (size_t i = 0; i < stats.status.size(); ++i) { + stats.status[i] += s.status[i]; + } + } + + auto ts = process_time_stats(workers); + + // Requests which have not been issued due to connection errors, are + // counted towards req_failed and req_error. + auto req_not_issued = + (stats.req_todo - stats.req_status_success - stats.req_failed); + stats.req_failed += req_not_issued; + stats.req_error += req_not_issued; + + // UI is heavily inspired by weighttp[1] and wrk[2] + // + // [1] https://github.com/lighttpd/weighttp + // [2] https://github.com/wg/wrk + double rps = 0; + int64_t bps = 0; + if (duration.count() > 0) { + if (config.is_timing_based_mode()) { + // we only want to consider the main duration if warm-up is given + rps = stats.req_success / config.duration; + bps = stats.bytes_total / config.duration; + } else { + auto secd = std::chrono::duration_cast< + std::chrono::duration<double, std::chrono::seconds::period>>( + duration); + rps = stats.req_success / secd.count(); + bps = stats.bytes_total / secd.count(); + } + } + + double header_space_savings = 0.; + if (stats.bytes_head_decomp > 0) { + header_space_savings = + 1. - static_cast<double>(stats.bytes_head) / stats.bytes_head_decomp; + } + + std::cout << std::fixed << std::setprecision(2) << R"( +finished in )" + << util::format_duration(duration) << ", " << rps << " req/s, " + << util::utos_funit(bps) << R"(B/s +requests: )" << stats.req_todo + << " total, " << stats.req_started << " started, " << stats.req_done + << " done, " << stats.req_status_success << " succeeded, " + << stats.req_failed << " failed, " << stats.req_error + << " errored, " << stats.req_timedout << R"( timeout +status codes: )" + << stats.status[2] << " 2xx, " << stats.status[3] << " 3xx, " + << stats.status[4] << " 4xx, " << stats.status[5] << R"( 5xx +traffic: )" << util::utos_funit(stats.bytes_total) + << "B (" << stats.bytes_total << ") total, " + << util::utos_funit(stats.bytes_head) << "B (" << stats.bytes_head + << ") headers (space savings " << header_space_savings * 100 + << "%), " << util::utos_funit(stats.bytes_body) << "B (" + << stats.bytes_body << R"() data)" << std::endl; +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + std::cout << "UDP datagram: " << stats.udp_dgram_sent << " sent, " + << stats.udp_dgram_recv << " received" << std::endl; + } +#endif // ENABLE_HTTP3 + std::cout + << R"( min max mean sd +/- sd +time for request: )" + << std::setw(10) << util::format_duration(ts.request.min) << " " + << std::setw(10) << util::format_duration(ts.request.max) << " " + << std::setw(10) << util::format_duration(ts.request.mean) << " " + << std::setw(10) << util::format_duration(ts.request.sd) << std::setw(9) + << util::dtos(ts.request.within_sd) << "%" + << "\ntime for connect: " << std::setw(10) + << util::format_duration(ts.connect.min) << " " << std::setw(10) + << util::format_duration(ts.connect.max) << " " << std::setw(10) + << util::format_duration(ts.connect.mean) << " " << std::setw(10) + << util::format_duration(ts.connect.sd) << std::setw(9) + << util::dtos(ts.connect.within_sd) << "%" + << "\ntime to 1st byte: " << std::setw(10) + << util::format_duration(ts.ttfb.min) << " " << std::setw(10) + << util::format_duration(ts.ttfb.max) << " " << std::setw(10) + << util::format_duration(ts.ttfb.mean) << " " << std::setw(10) + << util::format_duration(ts.ttfb.sd) << std::setw(9) + << util::dtos(ts.ttfb.within_sd) << "%" + << "\nreq/s : " << std::setw(10) << ts.rps.min << " " + << std::setw(10) << ts.rps.max << " " << std::setw(10) << ts.rps.mean + << " " << std::setw(10) << ts.rps.sd << std::setw(9) + << util::dtos(ts.rps.within_sd) << "%" << std::endl; + + SSL_CTX_free(ssl_ctx); + + if (config.log_fd != -1) { + close(config.log_fd); + } + + return 0; +} + +} // namespace h2load + +int main(int argc, char **argv) { return h2load::main(argc, argv); } diff --git a/src/h2load.h b/src/h2load.h new file mode 100644 index 0000000..11bb54c --- /dev/null +++ b/src/h2load.h @@ -0,0 +1,510 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_H +#define H2LOAD_H + +#include "nghttp2_config.h" + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H +#include <sys/un.h> + +#include <vector> +#include <string> +#include <unordered_map> +#include <memory> +#include <chrono> +#include <array> + +#include <nghttp2/nghttp2.h> + +#ifdef ENABLE_HTTP3 +# include <ngtcp2/ngtcp2.h> +# include <ngtcp2/ngtcp2_crypto.h> +#endif // ENABLE_HTTP3 + +#include <ev.h> + +#include <openssl/ssl.h> + +#include "http2.h" +#ifdef ENABLE_HTTP3 +# include "quic.h" +#endif // ENABLE_HTTP3 +#include "memchunk.h" +#include "template.h" + +using namespace nghttp2; + +namespace h2load { + +constexpr auto BACKOFF_WRITE_BUFFER_THRES = 16_k; + +class Session; +struct Worker; + +struct Config { + std::vector<std::vector<nghttp2_nv>> nva; + std::vector<std::string> h1reqs; + std::vector<std::chrono::steady_clock::duration> timings; + nghttp2::Headers custom_headers; + std::string scheme; + std::string host; + std::string connect_to_host; + std::string ifile; + std::string ciphers; + std::string tls13_ciphers; + // supported groups (or curves). + std::string groups; + // length of upload data + int64_t data_length; + // memory mapped upload data + uint8_t *data; + addrinfo *addrs; + size_t nreqs; + size_t nclients; + size_t nthreads; + // The maximum number of concurrent streams per session. + ssize_t max_concurrent_streams; + size_t window_bits; + size_t connection_window_bits; + size_t max_frame_size; + // rate at which connections should be made + size_t rate; + ev_tstamp rate_period; + // amount of time for main measurements in timing-based test + ev_tstamp duration; + // amount of time to wait before starting measurements in timing-based test + ev_tstamp warm_up_time; + // amount of time to wait for activity on a given connection + ev_tstamp conn_active_timeout; + // amount of time to wait after the last request is made on a connection + ev_tstamp conn_inactivity_timeout; + enum { PROTO_HTTP2, PROTO_HTTP1_1 } no_tls_proto; + uint32_t header_table_size; + uint32_t encoder_header_table_size; + // file descriptor for upload data + int data_fd; + // file descriptor to write per-request stats to. + int log_fd; + // base file name of qlog output files + std::string qlog_file_base; + uint16_t port; + uint16_t default_port; + uint16_t connect_to_port; + bool verbose; + bool timing_script; + std::string base_uri; + // true if UNIX domain socket is used. In this case, base_uri is + // not used in usual way. + bool base_uri_unix; + // used when UNIX domain socket is used (base_uri_unix is true). + sockaddr_un unix_addr; + // list of supported ALPN protocol strings in the order of + // preference. + std::vector<std::string> alpn_list; + // The number of request per second for each client. + double rps; + // Disables GSO for UDP connections. + bool no_udp_gso; + // The maximum UDP datagram payload size to send. + size_t max_udp_payload_size; + // Enable ktls. + bool ktls; + + Config(); + ~Config(); + + bool is_rate_mode() const; + bool is_timing_based_mode() const; + bool has_base_uri() const; + bool rps_enabled() const; + bool is_quic() const; +}; + +struct RequestStat { + // time point when request was sent + std::chrono::steady_clock::time_point request_time; + // same, but in wall clock reference frame + std::chrono::system_clock::time_point request_wall_time; + // time point when stream was closed + std::chrono::steady_clock::time_point stream_close_time; + // upload data length sent so far + int64_t data_offset; + // HTTP status code + int status; + // true if stream was successfully closed. This means stream was + // not reset, but it does not mean HTTP level error (e.g., 404). + bool completed; +}; + +struct ClientStat { + // time client started (i.e., first connect starts) + std::chrono::steady_clock::time_point client_start_time; + // time client end (i.e., client somehow processed all requests it + // is responsible for, and disconnected) + std::chrono::steady_clock::time_point client_end_time; + // The number of requests completed successful, but not necessarily + // means successful HTTP status code. + size_t req_success; + + // The following 3 numbers are overwritten each time when connection + // is made. + + // time connect starts + std::chrono::steady_clock::time_point connect_start_time; + // time to connect + std::chrono::steady_clock::time_point connect_time; + // time to first byte (TTFB) + std::chrono::steady_clock::time_point ttfb; +}; + +struct SDStat { + // min, max, mean and sd (standard deviation) + double min, max, mean, sd; + // percentage of samples inside mean -/+ sd + double within_sd; +}; + +struct SDStats { + // time for request + SDStat request; + // time for connect + SDStat connect; + // time to first byte (TTFB) + SDStat ttfb; + // request per second for each client + SDStat rps; +}; + +struct Stats { + Stats(size_t req_todo, size_t nclients); + // The total number of requests + size_t req_todo; + // The number of requests issued so far + size_t req_started; + // The number of requests finished + size_t req_done; + // The number of requests completed successful, but not necessarily + // means successful HTTP status code. + size_t req_success; + // The number of requests marked as success. HTTP status code is + // also considered as success. This is subset of req_done. + size_t req_status_success; + // The number of requests failed. This is subset of req_done. + size_t req_failed; + // The number of requests failed due to network errors. This is + // subset of req_failed. + size_t req_error; + // The number of requests that failed due to timeout. + size_t req_timedout; + // The number of bytes received on the "wire". If SSL/TLS is used, + // this is the number of decrypted bytes the application received. + int64_t bytes_total; + // The number of bytes received for header fields. This is + // compressed version. + int64_t bytes_head; + // The number of bytes received for header fields after they are + // decompressed. + int64_t bytes_head_decomp; + // The number of bytes received in DATA frame. + int64_t bytes_body; + // The number of each HTTP status category, status[i] is status code + // in the range [i*100, (i+1)*100). + std::array<size_t, 6> status; + // The statistics per request + std::vector<RequestStat> req_stats; + // The statistics per client + std::vector<ClientStat> client_stats; + // The number of UDP datagrams received. + size_t udp_dgram_recv; + // The number of UDP datagrams sent. + size_t udp_dgram_sent; +}; + +enum ClientState { CLIENT_IDLE, CLIENT_CONNECTED }; + +// This type tells whether the client is in warmup phase or not or is over +enum class Phase { + INITIAL_IDLE, // Initial idle state before warm-up phase + WARM_UP, // Warm up phase when no measurements are done + MAIN_DURATION, // Main measurement phase; if timing-based + // test is not run, this is the default phase + DURATION_OVER // This phase occurs after the measurements are over +}; + +struct Client; + +// We use reservoir sampling method +struct Sampling { + // maximum number of samples + size_t max_samples; + // number of samples seen, including discarded samples. + size_t n; +}; + +struct Worker { + MemchunkPool mcpool; + std::mt19937 randgen; + Stats stats; + Sampling request_times_smp; + Sampling client_smp; + struct ev_loop *loop; + SSL_CTX *ssl_ctx; + Config *config; + size_t progress_interval; + uint32_t id; + bool tls_info_report_done; + bool app_info_report_done; + size_t nconns_made; + // number of clients this worker handles + size_t nclients; + // number of requests each client issues + size_t nreqs_per_client; + // at most nreqs_rem clients get an extra request + size_t nreqs_rem; + size_t rate; + // maximum number of samples in this worker thread + size_t max_samples; + ev_timer timeout_watcher; + // The next client ID this worker assigns + uint32_t next_client_id; + // Keeps track of the current phase (for timing-based experiment) for the + // worker + Phase current_phase; + // We need to keep track of the clients in order to stop them when needed + std::vector<Client *> clients; + // This is only active when there is not a bounded number of requests + // specified + ev_timer duration_watcher; + ev_timer warmup_watcher; + + Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t nreq_todo, size_t nclients, + size_t rate, size_t max_samples, Config *config); + ~Worker(); + Worker(Worker &&o) = default; + void run(); + void sample_req_stat(RequestStat *req_stat); + void sample_client_stat(ClientStat *cstat); + void report_progress(); + void report_rate_progress(); + // This function calls the destructors of all the clients. + void stop_all_clients(); + // This function frees a client from the list of clients for this Worker. + void free_client(Client *); +}; + +struct Stream { + RequestStat req_stat; + int status_success; + Stream(); +}; + +struct Client { + DefaultMemchunks wb; + std::unordered_map<int32_t, Stream> streams; + ClientStat cstat; + std::unique_ptr<Session> session; + ev_io wev; + ev_io rev; + std::function<int(Client &)> readfn, writefn; + Worker *worker; + SSL *ssl; +#ifdef ENABLE_HTTP3 + struct { + ngtcp2_crypto_conn_ref conn_ref; + ev_timer pkt_timer; + ngtcp2_conn *conn; + ngtcp2_ccerr last_error; + bool close_requested; + FILE *qlog_file; + + struct { + bool send_blocked; + size_t num_blocked; + size_t num_blocked_sent; + struct { + Address remote_addr; + const uint8_t *data; + size_t datalen; + size_t gso_size; + } blocked[2]; + std::unique_ptr<uint8_t[]> data; + } tx; + } quic; +#endif // ENABLE_HTTP3 + ev_timer request_timeout_watcher; + addrinfo *next_addr; + // Address for the current address. When try_new_connection() is + // used and current_addr is not nullptr, it is used instead of + // trying next address though next_addr. To try new address, set + // nullptr to current_addr before calling connect(). + addrinfo *current_addr; + size_t reqidx; + ClientState state; + // The number of requests this client has to issue. + size_t req_todo; + // The number of requests left to issue + size_t req_left; + // The number of requests currently have started, but not abandoned + // or finished. + size_t req_inflight; + // The number of requests this client has issued so far. + size_t req_started; + // The number of requests this client has done so far. + size_t req_done; + // The client id per worker + uint32_t id; + int fd; + Address local_addr; + ev_timer conn_active_watcher; + ev_timer conn_inactivity_watcher; + std::string selected_proto; + bool new_connection_requested; + // true if the current connection will be closed, and no more new + // request cannot be processed. + bool final; + // rps_watcher is a timer to invoke callback periodically to + // generate a new request. + ev_timer rps_watcher; + // The timestamp that starts the period which contributes to the + // next request generation. + std::chrono::steady_clock::time_point rps_duration_started; + // The number of requests allowed by rps, but limited by stream + // concurrency. + size_t rps_req_pending; + // The number of in-flight streams. req_inflight has similar value + // but it only measures requests made during Phase::MAIN_DURATION. + // rps_req_inflight measures the number of requests in all phases, + // and it is only used if --rps is given. + size_t rps_req_inflight; + + enum { ERR_CONNECT_FAIL = -100 }; + + Client(uint32_t id, Worker *worker, size_t req_todo); + ~Client(); + int make_socket(addrinfo *addr); + int connect(); + void disconnect(); + void fail(); + // Call this function when do_read() returns -1. This function + // tries to connect to the remote host again if it is requested. If + // so, this function returns 0, and this object should be retained. + // Otherwise, this function returns -1, and this object should be + // deleted. + int try_again_or_fail(); + void timeout(); + void restart_timeout(); + int submit_request(); + void process_request_failure(); + void process_timedout_streams(); + void process_abandoned_streams(); + void report_tls_info(); + void report_app_info(); + void terminate_session(); + // Asks client to create new connection, instead of just fail. + void try_new_connection(); + + int do_read(); + int do_write(); + + // low-level I/O callback functions called by do_read/do_write + int connected(); + int read_clear(); + int write_clear(); + int tls_handshake(); + int read_tls(); + int write_tls(); + + int on_read(const uint8_t *data, size_t len); + int on_write(); + + int connection_made(); + + void on_request(int32_t stream_id); + void on_header(int32_t stream_id, const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen); + void on_status_code(int32_t stream_id, uint16_t status); + // |success| == true means that the request/response was exchanged + // |successfully, but it does not mean response carried successful + // |HTTP status code. + void on_stream_close(int32_t stream_id, bool success, bool final = false); + // Returns RequestStat for |stream_id|. This function must be + // called after on_request(stream_id), and before + // on_stream_close(stream_id, ...). Otherwise, this will return + // nullptr. + RequestStat *get_req_stat(int32_t stream_id); + + void record_request_time(RequestStat *req_stat); + void record_connect_start_time(); + void record_connect_time(); + void record_ttfb(); + void clear_connect_times(); + void record_client_start_time(); + void record_client_end_time(); + + void signal_write(); + +#ifdef ENABLE_HTTP3 + // QUIC + int quic_init(const sockaddr *local_addr, socklen_t local_addrlen, + const sockaddr *remote_addr, socklen_t remote_addrlen); + void quic_free(); + int read_quic(); + int write_quic(); + int write_udp(const sockaddr *addr, socklen_t addrlen, const uint8_t *data, + size_t datalen, size_t gso_size); + void on_send_blocked(const ngtcp2_addr &remote_addr, const uint8_t *data, + size_t datalen, size_t gso_size); + int send_blocked_packet(); + void quic_close_connection(); + + int quic_handshake_completed(); + int quic_recv_stream_data(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen); + int quic_acked_stream_data_offset(int64_t stream_id, size_t datalen); + int quic_stream_close(int64_t stream_id, uint64_t app_error_code); + int quic_stream_reset(int64_t stream_id, uint64_t app_error_code); + int quic_stream_stop_sending(int64_t stream_id, uint64_t app_error_code); + int quic_extend_max_local_streams(); + int quic_extend_max_stream_data(int64_t stream_id); + + int quic_write_client_handshake(ngtcp2_encryption_level level, + const uint8_t *data, size_t datalen); + int quic_pkt_timeout(); + void quic_restart_pkt_timer(); + void quic_write_qlog(const void *data, size_t datalen); + int quic_make_http3_session(); +#endif // ENABLE_HTTP3 +}; + +} // namespace h2load + +#endif // H2LOAD_H diff --git a/src/h2load_http1_session.cc b/src/h2load_http1_session.cc new file mode 100644 index 0000000..6bdcd04 --- /dev/null +++ b/src/h2load_http1_session.cc @@ -0,0 +1,306 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 British Broadcasting Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load_http1_session.h" + +#include <cassert> +#include <cerrno> + +#include "h2load.h" +#include "util.h" +#include "template.h" + +#include <iostream> +#include <fstream> + +using namespace nghttp2; + +namespace h2load { + +namespace { +// HTTP response message begin +int htp_msg_begincb(llhttp_t *htp) { + auto session = static_cast<Http1Session *>(htp->data); + + if (session->stream_resp_counter_ > session->stream_req_counter_) { + return -1; + } + + return 0; +} +} // namespace + +namespace { +// HTTP response status code +int htp_statuscb(llhttp_t *htp, const char *at, size_t length) { + auto session = static_cast<Http1Session *>(htp->data); + auto client = session->get_client(); + + if (htp->status_code / 100 == 1) { + return 0; + } + + client->on_status_code(session->stream_resp_counter_, htp->status_code); + + return 0; +} +} // namespace + +namespace { +// HTTP response message complete +int htp_msg_completecb(llhttp_t *htp) { + auto session = static_cast<Http1Session *>(htp->data); + auto client = session->get_client(); + + if (htp->status_code / 100 == 1) { + return 0; + } + + client->final = llhttp_should_keep_alive(htp) == 0; + auto req_stat = client->get_req_stat(session->stream_resp_counter_); + + assert(req_stat); + + auto config = client->worker->config; + if (req_stat->data_offset >= config->data_length) { + client->on_stream_close(session->stream_resp_counter_, true, client->final); + } + + session->stream_resp_counter_ += 2; + + if (client->final) { + session->stream_req_counter_ = session->stream_resp_counter_; + + // Connection is going down. If we have still request to do, + // create new connection and keep on doing the job. + if (client->req_left) { + client->try_new_connection(); + } + + return HPE_PAUSED; + } + + return 0; +} +} // namespace + +namespace { +int htp_hdr_keycb(llhttp_t *htp, const char *data, size_t len) { + auto session = static_cast<Http1Session *>(htp->data); + auto client = session->get_client(); + + client->worker->stats.bytes_head += len; + client->worker->stats.bytes_head_decomp += len; + return 0; +} +} // namespace + +namespace { +int htp_hdr_valcb(llhttp_t *htp, const char *data, size_t len) { + auto session = static_cast<Http1Session *>(htp->data); + auto client = session->get_client(); + + client->worker->stats.bytes_head += len; + client->worker->stats.bytes_head_decomp += len; + return 0; +} +} // namespace + +namespace { +int htp_hdrs_completecb(llhttp_t *htp) { + return !http2::expect_response_body(htp->status_code); +} +} // namespace + +namespace { +int htp_body_cb(llhttp_t *htp, const char *data, size_t len) { + auto session = static_cast<Http1Session *>(htp->data); + auto client = session->get_client(); + + client->record_ttfb(); + client->worker->stats.bytes_body += len; + + return 0; +} +} // namespace + +namespace { +constexpr llhttp_settings_t htp_hooks = { + htp_msg_begincb, // llhttp_cb on_message_begin; + nullptr, // llhttp_data_cb on_url; + htp_statuscb, // llhttp_data_cb on_status; + nullptr, // llhttp_data_cb on_method; + nullptr, // llhttp_data_cb on_version; + htp_hdr_keycb, // llhttp_data_cb on_header_field; + htp_hdr_valcb, // llhttp_data_cb on_header_value; + nullptr, // llhttp_data_cb on_chunk_extension_name; + nullptr, // llhttp_data_cb on_chunk_extension_value; + htp_hdrs_completecb, // llhttp_cb on_headers_complete; + htp_body_cb, // llhttp_data_cb on_body; + htp_msg_completecb, // llhttp_cb on_message_complete; + nullptr, // llhttp_cb on_url_complete; + nullptr, // llhttp_cb on_status_complete; + nullptr, // llhttp_cb on_method_complete; + nullptr, // llhttp_cb on_version_complete; + nullptr, // llhttp_cb on_header_field_complete; + nullptr, // llhttp_cb on_header_value_complete; + nullptr, // llhttp_cb on_chunk_extension_name_complete; + nullptr, // llhttp_cb on_chunk_extension_value_complete; + nullptr, // llhttp_cb on_chunk_header; + nullptr, // llhttp_cb on_chunk_complete; + nullptr, // llhttp_cb on_reset; +}; +} // namespace + +Http1Session::Http1Session(Client *client) + : stream_req_counter_(1), + stream_resp_counter_(1), + client_(client), + htp_(), + complete_(false) { + llhttp_init(&htp_, HTTP_RESPONSE, &htp_hooks); + htp_.data = this; +} + +Http1Session::~Http1Session() {} + +void Http1Session::on_connect() { client_->signal_write(); } + +int Http1Session::submit_request() { + auto config = client_->worker->config; + const auto &req = config->h1reqs[client_->reqidx]; + client_->reqidx++; + + if (client_->reqidx == config->h1reqs.size()) { + client_->reqidx = 0; + } + + client_->on_request(stream_req_counter_); + + auto req_stat = client_->get_req_stat(stream_req_counter_); + + client_->record_request_time(req_stat); + client_->wb.append(req); + + if (config->data_fd == -1 || config->data_length == 0) { + // increment for next request + stream_req_counter_ += 2; + + return 0; + } + + return on_write(); +} + +int Http1Session::on_read(const uint8_t *data, size_t len) { + auto htperr = + llhttp_execute(&htp_, reinterpret_cast<const char *>(data), len); + auto nread = htperr == HPE_OK + ? len + : static_cast<size_t>(reinterpret_cast<const uint8_t *>( + llhttp_get_error_pos(&htp_)) - + data); + + if (client_->worker->config->verbose) { + std::cout.write(reinterpret_cast<const char *>(data), nread); + } + + if (htperr == HPE_PAUSED) { + // pause is done only when connection: close is requested + return -1; + } + + if (htperr != HPE_OK) { + std::cerr << "[ERROR] HTTP parse error: " + << "(" << llhttp_errno_name(htperr) << ") " + << llhttp_get_error_reason(&htp_) << std::endl; + return -1; + } + + return 0; +} + +int Http1Session::on_write() { + if (complete_) { + return -1; + } + + auto config = client_->worker->config; + auto req_stat = client_->get_req_stat(stream_req_counter_); + if (!req_stat) { + return 0; + } + + if (req_stat->data_offset < config->data_length) { + auto req_stat = client_->get_req_stat(stream_req_counter_); + auto &wb = client_->wb; + + // TODO unfortunately, wb has no interface to use with read(2) + // family functions. + std::array<uint8_t, 16_k> buf; + + ssize_t nread; + while ((nread = pread(config->data_fd, buf.data(), buf.size(), + req_stat->data_offset)) == -1 && + errno == EINTR) + ; + + if (nread == -1) { + return -1; + } + + req_stat->data_offset += nread; + + wb.append(buf.data(), nread); + + if (client_->worker->config->verbose) { + std::cout << "[send " << nread << " byte(s)]" << std::endl; + } + + if (req_stat->data_offset == config->data_length) { + // increment for next request + stream_req_counter_ += 2; + + if (stream_resp_counter_ == stream_req_counter_) { + // Response has already been received + client_->on_stream_close(stream_resp_counter_ - 2, true, + client_->final); + } + } + } + + return 0; +} + +void Http1Session::terminate() { complete_ = true; } + +Client *Http1Session::get_client() { return client_; } + +size_t Http1Session::max_concurrent_streams() { + auto config = client_->worker->config; + + return config->data_fd == -1 ? config->max_concurrent_streams : 1; +} + +} // namespace h2load diff --git a/src/h2load_http1_session.h b/src/h2load_http1_session.h new file mode 100644 index 0000000..cc10f50 --- /dev/null +++ b/src/h2load_http1_session.h @@ -0,0 +1,60 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 British Broadcasting Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_HTTP1_SESSION_H +#define H2LOAD_HTTP1_SESSION_H + +#include "h2load_session.h" + +#include <nghttp2/nghttp2.h> + +#include "llhttp.h" + +namespace h2load { + +struct Client; + +class Http1Session : public Session { +public: + Http1Session(Client *client); + virtual ~Http1Session(); + virtual void on_connect(); + virtual int submit_request(); + virtual int on_read(const uint8_t *data, size_t len); + virtual int on_write(); + virtual void terminate(); + virtual size_t max_concurrent_streams(); + Client *get_client(); + int32_t stream_req_counter_; + int32_t stream_resp_counter_; + +private: + Client *client_; + llhttp_t htp_; + bool complete_; +}; + +} // namespace h2load + +#endif // H2LOAD_HTTP1_SESSION_H diff --git a/src/h2load_http2_session.cc b/src/h2load_http2_session.cc new file mode 100644 index 0000000..b058a33 --- /dev/null +++ b/src/h2load_http2_session.cc @@ -0,0 +1,314 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load_http2_session.h" + +#include <cassert> +#include <cerrno> +#include <iostream> + +#include "h2load.h" +#include "util.h" +#include "template.h" + +using namespace nghttp2; + +namespace h2load { + +Http2Session::Http2Session(Client *client) + : client_(client), session_(nullptr) {} + +Http2Session::~Http2Session() { nghttp2_session_del(session_); } + +namespace { +int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, + const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, uint8_t flags, + void *user_data) { + auto client = static_cast<Client *>(user_data); + if (frame->hd.type != NGHTTP2_HEADERS) { + return 0; + } + client->on_header(frame->hd.stream_id, name, namelen, value, valuelen); + client->worker->stats.bytes_head_decomp += namelen + valuelen; + + if (client->worker->config->verbose) { + std::cout << "[stream_id=" << frame->hd.stream_id << "] "; + std::cout.write(reinterpret_cast<const char *>(name), namelen); + std::cout << ": "; + std::cout.write(reinterpret_cast<const char *>(value), valuelen); + std::cout << "\n"; + } + + return 0; +} +} // namespace + +namespace { +int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + auto client = static_cast<Client *>(user_data); + switch (frame->hd.type) { + case NGHTTP2_HEADERS: + client->worker->stats.bytes_head += + frame->hd.length - frame->headers.padlen - + ((frame->hd.flags & NGHTTP2_FLAG_PRIORITY) ? 5 : 0); + // fall through + case NGHTTP2_DATA: + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + client->record_ttfb(); + } + break; + } + return 0; +} +} // namespace + +namespace { +int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data) { + auto client = static_cast<Client *>(user_data); + client->record_ttfb(); + client->worker->stats.bytes_body += len; + return 0; +} +} // namespace + +namespace { +int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *user_data) { + auto client = static_cast<Client *>(user_data); + client->on_stream_close(stream_id, error_code == NGHTTP2_NO_ERROR); + + return 0; +} +} // namespace + +namespace { +int before_frame_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + + auto client = static_cast<Client *>(user_data); + auto req_stat = client->get_req_stat(frame->hd.stream_id); + assert(req_stat); + client->record_request_time(req_stat); + + return 0; +} +} // namespace + +namespace { +ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, + uint8_t *buf, size_t length, uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) { + auto client = static_cast<Client *>(user_data); + auto config = client->worker->config; + auto req_stat = client->get_req_stat(stream_id); + assert(req_stat); + ssize_t nread; + while ((nread = pread(config->data_fd, buf, length, req_stat->data_offset)) == + -1 && + errno == EINTR) + ; + + if (nread == -1) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + req_stat->data_offset += nread; + + if (req_stat->data_offset == config->data_length) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + return nread; + } + + if (req_stat->data_offset > config->data_length || nread == 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + return nread; +} + +} // namespace + +namespace { +ssize_t send_callback(nghttp2_session *session, const uint8_t *data, + size_t length, int flags, void *user_data) { + auto client = static_cast<Client *>(user_data); + auto &wb = client->wb; + + if (wb.rleft() >= BACKOFF_WRITE_BUFFER_THRES) { + return NGHTTP2_ERR_WOULDBLOCK; + } + + return wb.append(data, length); +} +} // namespace + +void Http2Session::on_connect() { + int rv; + + // This is required with --disable-assert. + (void)rv; + + nghttp2_session_callbacks *callbacks; + + nghttp2_session_callbacks_new(&callbacks); + + auto callbacks_deleter = defer(nghttp2_session_callbacks_del, callbacks); + + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback); + + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); + + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_header_callback(callbacks, + on_header_callback); + + nghttp2_session_callbacks_set_before_frame_send_callback( + callbacks, before_frame_send_callback); + + nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); + + nghttp2_option *opt; + + rv = nghttp2_option_new(&opt); + assert(rv == 0); + + auto config = client_->worker->config; + + if (config->encoder_header_table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { + nghttp2_option_set_max_deflate_dynamic_table_size( + opt, config->encoder_header_table_size); + } + + nghttp2_session_client_new2(&session_, callbacks, client_, opt); + + nghttp2_option_del(opt); + + std::array<nghttp2_settings_entry, 4> iv; + size_t niv = 2; + iv[0].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; + iv[0].value = 0; + iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + iv[1].value = (1 << config->window_bits) - 1; + + if (config->header_table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { + iv[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; + iv[niv].value = config->header_table_size; + ++niv; + } + if (config->max_frame_size != 16_k) { + iv[niv].settings_id = NGHTTP2_SETTINGS_MAX_FRAME_SIZE; + iv[niv].value = config->max_frame_size; + ++niv; + } + + rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv.data(), niv); + + assert(rv == 0); + + auto connection_window = (1 << config->connection_window_bits) - 1; + nghttp2_session_set_local_window_size(session_, NGHTTP2_FLAG_NONE, 0, + connection_window); + + client_->signal_write(); +} + +int Http2Session::submit_request() { + if (nghttp2_session_check_request_allowed(session_) == 0) { + return -1; + } + + auto config = client_->worker->config; + auto &nva = config->nva[client_->reqidx++]; + + if (client_->reqidx == config->nva.size()) { + client_->reqidx = 0; + } + + nghttp2_data_provider prd{{0}, file_read_callback}; + + auto stream_id = + nghttp2_submit_request(session_, nullptr, nva.data(), nva.size(), + config->data_fd == -1 ? nullptr : &prd, nullptr); + if (stream_id < 0) { + return -1; + } + + client_->on_request(stream_id); + + return 0; +} + +int Http2Session::on_read(const uint8_t *data, size_t len) { + auto rv = nghttp2_session_mem_recv(session_, data, len); + if (rv < 0) { + return -1; + } + + assert(static_cast<size_t>(rv) == len); + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && client_->wb.rleft() == 0) { + return -1; + } + + client_->signal_write(); + + return 0; +} + +int Http2Session::on_write() { + auto rv = nghttp2_session_send(session_); + if (rv != 0) { + return -1; + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && client_->wb.rleft() == 0) { + return -1; + } + + return 0; +} + +void Http2Session::terminate() { + nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); +} + +size_t Http2Session::max_concurrent_streams() { + return (size_t)client_->worker->config->max_concurrent_streams; +} + +} // namespace h2load diff --git a/src/h2load_http2_session.h b/src/h2load_http2_session.h new file mode 100644 index 0000000..1b9b9f7 --- /dev/null +++ b/src/h2load_http2_session.h @@ -0,0 +1,54 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_HTTP2_SESSION_H +#define H2LOAD_HTTP2_SESSION_H + +#include "h2load_session.h" + +#include <nghttp2/nghttp2.h> + +namespace h2load { + +struct Client; + +class Http2Session : public Session { +public: + Http2Session(Client *client); + virtual ~Http2Session(); + virtual void on_connect(); + virtual int submit_request(); + virtual int on_read(const uint8_t *data, size_t len); + virtual int on_write(); + virtual void terminate(); + virtual size_t max_concurrent_streams(); + +private: + Client *client_; + nghttp2_session *session_; +}; + +} // namespace h2load + +#endif // H2LOAD_HTTP2_SESSION_H diff --git a/src/h2load_http3_session.cc b/src/h2load_http3_session.cc new file mode 100644 index 0000000..d491cba --- /dev/null +++ b/src/h2load_http3_session.cc @@ -0,0 +1,474 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load_http3_session.h" + +#include <iostream> + +#include <ngtcp2/ngtcp2.h> + +#include "h2load.h" + +namespace h2load { + +Http3Session::Http3Session(Client *client) + : client_(client), conn_(nullptr), npending_request_(0), reqidx_(0) {} + +Http3Session::~Http3Session() { nghttp3_conn_del(conn_); } + +void Http3Session::on_connect() {} + +int Http3Session::submit_request() { + if (npending_request_) { + ++npending_request_; + return 0; + } + + auto config = client_->worker->config; + reqidx_ = client_->reqidx; + + if (++client_->reqidx == config->nva.size()) { + client_->reqidx = 0; + } + + auto stream_id = submit_request_internal(); + if (stream_id < 0) { + if (stream_id == NGTCP2_ERR_STREAM_ID_BLOCKED) { + ++npending_request_; + return 0; + } + return -1; + } + + return 0; +} + +namespace { +nghttp3_ssize read_data(nghttp3_conn *conn, int64_t stream_id, nghttp3_vec *vec, + size_t veccnt, uint32_t *pflags, void *user_data, + void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + + s->read_data(vec, veccnt, pflags); + + return 1; +} +} // namespace + +void Http3Session::read_data(nghttp3_vec *vec, size_t veccnt, + uint32_t *pflags) { + assert(veccnt > 0); + + auto config = client_->worker->config; + + vec[0].base = config->data; + vec[0].len = config->data_length; + *pflags |= NGHTTP3_DATA_FLAG_EOF; +} + +int64_t Http3Session::submit_request_internal() { + int rv; + int64_t stream_id; + + auto config = client_->worker->config; + auto &nva = config->nva[reqidx_]; + + rv = ngtcp2_conn_open_bidi_stream(client_->quic.conn, &stream_id, nullptr); + if (rv != 0) { + return rv; + } + + nghttp3_data_reader dr{}; + dr.read_data = h2load::read_data; + + rv = nghttp3_conn_submit_request( + conn_, stream_id, reinterpret_cast<nghttp3_nv *>(nva.data()), nva.size(), + config->data_fd == -1 ? nullptr : &dr, nullptr); + if (rv != 0) { + return rv; + } + + client_->on_request(stream_id); + auto req_stat = client_->get_req_stat(stream_id); + assert(req_stat); + client_->record_request_time(req_stat); + + return stream_id; +} + +int Http3Session::on_read(const uint8_t *data, size_t len) { return -1; } + +int Http3Session::on_write() { return -1; } + +void Http3Session::terminate() {} + +size_t Http3Session::max_concurrent_streams() { + return (size_t)client_->worker->config->max_concurrent_streams; +} + +namespace { +int stream_close(nghttp3_conn *conn, int64_t stream_id, uint64_t app_error_code, + void *user_data, void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + if (s->stream_close(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Http3Session::stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + assert(!ngtcp2_conn_is_local_stream(client_->quic.conn, stream_id)); + ngtcp2_conn_extend_max_streams_uni(client_->quic.conn, 1); + } + client_->on_stream_close(stream_id, app_error_code == NGHTTP3_H3_NO_ERROR); + return 0; +} + +namespace { +int end_stream(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + if (s->end_stream(stream_id) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Http3Session::end_stream(int64_t stream_id) { + client_->record_ttfb(); + + return 0; +} + +namespace { +int recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + s->recv_data(stream_id, data, datalen); + return 0; +} +} // namespace + +void Http3Session::recv_data(int64_t stream_id, const uint8_t *data, + size_t datalen) { + client_->record_ttfb(); + client_->worker->stats.bytes_body += datalen; + consume(stream_id, datalen); +} + +namespace { +int deferred_consume(nghttp3_conn *conn, int64_t stream_id, size_t nconsumed, + void *user_data, void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + s->consume(stream_id, nconsumed); + return 0; +} +} // namespace + +void Http3Session::consume(int64_t stream_id, size_t nconsumed) { + ngtcp2_conn_extend_max_stream_offset(client_->quic.conn, stream_id, + nconsumed); + ngtcp2_conn_extend_max_offset(client_->quic.conn, nconsumed); +} + +namespace { +int begin_headers(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + s->begin_headers(stream_id); + return 0; +} +} // namespace + +void Http3Session::begin_headers(int64_t stream_id) { + auto payloadlen = nghttp3_conn_get_frame_payload_left(conn_, stream_id); + assert(payloadlen > 0); + + client_->worker->stats.bytes_head += payloadlen; +} + +namespace { +int recv_header(nghttp3_conn *conn, int64_t stream_id, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + auto k = nghttp3_rcbuf_get_buf(name); + auto v = nghttp3_rcbuf_get_buf(value); + s->recv_header(stream_id, &k, &v); + return 0; +} +} // namespace + +void Http3Session::recv_header(int64_t stream_id, const nghttp3_vec *name, + const nghttp3_vec *value) { + client_->on_header(stream_id, name->base, name->len, value->base, value->len); + client_->worker->stats.bytes_head_decomp += name->len + value->len; +} + +namespace { +int stop_sending(nghttp3_conn *conn, int64_t stream_id, uint64_t app_error_code, + void *user_data, void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + if (s->stop_sending(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Http3Session::stop_sending(int64_t stream_id, uint64_t app_error_code) { + auto rv = ngtcp2_conn_shutdown_stream_read(client_->quic.conn, 0, stream_id, + app_error_code); + if (rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream_read: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +namespace { +int reset_stream(nghttp3_conn *conn, int64_t stream_id, uint64_t app_error_code, + void *user_data, void *stream_user_data) { + auto s = static_cast<Http3Session *>(user_data); + if (s->reset_stream(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Http3Session::reset_stream(int64_t stream_id, uint64_t app_error_code) { + auto rv = ngtcp2_conn_shutdown_stream_write(client_->quic.conn, 0, stream_id, + app_error_code); + if (rv != 0) { + std::cerr << "ngtcp2_conn_shutdown_stream_write: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + return 0; +} + +int Http3Session::close_stream(int64_t stream_id, uint64_t app_error_code) { + auto rv = nghttp3_conn_close_stream(conn_, stream_id, app_error_code); + switch (rv) { + case 0: + return 0; + case NGHTTP3_ERR_STREAM_NOT_FOUND: + if (!ngtcp2_is_bidi_stream(stream_id)) { + assert(!ngtcp2_conn_is_local_stream(client_->quic.conn, stream_id)); + ngtcp2_conn_extend_max_streams_uni(client_->quic.conn, 1); + } + return 0; + default: + return -1; + } +} + +int Http3Session::shutdown_stream_read(int64_t stream_id) { + auto rv = nghttp3_conn_shutdown_stream_read(conn_, stream_id); + if (rv != 0) { + return -1; + } + return 0; +} + +int Http3Session::extend_max_local_streams() { + auto config = client_->worker->config; + + for (; npending_request_; --npending_request_) { + auto stream_id = submit_request_internal(); + if (stream_id < 0) { + if (stream_id == NGTCP2_ERR_STREAM_ID_BLOCKED) { + return 0; + } + return -1; + } + + if (++reqidx_ == config->nva.size()) { + reqidx_ = 0; + } + } + + return 0; +} + +int Http3Session::init_conn() { + int rv; + + assert(conn_ == nullptr); + + if (ngtcp2_conn_get_streams_uni_left(client_->quic.conn) < 3) { + return -1; + } + + nghttp3_callbacks callbacks{ + nullptr, // acked_stream_data + h2load::stream_close, + h2load::recv_data, + h2load::deferred_consume, + h2load::begin_headers, + h2load::recv_header, + nullptr, // end_headers + nullptr, // begin_trailers + h2load::recv_header, + nullptr, // end_trailers + h2load::stop_sending, + h2load::end_stream, + h2load::reset_stream, + nullptr, // shutdown + }; + + auto config = client_->worker->config; + + nghttp3_settings settings; + nghttp3_settings_default(&settings); + settings.qpack_max_dtable_capacity = config->header_table_size; + settings.qpack_blocked_streams = 100; + + auto mem = nghttp3_mem_default(); + + rv = nghttp3_conn_client_new(&conn_, &callbacks, &settings, mem, this); + if (rv != 0) { + std::cerr << "nghttp3_conn_client_new: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + int64_t ctrl_stream_id; + + rv = + ngtcp2_conn_open_uni_stream(client_->quic.conn, &ctrl_stream_id, nullptr); + if (rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + rv = nghttp3_conn_bind_control_stream(conn_, ctrl_stream_id); + if (rv != 0) { + std::cerr << "nghttp3_conn_bind_control_stream: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + int64_t qpack_enc_stream_id, qpack_dec_stream_id; + + rv = ngtcp2_conn_open_uni_stream(client_->quic.conn, &qpack_enc_stream_id, + nullptr); + if (rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + rv = ngtcp2_conn_open_uni_stream(client_->quic.conn, &qpack_dec_stream_id, + nullptr); + if (rv != 0) { + std::cerr << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + rv = nghttp3_conn_bind_qpack_streams(conn_, qpack_enc_stream_id, + qpack_dec_stream_id); + if (rv != 0) { + std::cerr << "nghttp3_conn_bind_qpack_streams: " << nghttp3_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +ssize_t Http3Session::read_stream(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen) { + auto nconsumed = nghttp3_conn_read_stream( + conn_, stream_id, data, datalen, flags & NGTCP2_STREAM_DATA_FLAG_FIN); + if (nconsumed < 0) { + std::cerr << "nghttp3_conn_read_stream: " << nghttp3_strerror(nconsumed) + << std::endl; + ngtcp2_ccerr_set_application_error( + &client_->quic.last_error, + nghttp3_err_infer_quic_app_error_code(nconsumed), nullptr, 0); + return -1; + } + return nconsumed; +} + +ssize_t Http3Session::write_stream(int64_t &stream_id, int &fin, + nghttp3_vec *vec, size_t veccnt) { + auto sveccnt = + nghttp3_conn_writev_stream(conn_, &stream_id, &fin, vec, veccnt); + if (sveccnt < 0) { + ngtcp2_ccerr_set_application_error( + &client_->quic.last_error, + nghttp3_err_infer_quic_app_error_code(sveccnt), nullptr, 0); + return -1; + } + return sveccnt; +} + +void Http3Session::block_stream(int64_t stream_id) { + nghttp3_conn_block_stream(conn_, stream_id); +} + +int Http3Session::unblock_stream(int64_t stream_id) { + if (nghttp3_conn_unblock_stream(conn_, stream_id) != 0) { + return -1; + } + + return 0; +} + +void Http3Session::shutdown_stream_write(int64_t stream_id) { + nghttp3_conn_shutdown_stream_write(conn_, stream_id); +} + +int Http3Session::add_write_offset(int64_t stream_id, size_t ndatalen) { + auto rv = nghttp3_conn_add_write_offset(conn_, stream_id, ndatalen); + if (rv != 0) { + ngtcp2_ccerr_set_application_error( + &client_->quic.last_error, nghttp3_err_infer_quic_app_error_code(rv), + nullptr, 0); + return -1; + } + return 0; +} + +int Http3Session::add_ack_offset(int64_t stream_id, size_t datalen) { + auto rv = nghttp3_conn_add_ack_offset(conn_, stream_id, datalen); + if (rv != 0) { + ngtcp2_ccerr_set_application_error( + &client_->quic.last_error, nghttp3_err_infer_quic_app_error_code(rv), + nullptr, 0); + return -1; + } + return 0; +} + +} // namespace h2load diff --git a/src/h2load_http3_session.h b/src/h2load_http3_session.h new file mode 100644 index 0000000..8610417 --- /dev/null +++ b/src/h2load_http3_session.h @@ -0,0 +1,84 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_HTTP3_SESSION_H +#define H2LOAD_HTTP3_SESSION_H + +#include "h2load_session.h" + +#include <nghttp3/nghttp3.h> + +namespace h2load { + +struct Client; + +class Http3Session : public Session { +public: + Http3Session(Client *client); + virtual ~Http3Session(); + virtual void on_connect(); + virtual int submit_request(); + virtual int on_read(const uint8_t *data, size_t len); + virtual int on_write(); + virtual void terminate(); + virtual size_t max_concurrent_streams(); + + int init_conn(); + int stream_close(int64_t stream_id, uint64_t app_error_code); + int end_stream(int64_t stream_id); + void recv_data(int64_t stream_id, const uint8_t *data, size_t datalen); + void consume(int64_t stream_id, size_t nconsumed); + void begin_headers(int64_t stream_id); + void recv_header(int64_t stream_id, const nghttp3_vec *name, + const nghttp3_vec *value); + int stop_sending(int64_t stream_id, uint64_t app_error_code); + int reset_stream(int64_t stream_id, uint64_t app_error_code); + + int close_stream(int64_t stream_id, uint64_t app_error_code); + int shutdown_stream_read(int64_t stream_id); + int extend_max_local_streams(); + int64_t submit_request_internal(); + + ssize_t read_stream(uint32_t flags, int64_t stream_id, const uint8_t *data, + size_t datalen); + ssize_t write_stream(int64_t &stream_id, int &fin, nghttp3_vec *vec, + size_t veccnt); + void block_stream(int64_t stream_id); + int unblock_stream(int64_t stream_id); + void shutdown_stream_write(int64_t stream_id); + int add_write_offset(int64_t stream_id, size_t ndatalen); + int add_ack_offset(int64_t stream_id, size_t datalen); + + void read_data(nghttp3_vec *vec, size_t veccnt, uint32_t *pflags); + +private: + Client *client_; + nghttp3_conn *conn_; + size_t npending_request_; + size_t reqidx_; +}; + +} // namespace h2load + +#endif // H2LOAD_HTTP3_SESSION_H diff --git a/src/h2load_quic.cc b/src/h2load_quic.cc new file mode 100644 index 0000000..e492a3e --- /dev/null +++ b/src/h2load_quic.cc @@ -0,0 +1,844 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load_quic.h" + +#include <netinet/udp.h> + +#include <iostream> + +#ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# include <ngtcp2/ngtcp2_crypto_quictls.h> +#endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS +#ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +# include <ngtcp2/ngtcp2_crypto_boringssl.h> +#endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL + +#include <openssl/err.h> +#include <openssl/rand.h> + +#include "h2load_http3_session.h" + +namespace h2load { + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->quic_handshake_completed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::quic_handshake_completed() { return connection_made(); } + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->quic_recv_stream_data(flags, stream_id, data, datalen) != 0) { + // TODO Better to do this gracefully rather than + // NGTCP2_ERR_CALLBACK_FAILURE. Perhaps, call + // ngtcp2_conn_write_application_close() ? + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::quic_recv_stream_data(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen) { + if (worker->current_phase == Phase::MAIN_DURATION) { + worker->stats.bytes_total += datalen; + } + + auto s = static_cast<Http3Session *>(session.get()); + auto nconsumed = s->read_stream(flags, stream_id, data, datalen); + if (nconsumed == -1) { + return -1; + } + + ngtcp2_conn_extend_max_stream_offset(quic.conn, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(quic.conn, nconsumed); + + return 0; +} + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->quic_acked_stream_data_offset(stream_id, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::quic_acked_stream_data_offset(int64_t stream_id, size_t datalen) { + auto s = static_cast<Http3Session *>(session.get()); + if (s->add_ack_offset(stream_id, datalen) != 0) { + return -1; + } + return 0; +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + + if (!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + + if (c->quic_stream_close(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::quic_stream_close(int64_t stream_id, uint64_t app_error_code) { + auto s = static_cast<Http3Session *>(session.get()); + if (s->close_stream(stream_id, app_error_code) != 0) { + return -1; + } + return 0; +} + +namespace { +int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->quic_stream_reset(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::quic_stream_reset(int64_t stream_id, uint64_t app_error_code) { + auto s = static_cast<Http3Session *>(session.get()); + if (s->shutdown_stream_read(stream_id) != 0) { + return -1; + } + return 0; +} + +namespace { +int stream_stop_sending(ngtcp2_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + if (c->quic_stream_stop_sending(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} +} // namespace + +int Client::quic_stream_stop_sending(int64_t stream_id, + uint64_t app_error_code) { + auto s = static_cast<Http3Session *>(session.get()); + if (s->shutdown_stream_read(stream_id) != 0) { + return -1; + } + return 0; +} + +namespace { +int extend_max_local_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->quic_extend_max_local_streams() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::quic_extend_max_local_streams() { + auto s = static_cast<Http3Session *>(session.get()); + if (s->extend_max_local_streams() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto c = static_cast<Client *>(user_data); + + if (c->quic_extend_max_stream_data(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::quic_extend_max_stream_data(int64_t stream_id) { + auto s = static_cast<Http3Session *>(session.get()); + if (s->unblock_stream(stream_id) != 0) { + return -1; + } + return 0; +} + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, + size_t cidlen, void *user_data) { + if (RAND_bytes(cid->data, cidlen) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + cid->datalen = cidlen; + + if (RAND_bytes(token, NGTCP2_STATELESS_RESET_TOKENLEN) != 1) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +void debug_log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + fprintf(stderr, "\n"); +} +} // namespace + +namespace { +int generate_cid(ngtcp2_cid &dest) { + dest.datalen = 8; + + if (RAND_bytes(dest.data, dest.datalen) != 1) { + return -1; + } + + return 0; +} +} // namespace + +namespace { +ngtcp2_tstamp quic_timestamp() { + return std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} +} // namespace + +// qlog write callback -- excerpted from ngtcp2/examples/client_base.cc +namespace { +void qlog_write_cb(void *user_data, uint32_t flags, const void *data, + size_t datalen) { + auto c = static_cast<Client *>(user_data); + c->quic_write_qlog(data, datalen); +} +} // namespace + +void Client::quic_write_qlog(const void *data, size_t datalen) { + assert(quic.qlog_file != nullptr); + fwrite(data, 1, datalen, quic.qlog_file); +} + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + util::random_bytes(dest, dest + destlen, + *static_cast<std::mt19937 *>(rand_ctx->native_handle)); +} +} // namespace + +namespace { +int recv_rx_key(ngtcp2_conn *conn, ngtcp2_encryption_level level, + void *user_data) { + if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) { + return 0; + } + + auto c = static_cast<Client *>(user_data); + + if (c->quic_make_http3_session() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Client::quic_make_http3_session() { + auto s = std::make_unique<Http3Session>(this); + if (s->init_conn() == -1) { + return -1; + } + session = std::move(s); + + return 0; +} + +namespace { +ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + auto c = static_cast<Client *>(conn_ref->user_data); + return c->quic.conn; +} +} // namespace + +int Client::quic_init(const sockaddr *local_addr, socklen_t local_addrlen, + const sockaddr *remote_addr, socklen_t remote_addrlen) { + int rv; + + if (!ssl) { + ssl = SSL_new(worker->ssl_ctx); + + quic.conn_ref.get_conn = get_conn; + quic.conn_ref.user_data = this; + + SSL_set_app_data(ssl, &quic.conn_ref); + SSL_set_connect_state(ssl); + SSL_set_quic_use_legacy_codepoint(ssl, 0); + } + + auto callbacks = ngtcp2_callbacks{ + ngtcp2_crypto_client_initial_cb, + nullptr, // recv_client_initial + ngtcp2_crypto_recv_crypto_data_cb, + h2load::handshake_completed, + nullptr, // recv_version_negotiation + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + h2load::recv_stream_data, + h2load::acked_stream_data_offset, + nullptr, // stream_open + h2load::stream_close, + nullptr, // recv_stateless_reset + ngtcp2_crypto_recv_retry_cb, + h2load::extend_max_local_streams_bidi, + nullptr, // extend_max_local_streams_uni + h2load::rand, + get_new_connection_id, + nullptr, // remove_connection_id + ngtcp2_crypto_update_key_cb, + nullptr, // path_validation + nullptr, // select_preferred_addr + h2load::stream_reset, + nullptr, // extend_max_remote_streams_bidi + nullptr, // extend_max_remote_streams_uni + h2load::extend_max_stream_data, + nullptr, // dcid_status + nullptr, // handshake_confirmed + nullptr, // recv_new_token + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + nullptr, // recv_datagram + nullptr, // ack_datagram + nullptr, // lost_datagram + ngtcp2_crypto_get_path_challenge_data_cb, + h2load::stream_stop_sending, + nullptr, // version_negotiation + h2load::recv_rx_key, + nullptr, // recv_tx_key + }; + + ngtcp2_cid scid, dcid; + if (generate_cid(scid) != 0) { + return -1; + } + if (generate_cid(dcid) != 0) { + return -1; + } + + auto config = worker->config; + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + if (config->verbose) { + settings.log_printf = debug_log_printf; + } + settings.initial_ts = quic_timestamp(); + settings.rand_ctx.native_handle = &worker->randgen; + if (!config->qlog_file_base.empty()) { + assert(quic.qlog_file == nullptr); + auto path = config->qlog_file_base; + path += '.'; + path += util::utos(worker->id); + path += '.'; + path += util::utos(id); + path += ".sqlog"; + quic.qlog_file = fopen(path.c_str(), "w"); + if (quic.qlog_file == nullptr) { + std::cerr << "Failed to open a qlog file: " << path << std::endl; + return -1; + } + settings.qlog_write = qlog_write_cb; + } + if (config->max_udp_payload_size) { + settings.max_tx_udp_payload_size = config->max_udp_payload_size; + settings.no_tx_udp_payload_size_shaping = 1; + } + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + auto max_stream_data = + std::min((1 << 26) - 1, (1 << config->window_bits) - 1); + params.initial_max_stream_data_bidi_local = max_stream_data; + params.initial_max_stream_data_uni = max_stream_data; + params.initial_max_data = (1 << config->connection_window_bits) - 1; + params.initial_max_streams_bidi = 0; + params.initial_max_streams_uni = 100; + params.max_idle_timeout = 30 * NGTCP2_SECONDS; + + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(local_addr), + local_addrlen, + }, + { + const_cast<sockaddr *>(remote_addr), + remote_addrlen, + }, + }; + + assert(config->alpn_list.size()); + + uint32_t quic_version; + + if (config->alpn_list[0] == NGHTTP3_ALPN_H3) { + quic_version = NGTCP2_PROTO_VER_V1; + } else { + quic_version = NGTCP2_PROTO_VER_MIN; + } + + rv = ngtcp2_conn_client_new(&quic.conn, &dcid, &scid, &path, quic_version, + &callbacks, &settings, ¶ms, nullptr, this); + if (rv != 0) { + return -1; + } + + ngtcp2_conn_set_tls_native_handle(quic.conn, ssl); + + return 0; +} + +void Client::quic_free() { + ngtcp2_conn_del(quic.conn); + if (quic.qlog_file != nullptr) { + fclose(quic.qlog_file); + quic.qlog_file = nullptr; + } +} + +void Client::quic_close_connection() { + if (!quic.conn) { + return; + } + + std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf; + ngtcp2_path_storage ps; + ngtcp2_path_storage_zero(&ps); + + auto nwrite = ngtcp2_conn_write_connection_close( + quic.conn, &ps.path, nullptr, buf.data(), buf.size(), &quic.last_error, + quic_timestamp()); + + if (nwrite <= 0) { + return; + } + + write_udp(reinterpret_cast<sockaddr *>(ps.path.remote.addr), + ps.path.remote.addrlen, buf.data(), nwrite, 0); +} + +int Client::quic_write_client_handshake(ngtcp2_encryption_level level, + const uint8_t *data, size_t datalen) { + int rv; + + assert(level < 2); + + rv = ngtcp2_conn_submit_crypto_data(quic.conn, level, data, datalen); + if (rv != 0) { + std::cerr << "ngtcp2_conn_submit_crypto_data: " << ngtcp2_strerror(rv) + << std::endl; + return -1; + } + + return 0; +} + +void quic_pkt_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto c = static_cast<Client *>(w->data); + + if (c->quic_pkt_timeout() != 0) { + c->fail(); + c->worker->free_client(c); + delete c; + return; + } +} + +int Client::quic_pkt_timeout() { + int rv; + auto now = quic_timestamp(); + + rv = ngtcp2_conn_handle_expiry(quic.conn, now); + if (rv != 0) { + ngtcp2_ccerr_set_liberr(&quic.last_error, rv, nullptr, 0); + return -1; + } + + return write_quic(); +} + +void Client::quic_restart_pkt_timer() { + auto expiry = ngtcp2_conn_get_expiry(quic.conn); + auto now = quic_timestamp(); + auto t = expiry > now ? static_cast<ev_tstamp>(expiry - now) / NGTCP2_SECONDS + : 1e-9; + quic.pkt_timer.repeat = t; + ev_timer_again(worker->loop, &quic.pkt_timer); +} + +int Client::read_quic() { + std::array<uint8_t, 65535> buf; + sockaddr_union su; + int rv; + size_t pktcnt = 0; + ngtcp2_pkt_info pi{}; + + iovec msg_iov; + msg_iov.iov_base = buf.data(); + msg_iov.iov_len = buf.size(); + + msghdr msg{}; + msg.msg_name = &su; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(uint16_t))]; + msg.msg_control = msg_ctrl; + + auto ts = quic_timestamp(); + + for (;;) { + msg.msg_namelen = sizeof(su); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(fd, &msg, 0); + if (nread == -1) { + return 0; + } + + auto gso_size = util::msghdr_get_udp_gro(&msg); + if (gso_size == 0) { + gso_size = static_cast<size_t>(nread); + } + + assert(quic.conn); + + ++worker->stats.udp_dgram_recv; + + auto path = ngtcp2_path{ + { + &local_addr.su.sa, + static_cast<socklen_t>(local_addr.len), + }, + { + &su.sa, + msg.msg_namelen, + }, + }; + + auto data = buf.data(); + + for (;;) { + auto datalen = std::min(static_cast<size_t>(nread), gso_size); + + ++pktcnt; + + rv = ngtcp2_conn_read_pkt(quic.conn, &path, &pi, data, datalen, ts); + if (rv != 0) { + if (!quic.last_error.error_code) { + if (rv == NGTCP2_ERR_CRYPTO) { + ngtcp2_ccerr_set_tls_alert(&quic.last_error, + ngtcp2_conn_get_tls_alert(quic.conn), + nullptr, 0); + } else { + ngtcp2_ccerr_set_liberr(&quic.last_error, rv, nullptr, 0); + } + } + + return -1; + } + + nread -= datalen; + if (nread == 0) { + break; + } + + data += datalen; + } + + if (pktcnt >= 100) { + break; + } + } + + return 0; +} + +int Client::write_quic() { + int rv; + + ev_io_stop(worker->loop, &wev); + + if (quic.close_requested) { + return -1; + } + + if (quic.tx.send_blocked) { + rv = send_blocked_packet(); + if (rv != 0) { + return -1; + } + + if (quic.tx.send_blocked) { + return 0; + } + } + + std::array<nghttp3_vec, 16> vec; + size_t pktcnt = 0; + auto max_udp_payload_size = + ngtcp2_conn_get_max_tx_udp_payload_size(quic.conn); +#ifdef UDP_SEGMENT + auto path_max_udp_payload_size = + ngtcp2_conn_get_path_max_tx_udp_payload_size(quic.conn); +#endif // UDP_SEGMENT + auto max_pktcnt = + ngtcp2_conn_get_send_quantum(quic.conn) / max_udp_payload_size; + uint8_t *bufpos = quic.tx.data.get(); + ngtcp2_path_storage ps; + size_t gso_size = 0; + + ngtcp2_path_storage_zero(&ps); + + auto s = static_cast<Http3Session *>(session.get()); + auto ts = quic_timestamp(); + + for (;;) { + int64_t stream_id = -1; + int fin = 0; + ssize_t sveccnt = 0; + + if (session && ngtcp2_conn_get_max_data_left(quic.conn)) { + sveccnt = s->write_stream(stream_id, fin, vec.data(), vec.size()); + if (sveccnt == -1) { + return -1; + } + } + + ngtcp2_ssize ndatalen; + auto v = vec.data(); + auto vcnt = static_cast<size_t>(sveccnt); + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + auto nwrite = ngtcp2_conn_writev_stream( + quic.conn, &ps.path, nullptr, bufpos, max_udp_payload_size, &ndatalen, + flags, stream_id, reinterpret_cast<const ngtcp2_vec *>(v), vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + assert(ndatalen == -1); + s->block_stream(stream_id); + continue; + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + s->shutdown_stream_write(stream_id); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + if (s->add_write_offset(stream_id, ndatalen) != 0) { + return -1; + } + continue; + } + + ngtcp2_ccerr_set_liberr(&quic.last_error, nwrite, nullptr, 0); + return -1; + } else if (ndatalen >= 0 && s->add_write_offset(stream_id, ndatalen) != 0) { + return -1; + } + + quic_restart_pkt_timer(); + + if (nwrite == 0) { + if (bufpos - quic.tx.data.get()) { + auto data = quic.tx.data.get(); + auto datalen = bufpos - quic.tx.data.get(); + rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, + datalen, gso_size); + if (rv == 1) { + on_send_blocked(ps.path.remote, data, datalen, gso_size); + signal_write(); + return 0; + } + } + return 0; + } + + bufpos += nwrite; + +#ifdef UDP_SEGMENT + if (worker->config->no_udp_gso) { +#endif // UDP_SEGMENT + auto data = quic.tx.data.get(); + auto datalen = bufpos - quic.tx.data.get(); + rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, datalen, + 0); + if (rv == 1) { + on_send_blocked(ps.path.remote, data, datalen, 0); + signal_write(); + return 0; + } + + if (++pktcnt == max_pktcnt) { + signal_write(); + return 0; + } + + bufpos = quic.tx.data.get(); + +#ifdef UDP_SEGMENT + continue; + } +#endif // UDP_SEGMENT + +#ifdef UDP_SEGMENT + if (pktcnt == 0) { + gso_size = nwrite; + } else if (static_cast<size_t>(nwrite) > gso_size || + (gso_size > path_max_udp_payload_size && + static_cast<size_t>(nwrite) != gso_size)) { + auto data = quic.tx.data.get(); + auto datalen = bufpos - quic.tx.data.get() - nwrite; + rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, datalen, + gso_size); + if (rv == 1) { + on_send_blocked(ps.path.remote, data, datalen, gso_size); + on_send_blocked(ps.path.remote, bufpos - nwrite, nwrite, 0); + } else { + auto data = bufpos - nwrite; + rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, + nwrite, 0); + if (rv == 1) { + on_send_blocked(ps.path.remote, data, nwrite, 0); + } + } + + signal_write(); + return 0; + } + + // Assume that the path does not change. + if (++pktcnt == max_pktcnt || static_cast<size_t>(nwrite) < gso_size) { + auto data = quic.tx.data.get(); + auto datalen = bufpos - quic.tx.data.get(); + rv = write_udp(ps.path.remote.addr, ps.path.remote.addrlen, data, datalen, + gso_size); + if (rv == 1) { + on_send_blocked(ps.path.remote, data, datalen, gso_size); + } + signal_write(); + return 0; + } +#endif // UDP_SEGMENT + } +} + +void Client::on_send_blocked(const ngtcp2_addr &remote_addr, + const uint8_t *data, size_t datalen, + size_t gso_size) { + assert(quic.tx.num_blocked || !quic.tx.send_blocked); + assert(quic.tx.num_blocked < 2); + + quic.tx.send_blocked = true; + + auto &p = quic.tx.blocked[quic.tx.num_blocked++]; + + memcpy(&p.remote_addr.su, remote_addr.addr, remote_addr.addrlen); + + p.remote_addr.len = remote_addr.addrlen; + p.data = data; + p.datalen = datalen; + p.gso_size = gso_size; +} + +int Client::send_blocked_packet() { + int rv; + + assert(quic.tx.send_blocked); + + for (; quic.tx.num_blocked_sent < quic.tx.num_blocked; + ++quic.tx.num_blocked_sent) { + auto &p = quic.tx.blocked[quic.tx.num_blocked_sent]; + + rv = write_udp(&p.remote_addr.su.sa, p.remote_addr.len, p.data, p.datalen, + p.gso_size); + if (rv == 1) { + signal_write(); + + return 0; + } + } + + quic.tx.send_blocked = false; + quic.tx.num_blocked = 0; + quic.tx.num_blocked_sent = 0; + + return 0; +} + +} // namespace h2load diff --git a/src/h2load_quic.h b/src/h2load_quic.h new file mode 100644 index 0000000..225f00f --- /dev/null +++ b/src/h2load_quic.h @@ -0,0 +1,38 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_QUIC_H +#define H2LOAD_QUIC_H + +#include "nghttp2_config.h" + +#include <ev.h> + +#include "h2load.h" + +namespace h2load { +void quic_pkt_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents); +} // namespace h2load + +#endif // H2LOAD_QUIC_H diff --git a/src/h2load_session.h b/src/h2load_session.h new file mode 100644 index 0000000..ab3b8ec --- /dev/null +++ b/src/h2load_session.h @@ -0,0 +1,59 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef H2LOAD_SESSION_H +#define H2LOAD_SESSION_H + +#include "nghttp2_config.h" + +#include <sys/types.h> + +#include <cinttypes> + +#include "h2load.h" + +namespace h2load { + +class Session { +public: + virtual ~Session() {} + // Called when the connection was made. + virtual void on_connect() = 0; + // Called when one request must be issued. + virtual int submit_request() = 0; + // Called when incoming bytes are available. The subclass has to + // return the number of bytes read. + virtual int on_read(const uint8_t *data, size_t len) = 0; + // Called when write is available. Returns 0 on success, otherwise + // return -1. + virtual int on_write() = 0; + // Called when the underlying session must be terminated. + virtual void terminate() = 0; + // Return the maximum concurrency per connection + virtual size_t max_concurrent_streams() = 0; +}; + +} // namespace h2load + +#endif // H2LOAD_SESSION_H diff --git a/src/http-parser.patch b/src/http-parser.patch new file mode 100644 index 0000000..ef80940 --- /dev/null +++ b/src/http-parser.patch @@ -0,0 +1,28 @@ +commit a143133d43420ef89e4ba0d84c73998863cf9f81 +Author: Tatsuhiro Tsujikawa <tatsuhiro.t@gmail.com> +Date: Wed Jul 11 18:46:00 2012 +0900 + + Use http_parser for tunneling connection transparently + +diff --git a/examples/http-parser/http_parser.c b/examples/http-parser/http_parser.c +index 0c11eb8..610da57 100644 +--- a/examples/http-parser/http_parser.c ++++ b/examples/http-parser/http_parser.c +@@ -1627,9 +1627,14 @@ size_t http_parser_execute (http_parser *parser, + + /* Exit, the rest of the connect is in a different protocol. */ + if (parser->upgrade) { +- parser->state = NEW_MESSAGE(); +- CALLBACK_NOTIFY(message_complete); +- return (p - data) + 1; ++ /* We want to use http_parser for tunneling connection ++ transparently */ ++ /* Read body until EOF */ ++ parser->state = s_body_identity_eof; ++ break; ++ /* parser->state = NEW_MESSAGE(); */ ++ /* CALLBACK_NOTIFY(message_complete); */ ++ /* return (p - data) + 1; */ + } + + if (parser->flags & F_SKIPBODY) { diff --git a/src/http2.cc b/src/http2.cc new file mode 100644 index 0000000..661c4c9 --- /dev/null +++ b/src/http2.cc @@ -0,0 +1,2096 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http2.h" + +#include "llhttp.h" + +#include "util.h" + +namespace nghttp2 { + +namespace http2 { + +StringRef get_reason_phrase(unsigned int status_code) { + switch (status_code) { + case 100: + return StringRef::from_lit("Continue"); + case 101: + return StringRef::from_lit("Switching Protocols"); + case 103: + return StringRef::from_lit("Early Hints"); + case 200: + return StringRef::from_lit("OK"); + case 201: + return StringRef::from_lit("Created"); + case 202: + return StringRef::from_lit("Accepted"); + case 203: + return StringRef::from_lit("Non-Authoritative Information"); + case 204: + return StringRef::from_lit("No Content"); + case 205: + return StringRef::from_lit("Reset Content"); + case 206: + return StringRef::from_lit("Partial Content"); + case 300: + return StringRef::from_lit("Multiple Choices"); + case 301: + return StringRef::from_lit("Moved Permanently"); + case 302: + return StringRef::from_lit("Found"); + case 303: + return StringRef::from_lit("See Other"); + case 304: + return StringRef::from_lit("Not Modified"); + case 305: + return StringRef::from_lit("Use Proxy"); + // case 306: return StringRef::from_lit("(Unused)"); + case 307: + return StringRef::from_lit("Temporary Redirect"); + case 308: + return StringRef::from_lit("Permanent Redirect"); + case 400: + return StringRef::from_lit("Bad Request"); + case 401: + return StringRef::from_lit("Unauthorized"); + case 402: + return StringRef::from_lit("Payment Required"); + case 403: + return StringRef::from_lit("Forbidden"); + case 404: + return StringRef::from_lit("Not Found"); + case 405: + return StringRef::from_lit("Method Not Allowed"); + case 406: + return StringRef::from_lit("Not Acceptable"); + case 407: + return StringRef::from_lit("Proxy Authentication Required"); + case 408: + return StringRef::from_lit("Request Timeout"); + case 409: + return StringRef::from_lit("Conflict"); + case 410: + return StringRef::from_lit("Gone"); + case 411: + return StringRef::from_lit("Length Required"); + case 412: + return StringRef::from_lit("Precondition Failed"); + case 413: + return StringRef::from_lit("Payload Too Large"); + case 414: + return StringRef::from_lit("URI Too Long"); + case 415: + return StringRef::from_lit("Unsupported Media Type"); + case 416: + return StringRef::from_lit("Requested Range Not Satisfiable"); + case 417: + return StringRef::from_lit("Expectation Failed"); + case 421: + return StringRef::from_lit("Misdirected Request"); + case 425: + // https://tools.ietf.org/html/rfc8470 + return StringRef::from_lit("Too Early"); + case 426: + return StringRef::from_lit("Upgrade Required"); + case 428: + return StringRef::from_lit("Precondition Required"); + case 429: + return StringRef::from_lit("Too Many Requests"); + case 431: + return StringRef::from_lit("Request Header Fields Too Large"); + case 451: + return StringRef::from_lit("Unavailable For Legal Reasons"); + case 500: + return StringRef::from_lit("Internal Server Error"); + case 501: + return StringRef::from_lit("Not Implemented"); + case 502: + return StringRef::from_lit("Bad Gateway"); + case 503: + return StringRef::from_lit("Service Unavailable"); + case 504: + return StringRef::from_lit("Gateway Timeout"); + case 505: + return StringRef::from_lit("HTTP Version Not Supported"); + case 511: + return StringRef::from_lit("Network Authentication Required"); + default: + return StringRef{}; + } +} + +StringRef stringify_status(BlockAllocator &balloc, unsigned int status_code) { + switch (status_code) { + case 100: + return StringRef::from_lit("100"); + case 101: + return StringRef::from_lit("101"); + case 103: + return StringRef::from_lit("103"); + case 200: + return StringRef::from_lit("200"); + case 201: + return StringRef::from_lit("201"); + case 202: + return StringRef::from_lit("202"); + case 203: + return StringRef::from_lit("203"); + case 204: + return StringRef::from_lit("204"); + case 205: + return StringRef::from_lit("205"); + case 206: + return StringRef::from_lit("206"); + case 300: + return StringRef::from_lit("300"); + case 301: + return StringRef::from_lit("301"); + case 302: + return StringRef::from_lit("302"); + case 303: + return StringRef::from_lit("303"); + case 304: + return StringRef::from_lit("304"); + case 305: + return StringRef::from_lit("305"); + // case 306: return StringRef::from_lit("306"); + case 307: + return StringRef::from_lit("307"); + case 308: + return StringRef::from_lit("308"); + case 400: + return StringRef::from_lit("400"); + case 401: + return StringRef::from_lit("401"); + case 402: + return StringRef::from_lit("402"); + case 403: + return StringRef::from_lit("403"); + case 404: + return StringRef::from_lit("404"); + case 405: + return StringRef::from_lit("405"); + case 406: + return StringRef::from_lit("406"); + case 407: + return StringRef::from_lit("407"); + case 408: + return StringRef::from_lit("408"); + case 409: + return StringRef::from_lit("409"); + case 410: + return StringRef::from_lit("410"); + case 411: + return StringRef::from_lit("411"); + case 412: + return StringRef::from_lit("412"); + case 413: + return StringRef::from_lit("413"); + case 414: + return StringRef::from_lit("414"); + case 415: + return StringRef::from_lit("415"); + case 416: + return StringRef::from_lit("416"); + case 417: + return StringRef::from_lit("417"); + case 421: + return StringRef::from_lit("421"); + case 426: + return StringRef::from_lit("426"); + case 428: + return StringRef::from_lit("428"); + case 429: + return StringRef::from_lit("429"); + case 431: + return StringRef::from_lit("431"); + case 451: + return StringRef::from_lit("451"); + case 500: + return StringRef::from_lit("500"); + case 501: + return StringRef::from_lit("501"); + case 502: + return StringRef::from_lit("502"); + case 503: + return StringRef::from_lit("503"); + case 504: + return StringRef::from_lit("504"); + case 505: + return StringRef::from_lit("505"); + case 511: + return StringRef::from_lit("511"); + default: + return util::make_string_ref_uint(balloc, status_code); + } +} + +void capitalize(DefaultMemchunks *buf, const StringRef &s) { + buf->append(util::upcase(s[0])); + for (size_t i = 1; i < s.size(); ++i) { + if (s[i - 1] == '-') { + buf->append(util::upcase(s[i])); + } else { + buf->append(s[i]); + } + } +} + +bool lws(const char *value) { + for (; *value; ++value) { + switch (*value) { + case '\t': + case ' ': + continue; + default: + return false; + } + } + return true; +} + +void copy_url_component(std::string &dest, const http_parser_url *u, int field, + const char *url) { + if (u->field_set & (1 << field)) { + dest.assign(url + u->field_data[field].off, u->field_data[field].len); + } +} + +Headers::value_type to_header(const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, + bool no_index, int32_t token) { + return Header(std::string(reinterpret_cast<const char *>(name), namelen), + std::string(reinterpret_cast<const char *>(value), valuelen), + no_index, token); +} + +void add_header(Headers &nva, const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, bool no_index, + int32_t token) { + if (valuelen > 0) { + size_t i, j; + for (i = 0; i < valuelen && (value[i] == ' ' || value[i] == '\t'); ++i) + ; + for (j = valuelen - 1; j > i && (value[j] == ' ' || value[j] == '\t'); --j) + ; + value += i; + valuelen -= i + (valuelen - j - 1); + } + nva.push_back(to_header(name, namelen, value, valuelen, no_index, token)); +} + +const Headers::value_type *get_header(const Headers &nva, const char *name) { + const Headers::value_type *res = nullptr; + for (auto &nv : nva) { + if (nv.name == name) { + res = &nv; + } + } + return res; +} + +bool non_empty_value(const HeaderRefs::value_type *nv) { + return nv && !nv->value.empty(); +} + +namespace { +nghttp2_nv make_nv_internal(const std::string &name, const std::string &value, + bool no_index, uint8_t nv_flags) { + uint8_t flags; + + flags = + nv_flags | (no_index ? NGHTTP2_NV_FLAG_NO_INDEX : NGHTTP2_NV_FLAG_NONE); + + return {(uint8_t *)name.c_str(), (uint8_t *)value.c_str(), name.size(), + value.size(), flags}; +} +} // namespace + +namespace { +nghttp2_nv make_nv_internal(const StringRef &name, const StringRef &value, + bool no_index, uint8_t nv_flags) { + uint8_t flags; + + flags = + nv_flags | (no_index ? NGHTTP2_NV_FLAG_NO_INDEX : NGHTTP2_NV_FLAG_NONE); + + return {(uint8_t *)name.c_str(), (uint8_t *)value.c_str(), name.size(), + value.size(), flags}; +} +} // namespace + +nghttp2_nv make_nv(const std::string &name, const std::string &value, + bool no_index) { + return make_nv_internal(name, value, no_index, NGHTTP2_NV_FLAG_NONE); +} + +nghttp2_nv make_nv(const StringRef &name, const StringRef &value, + bool no_index) { + return make_nv_internal(name, value, no_index, NGHTTP2_NV_FLAG_NONE); +} + +nghttp2_nv make_nv_nocopy(const std::string &name, const std::string &value, + bool no_index) { + return make_nv_internal(name, value, no_index, + NGHTTP2_NV_FLAG_NO_COPY_NAME | + NGHTTP2_NV_FLAG_NO_COPY_VALUE); +} + +nghttp2_nv make_nv_nocopy(const StringRef &name, const StringRef &value, + bool no_index) { + return make_nv_internal(name, value, no_index, + NGHTTP2_NV_FLAG_NO_COPY_NAME | + NGHTTP2_NV_FLAG_NO_COPY_VALUE); +} + +namespace { +void copy_headers_to_nva_internal(std::vector<nghttp2_nv> &nva, + const HeaderRefs &headers, uint8_t nv_flags, + uint32_t flags) { + auto it_forwarded = std::end(headers); + auto it_xff = std::end(headers); + auto it_xfp = std::end(headers); + auto it_via = std::end(headers); + + for (auto it = std::begin(headers); it != std::end(headers); ++it) { + auto kv = &(*it); + if (kv->name.empty() || kv->name[0] == ':') { + continue; + } + switch (kv->token) { + case HD_COOKIE: + case HD_CONNECTION: + case HD_HOST: + case HD_HTTP2_SETTINGS: + case HD_KEEP_ALIVE: + case HD_PROXY_CONNECTION: + case HD_SERVER: + case HD_TE: + case HD_TRANSFER_ENCODING: + case HD_UPGRADE: + continue; + case HD_EARLY_DATA: + if (flags & HDOP_STRIP_EARLY_DATA) { + continue; + } + break; + case HD_SEC_WEBSOCKET_ACCEPT: + if (flags & HDOP_STRIP_SEC_WEBSOCKET_ACCEPT) { + continue; + } + break; + case HD_SEC_WEBSOCKET_KEY: + if (flags & HDOP_STRIP_SEC_WEBSOCKET_KEY) { + continue; + } + break; + case HD_FORWARDED: + if (flags & HDOP_STRIP_FORWARDED) { + continue; + } + + if (it_forwarded == std::end(headers)) { + it_forwarded = it; + continue; + } + + kv = &(*it_forwarded); + it_forwarded = it; + break; + case HD_X_FORWARDED_FOR: + if (flags & HDOP_STRIP_X_FORWARDED_FOR) { + continue; + } + + if (it_xff == std::end(headers)) { + it_xff = it; + continue; + } + + kv = &(*it_xff); + it_xff = it; + break; + case HD_X_FORWARDED_PROTO: + if (flags & HDOP_STRIP_X_FORWARDED_PROTO) { + continue; + } + + if (it_xfp == std::end(headers)) { + it_xfp = it; + continue; + } + + kv = &(*it_xfp); + it_xfp = it; + break; + case HD_VIA: + if (flags & HDOP_STRIP_VIA) { + continue; + } + + if (it_via == std::end(headers)) { + it_via = it; + continue; + } + + kv = &(*it_via); + it_via = it; + break; + } + nva.push_back( + make_nv_internal(kv->name, kv->value, kv->no_index, nv_flags)); + } +} +} // namespace + +void copy_headers_to_nva(std::vector<nghttp2_nv> &nva, + const HeaderRefs &headers, uint32_t flags) { + copy_headers_to_nva_internal(nva, headers, NGHTTP2_NV_FLAG_NONE, flags); +} + +void copy_headers_to_nva_nocopy(std::vector<nghttp2_nv> &nva, + const HeaderRefs &headers, uint32_t flags) { + copy_headers_to_nva_internal( + nva, headers, + NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE, flags); +} + +void build_http1_headers_from_headers(DefaultMemchunks *buf, + const HeaderRefs &headers, + uint32_t flags) { + auto it_forwarded = std::end(headers); + auto it_xff = std::end(headers); + auto it_xfp = std::end(headers); + auto it_via = std::end(headers); + + for (auto it = std::begin(headers); it != std::end(headers); ++it) { + auto kv = &(*it); + if (kv->name.empty() || kv->name[0] == ':') { + continue; + } + switch (kv->token) { + case HD_CONNECTION: + case HD_COOKIE: + case HD_HOST: + case HD_HTTP2_SETTINGS: + case HD_KEEP_ALIVE: + case HD_PROXY_CONNECTION: + case HD_SERVER: + case HD_UPGRADE: + continue; + case HD_EARLY_DATA: + if (flags & HDOP_STRIP_EARLY_DATA) { + continue; + } + break; + case HD_TRANSFER_ENCODING: + if (flags & HDOP_STRIP_TRANSFER_ENCODING) { + continue; + } + break; + case HD_FORWARDED: + if (flags & HDOP_STRIP_FORWARDED) { + continue; + } + + if (it_forwarded == std::end(headers)) { + it_forwarded = it; + continue; + } + + kv = &(*it_forwarded); + it_forwarded = it; + break; + case HD_X_FORWARDED_FOR: + if (flags & HDOP_STRIP_X_FORWARDED_FOR) { + continue; + } + + if (it_xff == std::end(headers)) { + it_xff = it; + continue; + } + + kv = &(*it_xff); + it_xff = it; + break; + case HD_X_FORWARDED_PROTO: + if (flags & HDOP_STRIP_X_FORWARDED_PROTO) { + continue; + } + + if (it_xfp == std::end(headers)) { + it_xfp = it; + continue; + } + + kv = &(*it_xfp); + it_xfp = it; + break; + case HD_VIA: + if (flags & HDOP_STRIP_VIA) { + continue; + } + + if (it_via == std::end(headers)) { + it_via = it; + continue; + } + + kv = &(*it_via); + it_via = it; + break; + } + capitalize(buf, kv->name); + buf->append(": "); + buf->append(kv->value); + buf->append("\r\n"); + } +} + +int32_t determine_window_update_transmission(nghttp2_session *session, + int32_t stream_id) { + int32_t recv_length, window_size; + if (stream_id == 0) { + recv_length = nghttp2_session_get_effective_recv_data_length(session); + window_size = nghttp2_session_get_effective_local_window_size(session); + } else { + recv_length = nghttp2_session_get_stream_effective_recv_data_length( + session, stream_id); + window_size = nghttp2_session_get_stream_effective_local_window_size( + session, stream_id); + } + if (recv_length != -1 && window_size != -1) { + if (recv_length >= window_size / 2) { + return recv_length; + } + } + return -1; +} + +void dump_nv(FILE *out, const char **nv) { + for (size_t i = 0; nv[i]; i += 2) { + fprintf(out, "%s: %s\n", nv[i], nv[i + 1]); + } + fputc('\n', out); + fflush(out); +} + +void dump_nv(FILE *out, const nghttp2_nv *nva, size_t nvlen) { + auto end = nva + nvlen; + for (; nva != end; ++nva) { + fprintf(out, "%s: %s\n", nva->name, nva->value); + } + fputc('\n', out); + fflush(out); +} + +void dump_nv(FILE *out, const Headers &nva) { + for (auto &nv : nva) { + fprintf(out, "%s: %s\n", nv.name.c_str(), nv.value.c_str()); + } + fputc('\n', out); + fflush(out); +} + +void dump_nv(FILE *out, const HeaderRefs &nva) { + for (auto &nv : nva) { + fprintf(out, "%s: %s\n", nv.name.c_str(), nv.value.c_str()); + } + fputc('\n', out); + fflush(out); +} + +void erase_header(HeaderRef *hd) { + hd->name = StringRef{}; + hd->token = -1; +} + +StringRef rewrite_location_uri(BlockAllocator &balloc, const StringRef &uri, + const http_parser_url &u, + const StringRef &match_host, + const StringRef &request_authority, + const StringRef &upstream_scheme) { + // We just rewrite scheme and authority. + if ((u.field_set & (1 << UF_HOST)) == 0) { + return StringRef{}; + } + auto field = &u.field_data[UF_HOST]; + if (!util::starts_with(std::begin(match_host), std::end(match_host), + &uri[field->off], &uri[field->off] + field->len) || + (match_host.size() != field->len && match_host[field->len] != ':')) { + return StringRef{}; + } + + auto len = 0; + if (!request_authority.empty()) { + len += upstream_scheme.size() + str_size("://") + request_authority.size(); + } + + if (u.field_set & (1 << UF_PATH)) { + field = &u.field_data[UF_PATH]; + len += field->len; + } + + if (u.field_set & (1 << UF_QUERY)) { + field = &u.field_data[UF_QUERY]; + len += 1 + field->len; + } + + if (u.field_set & (1 << UF_FRAGMENT)) { + field = &u.field_data[UF_FRAGMENT]; + len += 1 + field->len; + } + + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + + if (!request_authority.empty()) { + p = std::copy(std::begin(upstream_scheme), std::end(upstream_scheme), p); + p = util::copy_lit(p, "://"); + p = std::copy(std::begin(request_authority), std::end(request_authority), + p); + } + if (u.field_set & (1 << UF_PATH)) { + field = &u.field_data[UF_PATH]; + p = std::copy_n(&uri[field->off], field->len, p); + } + if (u.field_set & (1 << UF_QUERY)) { + field = &u.field_data[UF_QUERY]; + *p++ = '?'; + p = std::copy_n(&uri[field->off], field->len, p); + } + if (u.field_set & (1 << UF_FRAGMENT)) { + field = &u.field_data[UF_FRAGMENT]; + *p++ = '#'; + p = std::copy_n(&uri[field->off], field->len, p); + } + + *p = '\0'; + + return StringRef{iov.base, p}; +} + +int parse_http_status_code(const StringRef &src) { + if (src.size() != 3) { + return -1; + } + + int status = 0; + for (auto c : src) { + if (!isdigit(c)) { + return -1; + } + status *= 10; + status += c - '0'; + } + + if (status < 100) { + return -1; + } + + return status; +} + +int lookup_token(const StringRef &name) { + return lookup_token(name.byte(), name.size()); +} + +// This function was generated by genheaderfunc.py. Inspired by h2o +// header lookup. https://github.com/h2o/h2o +int lookup_token(const uint8_t *name, size_t namelen) { + switch (namelen) { + case 2: + switch (name[1]) { + case 'e': + if (util::streq_l("t", name, 1)) { + return HD_TE; + } + break; + } + break; + case 3: + switch (name[2]) { + case 'a': + if (util::streq_l("vi", name, 2)) { + return HD_VIA; + } + break; + } + break; + case 4: + switch (name[3]) { + case 'e': + if (util::streq_l("dat", name, 3)) { + return HD_DATE; + } + break; + case 'k': + if (util::streq_l("lin", name, 3)) { + return HD_LINK; + } + break; + case 't': + if (util::streq_l("hos", name, 3)) { + return HD_HOST; + } + break; + } + break; + case 5: + switch (name[4]) { + case 'h': + if (util::streq_l(":pat", name, 4)) { + return HD__PATH; + } + break; + case 't': + if (util::streq_l(":hos", name, 4)) { + return HD__HOST; + } + break; + } + break; + case 6: + switch (name[5]) { + case 'e': + if (util::streq_l("cooki", name, 5)) { + return HD_COOKIE; + } + break; + case 'r': + if (util::streq_l("serve", name, 5)) { + return HD_SERVER; + } + break; + case 't': + if (util::streq_l("expec", name, 5)) { + return HD_EXPECT; + } + break; + } + break; + case 7: + switch (name[6]) { + case 'c': + if (util::streq_l("alt-sv", name, 6)) { + return HD_ALT_SVC; + } + break; + case 'd': + if (util::streq_l(":metho", name, 6)) { + return HD__METHOD; + } + break; + case 'e': + if (util::streq_l(":schem", name, 6)) { + return HD__SCHEME; + } + if (util::streq_l("upgrad", name, 6)) { + return HD_UPGRADE; + } + break; + case 'r': + if (util::streq_l("traile", name, 6)) { + return HD_TRAILER; + } + break; + case 's': + if (util::streq_l(":statu", name, 6)) { + return HD__STATUS; + } + break; + } + break; + case 8: + switch (name[7]) { + case 'n': + if (util::streq_l("locatio", name, 7)) { + return HD_LOCATION; + } + break; + case 'y': + if (util::streq_l("priorit", name, 7)) { + return HD_PRIORITY; + } + break; + } + break; + case 9: + switch (name[8]) { + case 'd': + if (util::streq_l("forwarde", name, 8)) { + return HD_FORWARDED; + } + break; + case 'l': + if (util::streq_l(":protoco", name, 8)) { + return HD__PROTOCOL; + } + break; + } + break; + case 10: + switch (name[9]) { + case 'a': + if (util::streq_l("early-dat", name, 9)) { + return HD_EARLY_DATA; + } + break; + case 'e': + if (util::streq_l("keep-aliv", name, 9)) { + return HD_KEEP_ALIVE; + } + break; + case 'n': + if (util::streq_l("connectio", name, 9)) { + return HD_CONNECTION; + } + break; + case 't': + if (util::streq_l("user-agen", name, 9)) { + return HD_USER_AGENT; + } + break; + case 'y': + if (util::streq_l(":authorit", name, 9)) { + return HD__AUTHORITY; + } + break; + } + break; + case 12: + switch (name[11]) { + case 'e': + if (util::streq_l("content-typ", name, 11)) { + return HD_CONTENT_TYPE; + } + break; + } + break; + case 13: + switch (name[12]) { + case 'l': + if (util::streq_l("cache-contro", name, 12)) { + return HD_CACHE_CONTROL; + } + break; + } + break; + case 14: + switch (name[13]) { + case 'h': + if (util::streq_l("content-lengt", name, 13)) { + return HD_CONTENT_LENGTH; + } + break; + case 's': + if (util::streq_l("http2-setting", name, 13)) { + return HD_HTTP2_SETTINGS; + } + break; + } + break; + case 15: + switch (name[14]) { + case 'e': + if (util::streq_l("accept-languag", name, 14)) { + return HD_ACCEPT_LANGUAGE; + } + break; + case 'g': + if (util::streq_l("accept-encodin", name, 14)) { + return HD_ACCEPT_ENCODING; + } + break; + case 'r': + if (util::streq_l("x-forwarded-fo", name, 14)) { + return HD_X_FORWARDED_FOR; + } + break; + } + break; + case 16: + switch (name[15]) { + case 'n': + if (util::streq_l("proxy-connectio", name, 15)) { + return HD_PROXY_CONNECTION; + } + break; + } + break; + case 17: + switch (name[16]) { + case 'e': + if (util::streq_l("if-modified-sinc", name, 16)) { + return HD_IF_MODIFIED_SINCE; + } + break; + case 'g': + if (util::streq_l("transfer-encodin", name, 16)) { + return HD_TRANSFER_ENCODING; + } + break; + case 'o': + if (util::streq_l("x-forwarded-prot", name, 16)) { + return HD_X_FORWARDED_PROTO; + } + break; + case 'y': + if (util::streq_l("sec-websocket-ke", name, 16)) { + return HD_SEC_WEBSOCKET_KEY; + } + break; + } + break; + case 20: + switch (name[19]) { + case 't': + if (util::streq_l("sec-websocket-accep", name, 19)) { + return HD_SEC_WEBSOCKET_ACCEPT; + } + break; + } + break; + } + return -1; +} + +void init_hdidx(HeaderIndex &hdidx) { + std::fill(std::begin(hdidx), std::end(hdidx), -1); +} + +void index_header(HeaderIndex &hdidx, int32_t token, size_t idx) { + if (token == -1) { + return; + } + assert(token < HD_MAXIDX); + hdidx[token] = idx; +} + +const Headers::value_type *get_header(const HeaderIndex &hdidx, int32_t token, + const Headers &nva) { + auto i = hdidx[token]; + if (i == -1) { + return nullptr; + } + return &nva[i]; +} + +Headers::value_type *get_header(const HeaderIndex &hdidx, int32_t token, + Headers &nva) { + auto i = hdidx[token]; + if (i == -1) { + return nullptr; + } + return &nva[i]; +} + +namespace { +template <typename InputIt> InputIt skip_lws(InputIt first, InputIt last) { + for (; first != last; ++first) { + switch (*first) { + case ' ': + case '\t': + continue; + default: + return first; + } + } + return first; +} +} // namespace + +namespace { +template <typename InputIt> +InputIt skip_to_next_field(InputIt first, InputIt last) { + for (; first != last; ++first) { + switch (*first) { + case ' ': + case '\t': + case ',': + continue; + default: + return first; + } + } + return first; +} +} // namespace + +namespace { +// Skip to the right dquote ('"'), handling backslash escapes. +// Returns |last| if input is not terminated with '"'. +template <typename InputIt> +InputIt skip_to_right_dquote(InputIt first, InputIt last) { + for (; first != last;) { + switch (*first) { + case '"': + return first; + // quoted-pair + case '\\': + ++first; + if (first == last) { + return first; + } + + switch (*first) { + case '\t': + case ' ': + break; + default: + if ((0x21 <= *first && *first <= 0x7e) /* VCHAR */ || + (0x80 <= *first && *first <= 0xff) /* obs-text */) { + break; + } + + return last; + } + + break; + // qdtext + case '\t': + case ' ': + case '!': + break; + default: + if ((0x23 <= *first && *first <= 0x5b) || + (0x5d <= *first && *first <= 0x7e)) { + break; + } + + return last; + } + ++first; + } + return first; +} +} // namespace + +namespace { +// Returns true if link-param does not match pattern |pat| of length +// |patlen| or it has empty value (""). |pat| should be parmname +// followed by "=". +bool check_link_param_empty(const char *first, const char *last, + const char *pat, size_t patlen) { + if (first + patlen <= last) { + if (std::equal(pat, pat + patlen, first, util::CaseCmp())) { + // we only accept URI if pat is followed by "" (e.g., + // loadpolicy="") here. + if (first + patlen + 2 <= last) { + if (*(first + patlen) != '"' || *(first + patlen + 1) != '"') { + return false; + } + } else { + // here we got invalid production (anchor=") or anchor=? + return false; + } + } + } + return true; +} +} // namespace + +namespace { +// Returns true if link-param consists of only parmname, and it +// matches string [pat, pat + patlen). +bool check_link_param_without_value(const char *first, const char *last, + const char *pat, size_t patlen) { + if (first + patlen > last) { + return false; + } + + if (first + patlen == last) { + return std::equal(pat, pat + patlen, first, util::CaseCmp()); + } + + switch (*(first + patlen)) { + case ';': + case ',': + return std::equal(pat, pat + patlen, first, util::CaseCmp()); + } + + return false; +} +} // namespace + +namespace { +std::pair<LinkHeader, const char *> +parse_next_link_header_once(const char *first, const char *last) { + first = skip_to_next_field(first, last); + if (first == last || *first != '<') { + return {{StringRef{}}, last}; + } + auto url_first = ++first; + first = std::find(first, last, '>'); + if (first == last) { + return {{StringRef{}}, first}; + } + auto url_last = first++; + if (first == last) { + return {{StringRef{}}, first}; + } + // we expect ';' or ',' here + switch (*first) { + case ',': + return {{StringRef{}}, ++first}; + case ';': + ++first; + break; + default: + return {{StringRef{}}, last}; + } + + auto ok = false; + auto ign = false; + for (;;) { + first = skip_lws(first, last); + if (first == last) { + return {{StringRef{}}, first}; + } + // we expect link-param + + if (!ign) { + if (!ok) { + // rel can take several relations using quoted form. + static constexpr char PLP[] = "rel=\""; + static constexpr size_t PLPLEN = str_size(PLP); + + static constexpr char PLT[] = "preload"; + static constexpr size_t PLTLEN = str_size(PLT); + if (first + PLPLEN < last && *(first + PLPLEN - 1) == '"' && + std::equal(PLP, PLP + PLPLEN, first, util::CaseCmp())) { + // we have to search preload in whitespace separated list: + // rel="preload something http://example.org/foo" + first += PLPLEN; + auto start = first; + for (; first != last;) { + if (*first != ' ' && *first != '"') { + ++first; + continue; + } + + if (start == first) { + return {{StringRef{}}, last}; + } + + if (!ok && start + PLTLEN == first && + std::equal(PLT, PLT + PLTLEN, start, util::CaseCmp())) { + ok = true; + } + + if (*first == '"') { + break; + } + first = skip_lws(first, last); + start = first; + } + if (first == last) { + return {{StringRef{}}, last}; + } + assert(*first == '"'); + ++first; + if (first == last || *first == ',') { + goto almost_done; + } + if (*first == ';') { + ++first; + // parse next link-param + continue; + } + return {{StringRef{}}, last}; + } + } + // we are only interested in rel=preload parameter. Others are + // simply skipped. + static constexpr char PL[] = "rel=preload"; + static constexpr size_t PLLEN = str_size(PL); + if (first + PLLEN == last) { + if (std::equal(PL, PL + PLLEN, first, util::CaseCmp())) { + // ok = true; + // this is the end of sequence + return {{{url_first, url_last}}, last}; + } + } else if (first + PLLEN + 1 <= last) { + switch (*(first + PLLEN)) { + case ',': + if (!std::equal(PL, PL + PLLEN, first, util::CaseCmp())) { + break; + } + // ok = true; + // skip including ',' + first += PLLEN + 1; + return {{{url_first, url_last}}, first}; + case ';': + if (!std::equal(PL, PL + PLLEN, first, util::CaseCmp())) { + break; + } + ok = true; + // skip including ';' + first += PLLEN + 1; + // continue parse next link-param + continue; + } + } + // we have to reject URI if we have nonempty anchor parameter. + static constexpr char ANCHOR[] = "anchor="; + static constexpr size_t ANCHORLEN = str_size(ANCHOR); + if (!ign && !check_link_param_empty(first, last, ANCHOR, ANCHORLEN)) { + ign = true; + } + + // reject URI if we have non-empty loadpolicy. This could be + // tightened up to just pick up "next" or "insert". + static constexpr char LOADPOLICY[] = "loadpolicy="; + static constexpr size_t LOADPOLICYLEN = str_size(LOADPOLICY); + if (!ign && + !check_link_param_empty(first, last, LOADPOLICY, LOADPOLICYLEN)) { + ign = true; + } + + // reject URI if we have nopush attribute. + static constexpr char NOPUSH[] = "nopush"; + static constexpr size_t NOPUSHLEN = str_size(NOPUSH); + if (!ign && + check_link_param_without_value(first, last, NOPUSH, NOPUSHLEN)) { + ign = true; + } + } + + auto param_first = first; + for (; first != last;) { + if (util::in_attr_char(*first)) { + ++first; + continue; + } + // '*' is only allowed at the end of parameter name and must be + // followed by '=' + if (last - first >= 2 && first != param_first) { + if (*first == '*' && *(first + 1) == '=') { + ++first; + break; + } + } + if (*first == '=' || *first == ';' || *first == ',') { + break; + } + return {{StringRef{}}, last}; + } + if (param_first == first) { + // empty parmname + return {{StringRef{}}, last}; + } + // link-param without value is acceptable (see link-extension) if + // it is not followed by '=' + if (first == last || *first == ',') { + goto almost_done; + } + if (*first == ';') { + ++first; + // parse next link-param + continue; + } + // now parsing link-param value + assert(*first == '='); + ++first; + if (first == last) { + // empty value is not acceptable + return {{StringRef{}}, first}; + } + if (*first == '"') { + // quoted-string + first = skip_to_right_dquote(first + 1, last); + if (first == last) { + return {{StringRef{}}, first}; + } + ++first; + if (first == last || *first == ',') { + goto almost_done; + } + if (*first == ';') { + ++first; + // parse next link-param + continue; + } + return {{StringRef{}}, last}; + } + // not quoted-string, skip to next ',' or ';' + if (*first == ',' || *first == ';') { + // empty value + return {{StringRef{}}, last}; + } + for (; first != last; ++first) { + if (*first == ',' || *first == ';') { + break; + } + } + if (first == last || *first == ',') { + goto almost_done; + } + assert(*first == ';'); + ++first; + // parse next link-param + } + +almost_done: + assert(first == last || *first == ','); + + if (first != last) { + ++first; + } + if (ok && !ign) { + return {{{url_first, url_last}}, first}; + } + return {{StringRef{}}, first}; +} +} // namespace + +std::vector<LinkHeader> parse_link_header(const StringRef &src) { + std::vector<LinkHeader> res; + for (auto first = std::begin(src); first != std::end(src);) { + auto rv = parse_next_link_header_once(first, std::end(src)); + first = rv.second; + auto &link = rv.first; + if (!link.uri.empty()) { + res.push_back(link); + } + } + return res; +} + +std::string path_join(const StringRef &base_path, const StringRef &base_query, + const StringRef &rel_path, const StringRef &rel_query) { + BlockAllocator balloc(1024, 1024); + + return path_join(balloc, base_path, base_query, rel_path, rel_query).str(); +} + +bool expect_response_body(int status_code) { + return status_code == 101 || + (status_code / 100 != 1 && status_code != 304 && status_code != 204); +} + +bool expect_response_body(const std::string &method, int status_code) { + return method != "HEAD" && expect_response_body(status_code); +} + +bool expect_response_body(int method_token, int status_code) { + return method_token != HTTP_HEAD && expect_response_body(status_code); +} + +int lookup_method_token(const StringRef &name) { + return lookup_method_token(name.byte(), name.size()); +} + +// This function was generated by genmethodfunc.py. +int lookup_method_token(const uint8_t *name, size_t namelen) { + switch (namelen) { + case 3: + switch (name[2]) { + case 'L': + if (util::streq_l("AC", name, 2)) { + return HTTP_ACL; + } + break; + case 'T': + if (util::streq_l("GE", name, 2)) { + return HTTP_GET; + } + if (util::streq_l("PU", name, 2)) { + return HTTP_PUT; + } + break; + } + break; + case 4: + switch (name[3]) { + case 'D': + if (util::streq_l("BIN", name, 3)) { + return HTTP_BIND; + } + if (util::streq_l("HEA", name, 3)) { + return HTTP_HEAD; + } + break; + case 'E': + if (util::streq_l("MOV", name, 3)) { + return HTTP_MOVE; + } + break; + case 'K': + if (util::streq_l("LIN", name, 3)) { + return HTTP_LINK; + } + if (util::streq_l("LOC", name, 3)) { + return HTTP_LOCK; + } + break; + case 'T': + if (util::streq_l("POS", name, 3)) { + return HTTP_POST; + } + break; + case 'Y': + if (util::streq_l("COP", name, 3)) { + return HTTP_COPY; + } + break; + } + break; + case 5: + switch (name[4]) { + case 'E': + if (util::streq_l("MERG", name, 4)) { + return HTTP_MERGE; + } + if (util::streq_l("PURG", name, 4)) { + return HTTP_PURGE; + } + if (util::streq_l("TRAC", name, 4)) { + return HTTP_TRACE; + } + break; + case 'H': + if (util::streq_l("PATC", name, 4)) { + return HTTP_PATCH; + } + break; + case 'L': + if (util::streq_l("MKCO", name, 4)) { + return HTTP_MKCOL; + } + break; + } + break; + case 6: + switch (name[5]) { + case 'D': + if (util::streq_l("REBIN", name, 5)) { + return HTTP_REBIND; + } + if (util::streq_l("UNBIN", name, 5)) { + return HTTP_UNBIND; + } + break; + case 'E': + if (util::streq_l("DELET", name, 5)) { + return HTTP_DELETE; + } + if (util::streq_l("SOURC", name, 5)) { + return HTTP_SOURCE; + } + break; + case 'H': + if (util::streq_l("SEARC", name, 5)) { + return HTTP_SEARCH; + } + break; + case 'K': + if (util::streq_l("UNLIN", name, 5)) { + return HTTP_UNLINK; + } + if (util::streq_l("UNLOC", name, 5)) { + return HTTP_UNLOCK; + } + break; + case 'T': + if (util::streq_l("REPOR", name, 5)) { + return HTTP_REPORT; + } + break; + case 'Y': + if (util::streq_l("NOTIF", name, 5)) { + return HTTP_NOTIFY; + } + break; + } + break; + case 7: + switch (name[6]) { + case 'H': + if (util::streq_l("MSEARC", name, 6)) { + return HTTP_MSEARCH; + } + break; + case 'S': + if (util::streq_l("OPTION", name, 6)) { + return HTTP_OPTIONS; + } + break; + case 'T': + if (util::streq_l("CONNEC", name, 6)) { + return HTTP_CONNECT; + } + break; + } + break; + case 8: + switch (name[7]) { + case 'D': + if (util::streq_l("PROPFIN", name, 7)) { + return HTTP_PROPFIND; + } + break; + case 'T': + if (util::streq_l("CHECKOU", name, 7)) { + return HTTP_CHECKOUT; + } + break; + } + break; + case 9: + switch (name[8]) { + case 'E': + if (util::streq_l("SUBSCRIB", name, 8)) { + return HTTP_SUBSCRIBE; + } + break; + case 'H': + if (util::streq_l("PROPPATC", name, 8)) { + return HTTP_PROPPATCH; + } + break; + } + break; + case 10: + switch (name[9]) { + case 'R': + if (util::streq_l("MKCALENDA", name, 9)) { + return HTTP_MKCALENDAR; + } + break; + case 'Y': + if (util::streq_l("MKACTIVIT", name, 9)) { + return HTTP_MKACTIVITY; + } + break; + } + break; + case 11: + switch (name[10]) { + case 'E': + if (util::streq_l("UNSUBSCRIB", name, 10)) { + return HTTP_UNSUBSCRIBE; + } + break; + } + break; + } + return -1; +} + +StringRef to_method_string(int method_token) { + // we happened to use same value for method with llhttp. + return StringRef{ + llhttp_method_name(static_cast<llhttp_method>(method_token))}; +} + +StringRef get_pure_path_component(const StringRef &uri) { + int rv; + + http_parser_url u{}; + rv = http_parser_parse_url(uri.c_str(), uri.size(), 0, &u); + if (rv != 0) { + return StringRef{}; + } + + if (u.field_set & (1 << UF_PATH)) { + auto &f = u.field_data[UF_PATH]; + return StringRef{uri.c_str() + f.off, f.len}; + } + + return StringRef::from_lit("/"); +} + +int construct_push_component(BlockAllocator &balloc, StringRef &scheme, + StringRef &authority, StringRef &path, + const StringRef &base, const StringRef &uri) { + int rv; + StringRef rel, relq; + + if (uri.size() == 0) { + return -1; + } + + http_parser_url u{}; + + rv = http_parser_parse_url(uri.c_str(), uri.size(), 0, &u); + + if (rv != 0) { + if (uri[0] == '/') { + return -1; + } + + // treat link_url as relative URI. + auto end = std::find(std::begin(uri), std::end(uri), '#'); + auto q = std::find(std::begin(uri), end, '?'); + + rel = StringRef{std::begin(uri), q}; + if (q != end) { + relq = StringRef{q + 1, std::end(uri)}; + } + } else { + if (u.field_set & (1 << UF_SCHEMA)) { + scheme = util::get_uri_field(uri.c_str(), u, UF_SCHEMA); + } + + if (u.field_set & (1 << UF_HOST)) { + auto auth = util::get_uri_field(uri.c_str(), u, UF_HOST); + auto len = auth.size(); + auto port_exists = u.field_set & (1 << UF_PORT); + if (port_exists) { + len += 1 + str_size("65535"); + } + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + p = std::copy(std::begin(auth), std::end(auth), p); + if (port_exists) { + *p++ = ':'; + p = util::utos(p, u.port); + } + *p = '\0'; + + authority = StringRef{iov.base, p}; + } + + if (u.field_set & (1 << UF_PATH)) { + auto &f = u.field_data[UF_PATH]; + rel = StringRef{uri.c_str() + f.off, f.len}; + } else { + rel = StringRef::from_lit("/"); + } + + if (u.field_set & (1 << UF_QUERY)) { + auto &f = u.field_data[UF_QUERY]; + relq = StringRef{uri.c_str() + f.off, f.len}; + } + } + + path = http2::path_join(balloc, base, StringRef{}, rel, relq); + + return 0; +} + +namespace { +template <typename InputIt> InputIt eat_file(InputIt first, InputIt last) { + if (first == last) { + *first++ = '/'; + return first; + } + + if (*(last - 1) == '/') { + return last; + } + + auto p = last; + for (; p != first && *(p - 1) != '/'; --p) + ; + if (p == first) { + // this should not happened in normal case, where we expect path + // starts with '/' + *first++ = '/'; + return first; + } + + return p; +} +} // namespace + +namespace { +template <typename InputIt> InputIt eat_dir(InputIt first, InputIt last) { + auto p = eat_file(first, last); + + --p; + + assert(*p == '/'); + + return eat_file(first, p); +} +} // namespace + +StringRef path_join(BlockAllocator &balloc, const StringRef &base_path, + const StringRef &base_query, const StringRef &rel_path, + const StringRef &rel_query) { + auto res = make_byte_ref( + balloc, std::max(static_cast<size_t>(1), base_path.size()) + + rel_path.size() + 1 + + std::max(base_query.size(), rel_query.size()) + 1); + auto p = res.base; + + if (rel_path.empty()) { + if (base_path.empty()) { + *p++ = '/'; + } else { + p = std::copy(std::begin(base_path), std::end(base_path), p); + } + if (rel_query.empty()) { + if (!base_query.empty()) { + *p++ = '?'; + p = std::copy(std::begin(base_query), std::end(base_query), p); + } + *p = '\0'; + return StringRef{res.base, p}; + } + *p++ = '?'; + p = std::copy(std::begin(rel_query), std::end(rel_query), p); + *p = '\0'; + return StringRef{res.base, p}; + } + + auto first = std::begin(rel_path); + auto last = std::end(rel_path); + + if (rel_path[0] == '/') { + *p++ = '/'; + ++first; + for (; first != last && *first == '/'; ++first) + ; + } else if (base_path.empty()) { + *p++ = '/'; + } else { + p = std::copy(std::begin(base_path), std::end(base_path), p); + } + + for (; first != last;) { + if (*first == '.') { + if (first + 1 == last) { + if (*(p - 1) != '/') { + p = eat_file(res.base, p); + } + break; + } + if (*(first + 1) == '/') { + if (*(p - 1) != '/') { + p = eat_file(res.base, p); + } + first += 2; + continue; + } + if (*(first + 1) == '.') { + if (first + 2 == last) { + p = eat_dir(res.base, p); + break; + } + if (*(first + 2) == '/') { + p = eat_dir(res.base, p); + first += 3; + continue; + } + } + } + if (*(p - 1) != '/') { + p = eat_file(res.base, p); + } + auto slash = std::find(first, last, '/'); + if (slash == last) { + p = std::copy(first, last, p); + break; + } + p = std::copy(first, slash + 1, p); + first = slash + 1; + for (; first != last && *first == '/'; ++first) + ; + } + if (!rel_query.empty()) { + *p++ = '?'; + p = std::copy(std::begin(rel_query), std::end(rel_query), p); + } + *p = '\0'; + return StringRef{res.base, p}; +} + +StringRef normalize_path(BlockAllocator &balloc, const StringRef &path, + const StringRef &query) { + // First, decode %XX for unreserved characters, then do + // http2::path_join + + // We won't find %XX if length is less than 3. + if (path.size() < 3 || + std::find(std::begin(path), std::end(path), '%') == std::end(path)) { + return path_join(balloc, StringRef{}, StringRef{}, path, query); + } + + // includes last terminal NULL. + auto result = make_byte_ref(balloc, path.size() + 1); + auto p = result.base; + + auto it = std::begin(path); + for (; it + 2 < std::end(path);) { + if (*it == '%') { + if (util::is_hex_digit(*(it + 1)) && util::is_hex_digit(*(it + 2))) { + auto c = + (util::hex_to_uint(*(it + 1)) << 4) + util::hex_to_uint(*(it + 2)); + if (util::in_rfc3986_unreserved_chars(c)) { + *p++ = c; + + it += 3; + + continue; + } + *p++ = '%'; + *p++ = util::upcase(*(it + 1)); + *p++ = util::upcase(*(it + 2)); + + it += 3; + + continue; + } + } + *p++ = *it++; + } + + p = std::copy(it, std::end(path), p); + *p = '\0'; + + return path_join(balloc, StringRef{}, StringRef{}, StringRef{result.base, p}, + query); +} + +StringRef normalize_path_colon(BlockAllocator &balloc, const StringRef &path, + const StringRef &query) { + // First, decode %XX for unreserved characters and ':', then do + // http2::path_join + + // We won't find %XX if length is less than 3. + if (path.size() < 3 || + std::find(std::begin(path), std::end(path), '%') == std::end(path)) { + return path_join(balloc, StringRef{}, StringRef{}, path, query); + } + + // includes last terminal NULL. + auto result = make_byte_ref(balloc, path.size() + 1); + auto p = result.base; + + auto it = std::begin(path); + for (; it + 2 < std::end(path);) { + if (*it == '%') { + if (util::is_hex_digit(*(it + 1)) && util::is_hex_digit(*(it + 2))) { + auto c = + (util::hex_to_uint(*(it + 1)) << 4) + util::hex_to_uint(*(it + 2)); + if (util::in_rfc3986_unreserved_chars(c) || c == ':') { + *p++ = c; + + it += 3; + + continue; + } + *p++ = '%'; + *p++ = util::upcase(*(it + 1)); + *p++ = util::upcase(*(it + 2)); + + it += 3; + + continue; + } + } + *p++ = *it++; + } + + p = std::copy(it, std::end(path), p); + *p = '\0'; + + return path_join(balloc, StringRef{}, StringRef{}, StringRef{result.base, p}, + query); +} + +std::string normalize_path(const StringRef &path, const StringRef &query) { + BlockAllocator balloc(1024, 1024); + + return normalize_path(balloc, path, query).str(); +} + +StringRef rewrite_clean_path(BlockAllocator &balloc, const StringRef &src) { + if (src.empty() || src[0] != '/') { + return src; + } + // probably, not necessary most of the case, but just in case. + auto fragment = std::find(std::begin(src), std::end(src), '#'); + auto raw_query = std::find(std::begin(src), fragment, '?'); + auto query = raw_query; + if (query != fragment) { + ++query; + } + return normalize_path(balloc, StringRef{std::begin(src), raw_query}, + StringRef{query, fragment}); +} + +StringRef copy_lower(BlockAllocator &balloc, const StringRef &src) { + auto iov = make_byte_ref(balloc, src.size() + 1); + auto p = iov.base; + p = std::copy(std::begin(src), std::end(src), p); + *p = '\0'; + util::inp_strlower(iov.base, p); + return StringRef{iov.base, p}; +} + +bool contains_trailers(const StringRef &s) { + constexpr auto trailers = StringRef::from_lit("trailers"); + + for (auto p = std::begin(s), end = std::end(s);; ++p) { + p = std::find_if(p, end, [](char c) { return c != ' ' && c != '\t'; }); + if (p == end || static_cast<size_t>(end - p) < trailers.size()) { + return false; + } + if (util::strieq(trailers, StringRef{p, p + trailers.size()})) { + // Make sure that there is no character other than white spaces + // before next "," or end of string. + p = std::find_if(p + trailers.size(), end, + [](char c) { return c != ' ' && c != '\t'; }); + if (p == end || *p == ',') { + return true; + } + } + // Skip to next ",". + p = std::find_if(p, end, [](char c) { return c == ','; }); + if (p == end) { + return false; + } + } +} + +StringRef make_websocket_accept_token(uint8_t *dest, const StringRef &key) { + static constexpr uint8_t magic[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + std::array<uint8_t, base64::encode_length(16) + str_size(magic)> s; + auto p = std::copy(std::begin(key), std::end(key), std::begin(s)); + std::copy_n(magic, str_size(magic), p); + + std::array<uint8_t, 20> h; + if (util::sha1(h.data(), StringRef{std::begin(s), std::end(s)}) != 0) { + return StringRef{}; + } + + auto end = base64::encode(std::begin(h), std::end(h), dest); + return StringRef{dest, end}; +} + +bool legacy_http1(int major, int minor) { + return major <= 0 || (major == 1 && minor == 0); +} + +bool check_transfer_encoding(const StringRef &s) { + if (s.empty()) { + return false; + } + + auto it = std::begin(s); + + for (;;) { + // token + if (!util::in_token(*it)) { + return false; + } + + ++it; + + for (; it != std::end(s) && util::in_token(*it); ++it) + ; + + if (it == std::end(s)) { + return true; + } + + for (;;) { + // OWS + it = skip_lws(it, std::end(s)); + if (it == std::end(s)) { + return false; + } + + if (*it == ',') { + ++it; + + it = skip_lws(it, std::end(s)); + if (it == std::end(s)) { + return false; + } + + break; + } + + if (*it != ';') { + return false; + } + + ++it; + + // transfer-parameter follows + + // OWS + it = skip_lws(it, std::end(s)); + if (it == std::end(s)) { + return false; + } + + // token + if (!util::in_token(*it)) { + return false; + } + + ++it; + + for (; it != std::end(s) && util::in_token(*it); ++it) + ; + + if (it == std::end(s)) { + return false; + } + + // No BWS allowed + if (*it != '=') { + return false; + } + + ++it; + + if (util::in_token(*it)) { + // token + ++it; + + for (; it != std::end(s) && util::in_token(*it); ++it) + ; + } else if (*it == '"') { + // quoted-string + ++it; + + it = skip_to_right_dquote(it, std::end(s)); + if (it == std::end(s)) { + return false; + } + + ++it; + } else { + return false; + } + + if (it == std::end(s)) { + return true; + } + } + } +} + +} // namespace http2 + +} // namespace nghttp2 diff --git a/src/http2.h b/src/http2.h new file mode 100644 index 0000000..7cfe461 --- /dev/null +++ b/src/http2.h @@ -0,0 +1,457 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP2_H +#define HTTP2_H + +#include "nghttp2_config.h" + +#include <cstdio> +#include <cstring> +#include <string> +#include <vector> +#include <array> + +#include <nghttp2/nghttp2.h> + +#include "url-parser/url_parser.h" + +#include "util.h" +#include "memchunk.h" +#include "template.h" +#include "allocator.h" +#include "base64.h" + +namespace nghttp2 { + +struct Header { + Header(std::string name, std::string value, bool no_index = false, + int32_t token = -1) + : name(std::move(name)), + value(std::move(value)), + token(token), + no_index(no_index) {} + + Header() : token(-1), no_index(false) {} + + bool operator==(const Header &other) const { + return name == other.name && value == other.value; + } + + bool operator<(const Header &rhs) const { + return name < rhs.name || (name == rhs.name && value < rhs.value); + } + + std::string name; + std::string value; + int32_t token; + bool no_index; +}; + +struct HeaderRef { + HeaderRef(const StringRef &name, const StringRef &value, + bool no_index = false, int32_t token = -1) + : name(name), value(value), token(token), no_index(no_index) {} + + HeaderRef() : token(-1), no_index(false) {} + + bool operator==(const HeaderRef &other) const { + return name == other.name && value == other.value; + } + + bool operator<(const HeaderRef &rhs) const { + return name < rhs.name || (name == rhs.name && value < rhs.value); + } + + StringRef name; + StringRef value; + int32_t token; + bool no_index; +}; + +using Headers = std::vector<Header>; +using HeaderRefs = std::vector<HeaderRef>; + +namespace http2 { + +// Returns reason-phrase for given |status code|. If there is no +// known reason-phrase for the given code, returns empty string. +StringRef get_reason_phrase(unsigned int status_code); + +// Returns string version of |status_code|. (e.g., "404") +StringRef stringify_status(BlockAllocator &balloc, unsigned int status_code); + +void capitalize(DefaultMemchunks *buf, const StringRef &s); + +// Returns true if |value| is LWS +bool lws(const char *value); + +// Copies the |field| component value from |u| and |url| to the +// |dest|. If |u| does not have |field|, then this function does +// nothing. +void copy_url_component(std::string &dest, const http_parser_url *u, int field, + const char *url); + +Headers::value_type to_header(const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, + bool no_index, int32_t token); + +// Add name/value pairs to |nva|. If |no_index| is true, this +// name/value pair won't be indexed when it is forwarded to the next +// hop. This function strips white spaces around |value|. +void add_header(Headers &nva, const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, bool no_index, + int32_t token); + +// Returns pointer to the entry in |nva| which has name |name|. If +// more than one entries which have the name |name|, last occurrence +// in |nva| is returned. If no such entry exist, returns nullptr. +const Headers::value_type *get_header(const Headers &nva, const char *name); + +// Returns true if the value of |nv| is not empty. +bool non_empty_value(const HeaderRefs::value_type *nv); + +// Creates nghttp2_nv using |name| and |value| and returns it. The +// returned value only references the data pointer to name.c_str() and +// value.c_str(). If |no_index| is true, nghttp2_nv flags member has +// NGHTTP2_NV_FLAG_NO_INDEX flag set. +nghttp2_nv make_nv(const std::string &name, const std::string &value, + bool no_index = false); + +nghttp2_nv make_nv(const StringRef &name, const StringRef &value, + bool no_index = false); + +nghttp2_nv make_nv_nocopy(const std::string &name, const std::string &value, + bool no_index = false); + +nghttp2_nv make_nv_nocopy(const StringRef &name, const StringRef &value, + bool no_index = false); + +// Create nghttp2_nv from string literal |name| and |value|. +template <size_t N, size_t M> +constexpr nghttp2_nv make_nv_ll(const char (&name)[N], const char (&value)[M]) { + return {(uint8_t *)name, (uint8_t *)value, N - 1, M - 1, + NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE}; +} + +// Create nghttp2_nv from string literal |name| and c-string |value|. +template <size_t N> +nghttp2_nv make_nv_lc(const char (&name)[N], const char *value) { + return {(uint8_t *)name, (uint8_t *)value, N - 1, strlen(value), + NGHTTP2_NV_FLAG_NO_COPY_NAME}; +} + +template <size_t N> +nghttp2_nv make_nv_lc_nocopy(const char (&name)[N], const char *value) { + return {(uint8_t *)name, (uint8_t *)value, N - 1, strlen(value), + NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE}; +} + +// Create nghttp2_nv from string literal |name| and std::string +// |value|. +template <size_t N> +nghttp2_nv make_nv_ls(const char (&name)[N], const std::string &value) { + return {(uint8_t *)name, (uint8_t *)value.c_str(), N - 1, value.size(), + NGHTTP2_NV_FLAG_NO_COPY_NAME}; +} + +template <size_t N> +nghttp2_nv make_nv_ls_nocopy(const char (&name)[N], const std::string &value) { + return {(uint8_t *)name, (uint8_t *)value.c_str(), N - 1, value.size(), + NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE}; +} + +template <size_t N> +nghttp2_nv make_nv_ls_nocopy(const char (&name)[N], const StringRef &value) { + return {(uint8_t *)name, (uint8_t *)value.c_str(), N - 1, value.size(), + NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE}; +} + +enum HeaderBuildOp { + HDOP_NONE, + // Forwarded header fields must be stripped. If this flag is not + // set, all Forwarded header fields other than last one are added. + HDOP_STRIP_FORWARDED = 1, + // X-Forwarded-For header fields must be stripped. If this flag is + // not set, all X-Forwarded-For header fields other than last one + // are added. + HDOP_STRIP_X_FORWARDED_FOR = 1 << 1, + // X-Forwarded-Proto header fields must be stripped. If this flag + // is not set, all X-Forwarded-Proto header fields other than last + // one are added. + HDOP_STRIP_X_FORWARDED_PROTO = 1 << 2, + // Via header fields must be stripped. If this flag is not set, all + // Via header fields other than last one are added. + HDOP_STRIP_VIA = 1 << 3, + // Early-Data header fields must be stripped. If this flag is not + // set, all Early-Data header fields are added. + HDOP_STRIP_EARLY_DATA = 1 << 4, + // Strip above all header fields. + HDOP_STRIP_ALL = HDOP_STRIP_FORWARDED | HDOP_STRIP_X_FORWARDED_FOR | + HDOP_STRIP_X_FORWARDED_PROTO | HDOP_STRIP_VIA | + HDOP_STRIP_EARLY_DATA, + // Sec-WebSocket-Accept header field must be stripped. If this flag + // is not set, all Sec-WebSocket-Accept header fields are added. + HDOP_STRIP_SEC_WEBSOCKET_ACCEPT = 1 << 5, + // Sec-WebSocket-Key header field must be stripped. If this flag is + // not set, all Sec-WebSocket-Key header fields are added. + HDOP_STRIP_SEC_WEBSOCKET_KEY = 1 << 6, + // Transfer-Encoding header field must be stripped. If this flag is + // not set, all Transfer-Encoding header fields are added. + HDOP_STRIP_TRANSFER_ENCODING = 1 << 7, +}; + +// Appends headers in |headers| to |nv|. |headers| must be indexed +// before this call (its element's token field is assigned). Certain +// headers, including disallowed headers in HTTP/2 spec and headers +// which require special handling (i.e. via), are not copied. |flags| +// is one or more of HeaderBuildOp flags. They tell function that +// certain header fields should not be added. +void copy_headers_to_nva(std::vector<nghttp2_nv> &nva, + const HeaderRefs &headers, uint32_t flags); + +// Just like copy_headers_to_nva(), but this adds +// NGHTTP2_NV_FLAG_NO_COPY_NAME and NGHTTP2_NV_FLAG_NO_COPY_VALUE. +void copy_headers_to_nva_nocopy(std::vector<nghttp2_nv> &nva, + const HeaderRefs &headers, uint32_t flags); + +// Appends HTTP/1.1 style header lines to |buf| from headers in +// |headers|. |headers| must be indexed before this call (its +// element's token field is assigned). Certain headers, which +// requires special handling (i.e. via and cookie), are not appended. +// |flags| is one or more of HeaderBuildOp flags. They tell function +// that certain header fields should not be added. +void build_http1_headers_from_headers(DefaultMemchunks *buf, + const HeaderRefs &headers, + uint32_t flags); + +// Return positive window_size_increment if WINDOW_UPDATE should be +// sent for the stream |stream_id|. If |stream_id| == 0, this function +// determines the necessity of the WINDOW_UPDATE for a connection. +// +// If the function determines WINDOW_UPDATE is not necessary at the +// moment, it returns -1. +int32_t determine_window_update_transmission(nghttp2_session *session, + int32_t stream_id); + +// Dumps name/value pairs in |nv| to |out|. The |nv| must be +// terminated by nullptr. +void dump_nv(FILE *out, const char **nv); + +// Dumps name/value pairs in |nva| to |out|. +void dump_nv(FILE *out, const nghttp2_nv *nva, size_t nvlen); + +// Dumps name/value pairs in |nva| to |out|. +void dump_nv(FILE *out, const Headers &nva); + +void dump_nv(FILE *out, const HeaderRefs &nva); + +// Ereases header in |hd|. +void erase_header(HeaderRef *hd); + +// Rewrites redirection URI which usually appears in location header +// field. The |uri| is the URI in the location header field. The |u| +// stores the result of parsed |uri|. The |request_authority| is the +// host or :authority header field value in the request. The +// |upstream_scheme| is either "https" or "http" in the upstream +// interface. Rewrite is done only if location header field value +// contains |match_host| as host excluding port. The |match_host| and +// |request_authority| could be different. If |request_authority| is +// empty, strip authority. +// +// This function returns the new rewritten URI on success. If the +// location URI is not subject to the rewrite, this function returns +// empty string. +StringRef rewrite_location_uri(BlockAllocator &balloc, const StringRef &uri, + const http_parser_url &u, + const StringRef &match_host, + const StringRef &request_authority, + const StringRef &upstream_scheme); + +// Returns parsed HTTP status code. Returns -1 on failure. +int parse_http_status_code(const StringRef &src); + +// Header fields to be indexed, except HD_MAXIDX which is convenient +// member to get maximum value. +// +// generated by genheaderfunc.py +enum { + HD__AUTHORITY, + HD__HOST, + HD__METHOD, + HD__PATH, + HD__PROTOCOL, + HD__SCHEME, + HD__STATUS, + HD_ACCEPT_ENCODING, + HD_ACCEPT_LANGUAGE, + HD_ALT_SVC, + HD_CACHE_CONTROL, + HD_CONNECTION, + HD_CONTENT_LENGTH, + HD_CONTENT_TYPE, + HD_COOKIE, + HD_DATE, + HD_EARLY_DATA, + HD_EXPECT, + HD_FORWARDED, + HD_HOST, + HD_HTTP2_SETTINGS, + HD_IF_MODIFIED_SINCE, + HD_KEEP_ALIVE, + HD_LINK, + HD_LOCATION, + HD_PRIORITY, + HD_PROXY_CONNECTION, + HD_SEC_WEBSOCKET_ACCEPT, + HD_SEC_WEBSOCKET_KEY, + HD_SERVER, + HD_TE, + HD_TRAILER, + HD_TRANSFER_ENCODING, + HD_UPGRADE, + HD_USER_AGENT, + HD_VIA, + HD_X_FORWARDED_FOR, + HD_X_FORWARDED_PROTO, + HD_MAXIDX, +}; + +using HeaderIndex = std::array<int16_t, HD_MAXIDX>; + +// Looks up header token for header name |name| of length |namelen|. +// Only headers we are interested in are tokenized. If header name +// cannot be tokenized, returns -1. +int lookup_token(const uint8_t *name, size_t namelen); +int lookup_token(const StringRef &name); + +// Initializes |hdidx|, header index. The |hdidx| must point to the +// array containing at least HD_MAXIDX elements. +void init_hdidx(HeaderIndex &hdidx); +// Indexes header |token| using index |idx|. +void index_header(HeaderIndex &hdidx, int32_t token, size_t idx); + +// Returns header denoted by |token| using index |hdidx|. +const Headers::value_type *get_header(const HeaderIndex &hdidx, int32_t token, + const Headers &nva); + +Headers::value_type *get_header(const HeaderIndex &hdidx, int32_t token, + Headers &nva); + +struct LinkHeader { + // The region of URI. This might not be NULL-terminated. + StringRef uri; +}; + +// Returns next URI-reference in Link header field value |src|. If no +// URI-reference found after searching all input, returned uri field +// is empty. This imply that empty URI-reference is ignored during +// parsing. +std::vector<LinkHeader> parse_link_header(const StringRef &src); + +// Constructs path by combining base path |base_path| with another +// path |rel_path|. The base path and another path can have optional +// query component. This function assumes |base_path| is normalized. +// In other words, it does not contain ".." or "." path components +// and starts with "/" if it is not empty. +std::string path_join(const StringRef &base, const StringRef &base_query, + const StringRef &rel_path, const StringRef &rel_query); + +StringRef path_join(BlockAllocator &balloc, const StringRef &base_path, + const StringRef &base_query, const StringRef &rel_path, + const StringRef &rel_query); + +// true if response has body, taking into account the request method +// and status code. +bool expect_response_body(const std::string &method, int status_code); +bool expect_response_body(int method_token, int status_code); + +// true if response has body, taking into account status code only. +bool expect_response_body(int status_code); + +// Looks up method token for method name |name| of length |namelen|. +// Only methods defined in llhttp.h (llhttp_method) are tokenized. If +// method name cannot be tokenized, returns -1. +int lookup_method_token(const uint8_t *name, size_t namelen); +int lookup_method_token(const StringRef &name); + +// Returns string representation of |method_token|. This is wrapper +// around llhttp_method_name from llhttp. If |method_token| is +// unknown, program aborts. The returned StringRef is guaranteed to +// be NULL-terminated. +StringRef to_method_string(int method_token); + +StringRef normalize_path(BlockAllocator &balloc, const StringRef &path, + const StringRef &query); + +// normalize_path_colon is like normalize_path, but it additionally +// does percent-decoding %3A in order to workaround the issue that ':' +// cannot be included in backend pattern. +StringRef normalize_path_colon(BlockAllocator &balloc, const StringRef &path, + const StringRef &query); + +std::string normalize_path(const StringRef &path, const StringRef &query); + +StringRef rewrite_clean_path(BlockAllocator &balloc, const StringRef &src); + +// Returns path component of |uri|. The returned path does not +// include query component. This function returns empty string if it +// fails. +StringRef get_pure_path_component(const StringRef &uri); + +// Deduces scheme, authority and path from given |uri|, and stores +// them in |scheme|, |authority|, and |path| respectively. If |uri| +// is relative path, path resolution takes place using path given in +// |base| of length |baselen|. This function returns 0 if it +// succeeds, or -1. +int construct_push_component(BlockAllocator &balloc, StringRef &scheme, + StringRef &authority, StringRef &path, + const StringRef &base, const StringRef &uri); + +// Copies |src| and return its lower-cased version. +StringRef copy_lower(BlockAllocator &balloc, const StringRef &src); + +// Returns true if te header field value |s| contains "trailers". +bool contains_trailers(const StringRef &s); + +// Creates Sec-WebSocket-Accept value for |key|. The capacity of +// buffer pointed by |dest| must have at least 24 bytes (base64 +// encoded length of 16 bytes data). It returns empty string in case +// of error. +StringRef make_websocket_accept_token(uint8_t *dest, const StringRef &key); + +// Returns true if HTTP version represents pre-HTTP/1.1 (e.g., +// HTTP/0.9 or HTTP/1.0). +bool legacy_http1(int major, int minor); + +// Returns true if transfer-encoding field value |s| conforms RFC +// strictly. This function does not allow empty value, BWS, and empty +// list elements. +bool check_transfer_encoding(const StringRef &s); + +} // namespace http2 + +} // namespace nghttp2 + +#endif // HTTP2_H diff --git a/src/http2_test.cc b/src/http2_test.cc new file mode 100644 index 0000000..f8be9f4 --- /dev/null +++ b/src/http2_test.cc @@ -0,0 +1,1249 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http2_test.h" + +#include <cassert> +#include <cstring> +#include <iostream> + +#include <CUnit/CUnit.h> + +#include "url-parser/url_parser.h" + +#include "http2.h" +#include "util.h" + +using namespace nghttp2; + +#define MAKE_NV(K, V) \ + { \ + (uint8_t *)K, (uint8_t *)V, sizeof(K) - 1, sizeof(V) - 1, \ + NGHTTP2_NV_FLAG_NONE \ + } + +namespace shrpx { + +namespace { +void check_nv(const HeaderRef &a, const nghttp2_nv *b) { + CU_ASSERT(a.name.size() == b->namelen); + CU_ASSERT(a.value.size() == b->valuelen); + CU_ASSERT(memcmp(a.name.c_str(), b->name, b->namelen) == 0); + CU_ASSERT(memcmp(a.value.c_str(), b->value, b->valuelen) == 0); +} +} // namespace + +void test_http2_add_header(void) { + auto nva = Headers(); + + http2::add_header(nva, (const uint8_t *)"alpha", 5, (const uint8_t *)"123", 3, + false, -1); + CU_ASSERT(Headers::value_type("alpha", "123") == nva[0]); + CU_ASSERT(!nva[0].no_index); + + nva.clear(); + + http2::add_header(nva, (const uint8_t *)"alpha", 5, (const uint8_t *)"", 0, + true, -1); + CU_ASSERT(Headers::value_type("alpha", "") == nva[0]); + CU_ASSERT(nva[0].no_index); + + nva.clear(); + + http2::add_header(nva, (const uint8_t *)"a", 1, (const uint8_t *)" b", 2, + false, -1); + CU_ASSERT(Headers::value_type("a", "b") == nva[0]); + + nva.clear(); + + http2::add_header(nva, (const uint8_t *)"a", 1, (const uint8_t *)"b ", 2, + false, -1); + CU_ASSERT(Headers::value_type("a", "b") == nva[0]); + + nva.clear(); + + http2::add_header(nva, (const uint8_t *)"a", 1, (const uint8_t *)" b ", 5, + false, -1); + CU_ASSERT(Headers::value_type("a", "b") == nva[0]); + + nva.clear(); + + http2::add_header(nva, (const uint8_t *)"a", 1, (const uint8_t *)" bravo ", + 9, false, -1); + CU_ASSERT(Headers::value_type("a", "bravo") == nva[0]); + + nva.clear(); + + http2::add_header(nva, (const uint8_t *)"a", 1, (const uint8_t *)" ", 4, + false, -1); + CU_ASSERT(Headers::value_type("a", "") == nva[0]); + + nva.clear(); + + http2::add_header(nva, (const uint8_t *)"te", 2, (const uint8_t *)"trailers", + 8, false, http2::HD_TE); + CU_ASSERT(http2::HD_TE == nva[0].token); +} + +void test_http2_get_header(void) { + auto nva = Headers{{"alpha", "1"}, {"bravo", "2"}, {"bravo", "3"}, + {"charlie", "4"}, {"delta", "5"}, {"echo", "6"}, + {"content-length", "7"}}; + const Headers::value_type *rv; + rv = http2::get_header(nva, "delta"); + CU_ASSERT(rv != nullptr); + CU_ASSERT("delta" == rv->name); + + rv = http2::get_header(nva, "bravo"); + CU_ASSERT(rv != nullptr); + CU_ASSERT("bravo" == rv->name); + + rv = http2::get_header(nva, "foxtrot"); + CU_ASSERT(rv == nullptr); + + http2::HeaderIndex hdidx; + http2::init_hdidx(hdidx); + hdidx[http2::HD_CONTENT_LENGTH] = 6; + rv = http2::get_header(hdidx, http2::HD_CONTENT_LENGTH, nva); + CU_ASSERT("content-length" == rv->name); +} + +namespace { +auto headers = HeaderRefs{ + {StringRef::from_lit("alpha"), StringRef::from_lit("0"), true}, + {StringRef::from_lit("bravo"), StringRef::from_lit("1")}, + {StringRef::from_lit("connection"), StringRef::from_lit("2"), false, + http2::HD_CONNECTION}, + {StringRef::from_lit("connection"), StringRef::from_lit("3"), false, + http2::HD_CONNECTION}, + {StringRef::from_lit("delta"), StringRef::from_lit("4")}, + {StringRef::from_lit("expect"), StringRef::from_lit("5")}, + {StringRef::from_lit("foxtrot"), StringRef::from_lit("6")}, + {StringRef::from_lit("tango"), StringRef::from_lit("7")}, + {StringRef::from_lit("te"), StringRef::from_lit("8"), false, http2::HD_TE}, + {StringRef::from_lit("te"), StringRef::from_lit("9"), false, http2::HD_TE}, + {StringRef::from_lit("x-forwarded-proto"), StringRef::from_lit("10"), false, + http2::HD_X_FORWARDED_FOR}, + {StringRef::from_lit("x-forwarded-proto"), StringRef::from_lit("11"), false, + http2::HD_X_FORWARDED_FOR}, + {StringRef::from_lit("zulu"), StringRef::from_lit("12")}}; +} // namespace + +namespace { +auto headers2 = HeaderRefs{ + {StringRef::from_lit("x-forwarded-for"), StringRef::from_lit("xff1"), false, + http2::HD_X_FORWARDED_FOR}, + {StringRef::from_lit("x-forwarded-for"), StringRef::from_lit("xff2"), false, + http2::HD_X_FORWARDED_FOR}, + {StringRef::from_lit("x-forwarded-proto"), StringRef::from_lit("xfp1"), + false, http2::HD_X_FORWARDED_PROTO}, + {StringRef::from_lit("x-forwarded-proto"), StringRef::from_lit("xfp2"), + false, http2::HD_X_FORWARDED_PROTO}, + {StringRef::from_lit("forwarded"), StringRef::from_lit("fwd1"), false, + http2::HD_FORWARDED}, + {StringRef::from_lit("forwarded"), StringRef::from_lit("fwd2"), false, + http2::HD_FORWARDED}, + {StringRef::from_lit("via"), StringRef::from_lit("via1"), false, + http2::HD_VIA}, + {StringRef::from_lit("via"), StringRef::from_lit("via2"), false, + http2::HD_VIA}, +}; +} // namespace + +void test_http2_copy_headers_to_nva(void) { + auto ans = std::vector<int>{0, 1, 4, 5, 6, 7, 12}; + std::vector<nghttp2_nv> nva; + + http2::copy_headers_to_nva_nocopy(nva, headers, + http2::HDOP_STRIP_X_FORWARDED_FOR); + CU_ASSERT(7 == nva.size()); + for (size_t i = 0; i < ans.size(); ++i) { + check_nv(headers[ans[i]], &nva[i]); + + if (ans[i] == 0) { + CU_ASSERT((NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE | + NGHTTP2_NV_FLAG_NO_INDEX) == nva[i].flags); + } else { + CU_ASSERT((NGHTTP2_NV_FLAG_NO_COPY_NAME | + NGHTTP2_NV_FLAG_NO_COPY_VALUE) == nva[i].flags); + } + } + + nva.clear(); + http2::copy_headers_to_nva(nva, headers, http2::HDOP_STRIP_X_FORWARDED_FOR); + CU_ASSERT(7 == nva.size()); + for (size_t i = 0; i < ans.size(); ++i) { + check_nv(headers[ans[i]], &nva[i]); + + if (ans[i] == 0) { + CU_ASSERT(nva[i].flags & NGHTTP2_NV_FLAG_NO_INDEX); + } else { + CU_ASSERT(NGHTTP2_NV_FLAG_NONE == nva[i].flags); + } + } + + nva.clear(); + + auto ans2 = std::vector<int>{0, 2, 4, 6}; + http2::copy_headers_to_nva(nva, headers2, http2::HDOP_NONE); + CU_ASSERT(ans2.size() == nva.size()); + for (size_t i = 0; i < ans2.size(); ++i) { + check_nv(headers2[ans2[i]], &nva[i]); + } + + nva.clear(); + + http2::copy_headers_to_nva(nva, headers2, http2::HDOP_STRIP_ALL); + CU_ASSERT(nva.empty()); +} + +void test_http2_build_http1_headers_from_headers(void) { + MemchunkPool pool; + DefaultMemchunks buf(&pool); + http2::build_http1_headers_from_headers(&buf, headers, + http2::HDOP_STRIP_X_FORWARDED_FOR); + auto hdrs = std::string(buf.head->pos, buf.head->last); + CU_ASSERT("Alpha: 0\r\n" + "Bravo: 1\r\n" + "Delta: 4\r\n" + "Expect: 5\r\n" + "Foxtrot: 6\r\n" + "Tango: 7\r\n" + "Te: 8\r\n" + "Te: 9\r\n" + "Zulu: 12\r\n" == hdrs); + + buf.reset(); + + http2::build_http1_headers_from_headers(&buf, headers2, http2::HDOP_NONE); + hdrs = std::string(buf.head->pos, buf.head->last); + CU_ASSERT("X-Forwarded-For: xff1\r\n" + "X-Forwarded-Proto: xfp1\r\n" + "Forwarded: fwd1\r\n" + "Via: via1\r\n" == hdrs); + + buf.reset(); + + http2::build_http1_headers_from_headers(&buf, headers2, + http2::HDOP_STRIP_ALL); + CU_ASSERT(0 == buf.rleft()); +} + +void test_http2_lws(void) { + CU_ASSERT(!http2::lws("alpha")); + CU_ASSERT(http2::lws(" ")); + CU_ASSERT(http2::lws("")); +} + +namespace { +void check_rewrite_location_uri(const std::string &want, const std::string &uri, + const std::string &match_host, + const std::string &req_authority, + const std::string &upstream_scheme) { + BlockAllocator balloc(4096, 4096); + http_parser_url u{}; + CU_ASSERT(0 == http_parser_parse_url(uri.c_str(), uri.size(), 0, &u)); + auto got = http2::rewrite_location_uri( + balloc, StringRef{uri}, u, StringRef{match_host}, + StringRef{req_authority}, StringRef{upstream_scheme}); + CU_ASSERT(want == got); +} +} // namespace + +void test_http2_rewrite_location_uri(void) { + check_rewrite_location_uri("https://localhost:3000/alpha?bravo#charlie", + "http://localhost:3001/alpha?bravo#charlie", + "localhost:3001", "localhost:3000", "https"); + check_rewrite_location_uri("https://localhost/", "http://localhost:3001/", + "localhost", "localhost", "https"); + check_rewrite_location_uri("http://localhost/", "http://localhost:3001/", + "localhost", "localhost", "http"); + check_rewrite_location_uri("http://localhost:443/", "http://localhost:3001/", + "localhost", "localhost:443", "http"); + check_rewrite_location_uri("https://localhost:80/", "http://localhost:3001/", + "localhost", "localhost:80", "https"); + check_rewrite_location_uri("", "http://localhost:3001/", "127.0.0.1", + "127.0.0.1", "https"); + check_rewrite_location_uri("https://localhost:3000/", + "http://localhost:3001/", "localhost", + "localhost:3000", "https"); + check_rewrite_location_uri("https://localhost:3000/", "http://localhost/", + "localhost", "localhost:3000", "https"); + + // match_host != req_authority + check_rewrite_location_uri("https://example.org", "http://127.0.0.1:8080", + "127.0.0.1", "example.org", "https"); + check_rewrite_location_uri("", "http://example.org", "127.0.0.1", + "example.org", "https"); +} + +void test_http2_parse_http_status_code(void) { + CU_ASSERT(200 == http2::parse_http_status_code(StringRef::from_lit("200"))); + CU_ASSERT(102 == http2::parse_http_status_code(StringRef::from_lit("102"))); + CU_ASSERT(-1 == http2::parse_http_status_code(StringRef::from_lit("099"))); + CU_ASSERT(-1 == http2::parse_http_status_code(StringRef::from_lit("99"))); + CU_ASSERT(-1 == http2::parse_http_status_code(StringRef::from_lit("-1"))); + CU_ASSERT(-1 == http2::parse_http_status_code(StringRef::from_lit("20a"))); + CU_ASSERT(-1 == http2::parse_http_status_code(StringRef{})); +} + +void test_http2_index_header(void) { + http2::HeaderIndex hdidx; + http2::init_hdidx(hdidx); + + http2::index_header(hdidx, http2::HD__AUTHORITY, 0); + http2::index_header(hdidx, -1, 1); + + CU_ASSERT(0 == hdidx[http2::HD__AUTHORITY]); +} + +void test_http2_lookup_token(void) { + CU_ASSERT(http2::HD__AUTHORITY == + http2::lookup_token(StringRef::from_lit(":authority"))); + CU_ASSERT(-1 == http2::lookup_token(StringRef::from_lit(":authorit"))); + CU_ASSERT(-1 == http2::lookup_token(StringRef::from_lit(":Authority"))); + CU_ASSERT(http2::HD_EXPECT == + http2::lookup_token(StringRef::from_lit("expect"))); +} + +void test_http2_parse_link_header(void) { + { + // only URI appears; we don't extract URI unless it bears rel=preload + auto res = http2::parse_link_header(StringRef::from_lit("<url>")); + CU_ASSERT(0 == res.size()); + } + { + // URI url should be extracted + auto res = + http2::parse_link_header(StringRef::from_lit("<url>; rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // With extra link-param. URI url should be extracted + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; rel=preload; as=file")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // With extra link-param. URI url should be extracted + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; as=file; rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // With extra link-param and quote-string. URI url should be + // extracted + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=preload; title="foo,bar")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // With extra link-param and quote-string. URI url should be + // extracted + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; title="foo,bar"; rel=preload)")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // ',' after quote-string + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; title="foo,bar", <url2>; rel=preload)")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url2" == res[0].uri); + } + { + // Only first URI should be extracted. + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; rel=preload, <url2>")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // Both have rel=preload, so both urls should be extracted + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; rel=preload, <url2>; rel=preload")); + CU_ASSERT(2 == res.size()); + CU_ASSERT("url" == res[0].uri); + CU_ASSERT("url2" == res[1].uri); + } + { + // Second URI uri should be extracted. + auto res = http2::parse_link_header( + StringRef::from_lit("<url>, <url2>;rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url2" == res[0].uri); + } + { + // Error if input ends with ';' + auto res = + http2::parse_link_header(StringRef::from_lit("<url>;rel=preload;")); + CU_ASSERT(0 == res.size()); + } + { + // Error if link header ends with ';' + auto res = http2::parse_link_header( + StringRef::from_lit("<url>;rel=preload;, <url>")); + CU_ASSERT(0 == res.size()); + } + { + // OK if input ends with ',' + auto res = + http2::parse_link_header(StringRef::from_lit("<url>;rel=preload,")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // Multiple repeated ','s between fields is OK + auto res = http2::parse_link_header( + StringRef::from_lit("<url>,,,<url2>;rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url2" == res[0].uri); + } + { + // Error if url is not enclosed by <> + auto res = + http2::parse_link_header(StringRef::from_lit("url>;rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // Error if url is not enclosed by <> + auto res = + http2::parse_link_header(StringRef::from_lit("<url;rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // Empty parameter value is not allowed + auto res = + http2::parse_link_header(StringRef::from_lit("<url>;rel=preload; as=")); + CU_ASSERT(0 == res.size()); + } + { + // Empty parameter value is not allowed + auto res = + http2::parse_link_header(StringRef::from_lit("<url>;as=;rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // Empty parameter value is not allowed + auto res = http2::parse_link_header( + StringRef::from_lit("<url>;as=, <url>;rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // Empty parameter name is not allowed + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; =file; rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // Without whitespaces + auto res = http2::parse_link_header( + StringRef::from_lit("<url>;as=file;rel=preload,<url2>;rel=preload")); + CU_ASSERT(2 == res.size()); + CU_ASSERT("url" == res[0].uri); + CU_ASSERT("url2" == res[1].uri); + } + { + // link-extension may have no value + auto res = + http2::parse_link_header(StringRef::from_lit("<url>; as; rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // ext-name-star + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; foo*=bar; rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // '*' is not allowed expect for trailing one + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; *=bar; rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // '*' is not allowed expect for trailing one + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; foo*bar=buzz; rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // ext-name-star must be followed by '=' + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; foo*; rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // '>' is not followed by ';' + auto res = + http2::parse_link_header(StringRef::from_lit("<url> rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // Starting with whitespace is no problem. + auto res = + http2::parse_link_header(StringRef::from_lit(" <url>; rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload is a prefix of bogus rel parameter value + auto res = + http2::parse_link_header(StringRef::from_lit("<url>; rel=preloadx")); + CU_ASSERT(0 == res.size()); + } + { + // preload in relation-types list + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload in relation-types list followed by another parameter + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload foo")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload in relation-types list following another parameter + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="foo preload")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload in relation-types list between other parameters + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="foo preload bar")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload in relation-types list between other parameters + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="foo preload bar")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // no preload in relation-types list + auto res = + http2::parse_link_header(StringRef::from_lit(R"(<url>; rel="foo")")); + CU_ASSERT(0 == res.size()); + } + { + // no preload in relation-types list, multiple unrelated elements. + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="foo bar")")); + CU_ASSERT(0 == res.size()); + } + { + // preload in relation-types list, followed by another link-value. + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload", <url2>)")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload in relation-types list, following another link-value. + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>, <url2>; rel="preload")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url2" == res[0].uri); + } + { + // preload in relation-types list, followed by another link-param. + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload"; as="font")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload in relation-types list, followed by character other + // than ';' or ',' + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload".)")); + CU_ASSERT(0 == res.size()); + } + { + // preload in relation-types list, followed by ';' but it + // terminates input + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload";)")); + CU_ASSERT(0 == res.size()); + } + { + // preload in relation-types list, followed by ',' but it + // terminates input + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload",)")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // preload in relation-types list but there is preceding white + // space. + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=" preload")")); + CU_ASSERT(0 == res.size()); + } + { + // preload in relation-types list but there is trailing white + // space. + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel="preload ")")); + CU_ASSERT(0 == res.size()); + } + { + // backslash escaped characters in quoted-string + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=preload; title="foo\"baz\"bar")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // anchor="" is acceptable + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=preload; anchor="")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // With anchor="#foo", url should be ignored + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=preload; anchor="#foo")")); + CU_ASSERT(0 == res.size()); + } + { + // With anchor=f, url should be ignored + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; rel=preload; anchor=f")); + CU_ASSERT(0 == res.size()); + } + { + // First url is ignored With anchor="#foo", but url should be + // accepted. + auto res = http2::parse_link_header(StringRef::from_lit( + R"(<url>; rel=preload; anchor="#foo", <url2>; rel=preload)")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url2" == res[0].uri); + } + { + // With loadpolicy="next", url should be ignored + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=preload; loadpolicy="next")")); + CU_ASSERT(0 == res.size()); + } + { + // url should be picked up if empty loadpolicy is specified + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=preload; loadpolicy="")")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // case-insensitive match + auto res = http2::parse_link_header( + StringRef::from_lit(R"(<url>; rel=preload; ANCHOR="#foo", <url2>; )" + R"(REL=PRELOAD, <url3>; REL="foo PRELOAD bar")")); + CU_ASSERT(2 == res.size()); + CU_ASSERT("url2" == res[0].uri); + CU_ASSERT("url3" == res[1].uri); + } + { + // nopush at the end of input + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; rel=preload; nopush")); + CU_ASSERT(0 == res.size()); + } + { + // nopush followed by ';' + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; rel=preload; nopush; foo")); + CU_ASSERT(0 == res.size()); + } + { + // nopush followed by ',' + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; nopush; rel=preload")); + CU_ASSERT(0 == res.size()); + } + { + // string whose prefix is nopush + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; nopushyes; rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } + { + // rel=preload twice + auto res = http2::parse_link_header( + StringRef::from_lit("<url>; rel=preload; rel=preload")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("url" == res[0].uri); + } +} + +void test_http2_path_join(void) { + { + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("/"); + CU_ASSERT("/" == http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("/alpha"); + CU_ASSERT("/alpha" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // rel ends with trailing '/' + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("/alpha/"); + CU_ASSERT("/alpha/" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // rel contains multiple components + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("/alpha/bravo"); + CU_ASSERT("/alpha/bravo" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // rel is relative + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("alpha/bravo"); + CU_ASSERT("/alpha/bravo" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // rel is relative and base ends without /, which means it refers + // to file. + auto base = StringRef::from_lit("/alpha"); + auto rel = StringRef::from_lit("bravo/charlie"); + CU_ASSERT("/bravo/charlie" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // rel contains repeated '/'s + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("/alpha/////bravo/////"); + CU_ASSERT("/alpha/bravo/" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // base ends with '/', so '..' eats 'bravo' + auto base = StringRef::from_lit("/alpha/bravo/"); + auto rel = StringRef::from_lit("../charlie/delta"); + CU_ASSERT("/alpha/charlie/delta" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // base does not end with '/', so '..' eats 'alpha/bravo' + auto base = StringRef::from_lit("/alpha/bravo"); + auto rel = StringRef::from_lit("../charlie"); + CU_ASSERT("/charlie" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // 'charlie' is eaten by following '..' + auto base = StringRef::from_lit("/alpha/bravo/"); + auto rel = StringRef::from_lit("../charlie/../delta"); + CU_ASSERT("/alpha/delta" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // excessive '..' results in '/' + auto base = StringRef::from_lit("/alpha/bravo/"); + auto rel = StringRef::from_lit("../../../"); + CU_ASSERT("/" == http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // excessive '..' and path component + auto base = StringRef::from_lit("/alpha/bravo/"); + auto rel = StringRef::from_lit("../../../charlie"); + CU_ASSERT("/charlie" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // rel ends with '..' + auto base = StringRef::from_lit("/alpha/bravo/"); + auto rel = StringRef::from_lit("charlie/.."); + CU_ASSERT("/alpha/bravo/" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // base empty and rel contains '..' + auto base = StringRef{}; + auto rel = StringRef::from_lit("charlie/.."); + CU_ASSERT("/" == http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // '.' is ignored + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("charlie/././././delta"); + CU_ASSERT("/charlie/delta" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // trailing '.' is ignored + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("charlie/."); + CU_ASSERT("/charlie/" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // query + auto base = StringRef::from_lit("/"); + auto rel = StringRef::from_lit("/"); + auto relq = StringRef::from_lit("q"); + CU_ASSERT("/?q" == http2::path_join(base, StringRef{}, rel, relq)); + } + { + // empty rel and query + auto base = StringRef::from_lit("/alpha"); + auto rel = StringRef{}; + auto relq = StringRef::from_lit("q"); + CU_ASSERT("/alpha?q" == http2::path_join(base, StringRef{}, rel, relq)); + } + { + // both rel and query are empty + auto base = StringRef::from_lit("/alpha"); + auto baseq = StringRef::from_lit("r"); + auto rel = StringRef{}; + auto relq = StringRef{}; + CU_ASSERT("/alpha?r" == http2::path_join(base, baseq, rel, relq)); + } + { + // empty base + auto base = StringRef{}; + auto rel = StringRef::from_lit("/alpha"); + CU_ASSERT("/alpha" == + http2::path_join(base, StringRef{}, rel, StringRef{})); + } + { + // everything is empty + CU_ASSERT("/" == http2::path_join(StringRef{}, StringRef{}, StringRef{}, + StringRef{})); + } + { + // only baseq is not empty + auto base = StringRef{}; + auto baseq = StringRef::from_lit("r"); + auto rel = StringRef{}; + CU_ASSERT("/?r" == http2::path_join(base, baseq, rel, StringRef{})); + } + { + // path starts with multiple '/'s. + auto base = StringRef{}; + auto baseq = StringRef{}; + auto rel = StringRef::from_lit("//alpha//bravo"); + auto relq = StringRef::from_lit("charlie"); + CU_ASSERT("/alpha/bravo?charlie" == + http2::path_join(base, baseq, rel, relq)); + } + // Test cases from RFC 3986, section 5.4. + constexpr auto base = StringRef::from_lit("/b/c/d;p"); + constexpr auto baseq = StringRef::from_lit("q"); + { + auto rel = StringRef::from_lit("g"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("./g"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g/"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("/g"); + auto relq = StringRef{}; + CU_ASSERT("/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef{}; + auto relq = StringRef::from_lit("y"); + CU_ASSERT("/b/c/d;p?y" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g"); + auto relq = StringRef::from_lit("y"); + CU_ASSERT("/b/c/g?y" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit(";x"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/;x" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g;x"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g;x" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g;x"); + auto relq = StringRef::from_lit("y"); + CU_ASSERT("/b/c/g;x?y" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef{}; + auto relq = StringRef{}; + CU_ASSERT("/b/c/d;p?q" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("."); + auto relq = StringRef{}; + CU_ASSERT("/b/c/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("./"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit(".."); + auto relq = StringRef{}; + CU_ASSERT("/b/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("../"); + auto relq = StringRef{}; + CU_ASSERT("/b/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("../g"); + auto relq = StringRef{}; + CU_ASSERT("/b/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("../.."); + auto relq = StringRef{}; + CU_ASSERT("/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("../../"); + auto relq = StringRef{}; + CU_ASSERT("/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("../../g"); + auto relq = StringRef{}; + CU_ASSERT("/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("../../../g"); + auto relq = StringRef{}; + CU_ASSERT("/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("../../../../g"); + auto relq = StringRef{}; + CU_ASSERT("/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("/./g"); + auto relq = StringRef{}; + CU_ASSERT("/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("/../g"); + auto relq = StringRef{}; + CU_ASSERT("/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g."); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g." == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit(".g"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/.g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g.."); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g.." == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("..g"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/..g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("./../g"); + auto relq = StringRef{}; + CU_ASSERT("/b/g" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("./g/."); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g/" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g/./h"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g/h" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g/../h"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/h" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g;x=1/./y"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/g;x=1/y" == http2::path_join(base, baseq, rel, relq)); + } + { + auto rel = StringRef::from_lit("g;x=1/../y"); + auto relq = StringRef{}; + CU_ASSERT("/b/c/y" == http2::path_join(base, baseq, rel, relq)); + } +} + +void test_http2_normalize_path(void) { + CU_ASSERT("/alpha/charlie" == + http2::normalize_path( + StringRef::from_lit("/alpha/bravo/../charlie"), StringRef{})); + + CU_ASSERT("/alpha" == + http2::normalize_path(StringRef::from_lit("/a%6c%70%68%61"), + StringRef{})); + + CU_ASSERT( + "/alpha%2F%3A" == + http2::normalize_path(StringRef::from_lit("/alpha%2f%3a"), StringRef{})); + + CU_ASSERT("/%2F" == + http2::normalize_path(StringRef::from_lit("%2f"), StringRef{})); + + CU_ASSERT("/%f" == + http2::normalize_path(StringRef::from_lit("%f"), StringRef{})); + + CU_ASSERT("/%" == + http2::normalize_path(StringRef::from_lit("%"), StringRef{})); + + CU_ASSERT("/" == http2::normalize_path(StringRef{}, StringRef{})); + + CU_ASSERT("/alpha?bravo" == + http2::normalize_path(StringRef::from_lit("/alpha"), + StringRef::from_lit("bravo"))); +} + +void test_http2_rewrite_clean_path(void) { + BlockAllocator balloc(4096, 4096); + + // unreserved characters + CU_ASSERT("/alpha/bravo/" == + http2::rewrite_clean_path(balloc, + StringRef::from_lit("/alpha/%62ravo/"))); + + // percent-encoding is converted to upper case. + CU_ASSERT("/delta%3A" == http2::rewrite_clean_path( + balloc, StringRef::from_lit("/delta%3a"))); + + // path component is normalized before matching + CU_ASSERT( + "/alpha/bravo/" == + http2::rewrite_clean_path( + balloc, StringRef::from_lit("/alpha/charlie/%2e././bravo/delta/.."))); + + CU_ASSERT("alpha%3a" == + http2::rewrite_clean_path(balloc, StringRef::from_lit("alpha%3a"))); + + CU_ASSERT("" == http2::rewrite_clean_path(balloc, StringRef{})); + + CU_ASSERT( + "/alpha?bravo" == + http2::rewrite_clean_path(balloc, StringRef::from_lit("//alpha?bravo"))); +} + +void test_http2_get_pure_path_component(void) { + CU_ASSERT("/" == http2::get_pure_path_component(StringRef::from_lit("/"))); + + CU_ASSERT("/foo" == + http2::get_pure_path_component(StringRef::from_lit("/foo"))); + + CU_ASSERT("/bar" == http2::get_pure_path_component( + StringRef::from_lit("https://example.org/bar"))); + + CU_ASSERT("/alpha" == http2::get_pure_path_component(StringRef::from_lit( + "https://example.org/alpha?q=a"))); + + CU_ASSERT("/bravo" == http2::get_pure_path_component(StringRef::from_lit( + "https://example.org/bravo?q=a#fragment"))); + + CU_ASSERT("" == + http2::get_pure_path_component(StringRef::from_lit("\x01\x02"))); +} + +void test_http2_construct_push_component(void) { + BlockAllocator balloc(4096, 4096); + StringRef base, uri; + StringRef scheme, authority, path; + + base = StringRef::from_lit("/b/"); + uri = StringRef::from_lit("https://example.org/foo"); + + CU_ASSERT(0 == http2::construct_push_component(balloc, scheme, authority, + path, base, uri)); + CU_ASSERT("https" == scheme); + CU_ASSERT("example.org" == authority); + CU_ASSERT("/foo" == path); + + scheme = StringRef{}; + authority = StringRef{}; + path = StringRef{}; + + uri = StringRef::from_lit("/foo/bar?q=a"); + + CU_ASSERT(0 == http2::construct_push_component(balloc, scheme, authority, + path, base, uri)); + CU_ASSERT("" == scheme); + CU_ASSERT("" == authority); + CU_ASSERT("/foo/bar?q=a" == path); + + scheme = StringRef{}; + authority = StringRef{}; + path = StringRef{}; + + uri = StringRef::from_lit("foo/../bar?q=a"); + + CU_ASSERT(0 == http2::construct_push_component(balloc, scheme, authority, + path, base, uri)); + CU_ASSERT("" == scheme); + CU_ASSERT("" == authority); + CU_ASSERT("/b/bar?q=a" == path); + + scheme = StringRef{}; + authority = StringRef{}; + path = StringRef{}; + + uri = StringRef{}; + + CU_ASSERT(-1 == http2::construct_push_component(balloc, scheme, authority, + path, base, uri)); + scheme = StringRef{}; + authority = StringRef{}; + path = StringRef{}; + + uri = StringRef::from_lit("?q=a"); + + CU_ASSERT(0 == http2::construct_push_component(balloc, scheme, authority, + path, base, uri)); + CU_ASSERT("" == scheme); + CU_ASSERT("" == authority); + CU_ASSERT("/b/?q=a" == path); +} + +void test_http2_contains_trailers(void) { + CU_ASSERT(!http2::contains_trailers(StringRef::from_lit(""))); + CU_ASSERT(http2::contains_trailers(StringRef::from_lit("trailers"))); + // Match must be case-insensitive. + CU_ASSERT(http2::contains_trailers(StringRef::from_lit("TRAILERS"))); + CU_ASSERT(!http2::contains_trailers(StringRef::from_lit("trailer"))); + CU_ASSERT(!http2::contains_trailers(StringRef::from_lit("trailers 3"))); + CU_ASSERT(http2::contains_trailers(StringRef::from_lit("trailers,"))); + CU_ASSERT(http2::contains_trailers(StringRef::from_lit("trailers,foo"))); + CU_ASSERT(http2::contains_trailers(StringRef::from_lit("foo,trailers"))); + CU_ASSERT(http2::contains_trailers(StringRef::from_lit("foo,trailers,bar"))); + CU_ASSERT( + http2::contains_trailers(StringRef::from_lit("foo, trailers ,bar"))); + CU_ASSERT(http2::contains_trailers(StringRef::from_lit(",trailers"))); +} + +void test_http2_check_transfer_encoding(void) { + CU_ASSERT(http2::check_transfer_encoding(StringRef::from_lit("chunked"))); + CU_ASSERT(http2::check_transfer_encoding(StringRef::from_lit("foo,chunked"))); + CU_ASSERT( + http2::check_transfer_encoding(StringRef::from_lit("foo, chunked"))); + CU_ASSERT( + http2::check_transfer_encoding(StringRef::from_lit("foo , chunked"))); + CU_ASSERT( + http2::check_transfer_encoding(StringRef::from_lit("chunked;foo=bar"))); + CU_ASSERT( + http2::check_transfer_encoding(StringRef::from_lit("chunked ; foo=bar"))); + CU_ASSERT(http2::check_transfer_encoding( + StringRef::from_lit(R"(chunked;foo="bar")"))); + CU_ASSERT(http2::check_transfer_encoding( + StringRef::from_lit(R"(chunked;foo="\bar\"";FOO=BAR)"))); + CU_ASSERT( + http2::check_transfer_encoding(StringRef::from_lit(R"(chunked;foo="")"))); + CU_ASSERT(http2::check_transfer_encoding( + StringRef::from_lit(R"(chunked;foo="bar" , gzip)"))); + + CU_ASSERT(!http2::check_transfer_encoding(StringRef{})); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit(",chunked"))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit("chunked,"))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit("chunked, "))); + CU_ASSERT( + !http2::check_transfer_encoding(StringRef::from_lit("foo,,chunked"))); + CU_ASSERT( + !http2::check_transfer_encoding(StringRef::from_lit("chunked;foo"))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit("chunked;"))); + CU_ASSERT( + !http2::check_transfer_encoding(StringRef::from_lit("chunked;foo=bar;"))); + CU_ASSERT( + !http2::check_transfer_encoding(StringRef::from_lit("chunked;?=bar"))); + CU_ASSERT( + !http2::check_transfer_encoding(StringRef::from_lit("chunked;=bar"))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit("chunked;;"))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit("chunked?"))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit(","))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit(" "))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit(";"))); + CU_ASSERT(!http2::check_transfer_encoding(StringRef::from_lit("\""))); + CU_ASSERT(!http2::check_transfer_encoding( + StringRef::from_lit(R"(chunked;foo="bar)"))); + CU_ASSERT(!http2::check_transfer_encoding( + StringRef::from_lit(R"(chunked;foo="bar\)"))); + CU_ASSERT( + !http2::check_transfer_encoding(StringRef::from_lit(R"(chunked;foo="bar\)" + "\x0a" + R"(")"))); + CU_ASSERT( + !http2::check_transfer_encoding(StringRef::from_lit(R"(chunked;foo=")" + "\x0a" + R"(")"))); + CU_ASSERT(!http2::check_transfer_encoding( + StringRef::from_lit(R"(chunked;foo="bar",,gzip)"))); +} + +} // namespace shrpx diff --git a/src/http2_test.h b/src/http2_test.h new file mode 100644 index 0000000..382470d --- /dev/null +++ b/src/http2_test.h @@ -0,0 +1,54 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP2_TEST_H +#define SHRPX_HTTP2_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_http2_add_header(void); +void test_http2_get_header(void); +void test_http2_copy_headers_to_nva(void); +void test_http2_build_http1_headers_from_headers(void); +void test_http2_lws(void); +void test_http2_rewrite_location_uri(void); +void test_http2_parse_http_status_code(void); +void test_http2_index_header(void); +void test_http2_lookup_token(void); +void test_http2_parse_link_header(void); +void test_http2_path_join(void); +void test_http2_normalize_path(void); +void test_http2_rewrite_clean_path(void); +void test_http2_get_pure_path_component(void); +void test_http2_construct_push_component(void); +void test_http2_contains_trailers(void); +void test_http2_check_transfer_encoding(void); + +} // namespace shrpx + +#endif // SHRPX_HTTP2_TEST_H diff --git a/src/http3.cc b/src/http3.cc new file mode 100644 index 0000000..61134ad --- /dev/null +++ b/src/http3.cc @@ -0,0 +1,206 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "http3.h" + +namespace nghttp2 { + +namespace http3 { + +namespace { +nghttp3_nv make_nv_internal(const std::string &name, const std::string &value, + bool never_index, uint8_t nv_flags) { + uint8_t flags; + + flags = nv_flags | + (never_index ? NGHTTP3_NV_FLAG_NEVER_INDEX : NGHTTP3_NV_FLAG_NONE); + + return {(uint8_t *)name.c_str(), (uint8_t *)value.c_str(), name.size(), + value.size(), flags}; +} +} // namespace + +namespace { +nghttp3_nv make_nv_internal(const StringRef &name, const StringRef &value, + bool never_index, uint8_t nv_flags) { + uint8_t flags; + + flags = nv_flags | + (never_index ? NGHTTP3_NV_FLAG_NEVER_INDEX : NGHTTP3_NV_FLAG_NONE); + + return {(uint8_t *)name.c_str(), (uint8_t *)value.c_str(), name.size(), + value.size(), flags}; +} +} // namespace + +nghttp3_nv make_nv(const std::string &name, const std::string &value, + bool never_index) { + return make_nv_internal(name, value, never_index, NGHTTP3_NV_FLAG_NONE); +} + +nghttp3_nv make_nv(const StringRef &name, const StringRef &value, + bool never_index) { + return make_nv_internal(name, value, never_index, NGHTTP3_NV_FLAG_NONE); +} + +nghttp3_nv make_nv_nocopy(const std::string &name, const std::string &value, + bool never_index) { + return make_nv_internal(name, value, never_index, + NGHTTP3_NV_FLAG_NO_COPY_NAME | + NGHTTP3_NV_FLAG_NO_COPY_VALUE); +} + +nghttp3_nv make_nv_nocopy(const StringRef &name, const StringRef &value, + bool never_index) { + return make_nv_internal(name, value, never_index, + NGHTTP3_NV_FLAG_NO_COPY_NAME | + NGHTTP3_NV_FLAG_NO_COPY_VALUE); +} + +namespace { +void copy_headers_to_nva_internal(std::vector<nghttp3_nv> &nva, + const HeaderRefs &headers, uint8_t nv_flags, + uint32_t flags) { + auto it_forwarded = std::end(headers); + auto it_xff = std::end(headers); + auto it_xfp = std::end(headers); + auto it_via = std::end(headers); + + for (auto it = std::begin(headers); it != std::end(headers); ++it) { + auto kv = &(*it); + if (kv->name.empty() || kv->name[0] == ':') { + continue; + } + switch (kv->token) { + case http2::HD_COOKIE: + case http2::HD_CONNECTION: + case http2::HD_HOST: + case http2::HD_HTTP2_SETTINGS: + case http2::HD_KEEP_ALIVE: + case http2::HD_PROXY_CONNECTION: + case http2::HD_SERVER: + case http2::HD_TE: + case http2::HD_TRANSFER_ENCODING: + case http2::HD_UPGRADE: + continue; + case http2::HD_EARLY_DATA: + if (flags & http2::HDOP_STRIP_EARLY_DATA) { + continue; + } + break; + case http2::HD_SEC_WEBSOCKET_ACCEPT: + if (flags & http2::HDOP_STRIP_SEC_WEBSOCKET_ACCEPT) { + continue; + } + break; + case http2::HD_SEC_WEBSOCKET_KEY: + if (flags & http2::HDOP_STRIP_SEC_WEBSOCKET_KEY) { + continue; + } + break; + case http2::HD_FORWARDED: + if (flags & http2::HDOP_STRIP_FORWARDED) { + continue; + } + + if (it_forwarded == std::end(headers)) { + it_forwarded = it; + continue; + } + + kv = &(*it_forwarded); + it_forwarded = it; + break; + case http2::HD_X_FORWARDED_FOR: + if (flags & http2::HDOP_STRIP_X_FORWARDED_FOR) { + continue; + } + + if (it_xff == std::end(headers)) { + it_xff = it; + continue; + } + + kv = &(*it_xff); + it_xff = it; + break; + case http2::HD_X_FORWARDED_PROTO: + if (flags & http2::HDOP_STRIP_X_FORWARDED_PROTO) { + continue; + } + + if (it_xfp == std::end(headers)) { + it_xfp = it; + continue; + } + + kv = &(*it_xfp); + it_xfp = it; + break; + case http2::HD_VIA: + if (flags & http2::HDOP_STRIP_VIA) { + continue; + } + + if (it_via == std::end(headers)) { + it_via = it; + continue; + } + + kv = &(*it_via); + it_via = it; + break; + } + nva.push_back( + make_nv_internal(kv->name, kv->value, kv->no_index, nv_flags)); + } +} +} // namespace + +void copy_headers_to_nva(std::vector<nghttp3_nv> &nva, + const HeaderRefs &headers, uint32_t flags) { + copy_headers_to_nva_internal(nva, headers, NGHTTP3_NV_FLAG_NONE, flags); +} + +void copy_headers_to_nva_nocopy(std::vector<nghttp3_nv> &nva, + const HeaderRefs &headers, uint32_t flags) { + copy_headers_to_nva_internal( + nva, headers, + NGHTTP3_NV_FLAG_NO_COPY_NAME | NGHTTP3_NV_FLAG_NO_COPY_VALUE, flags); +} + +int check_nv(const uint8_t *name, size_t namelen, const uint8_t *value, + size_t valuelen) { + if (!nghttp3_check_header_name(name, namelen)) { + return 0; + } + if (!nghttp3_check_header_value(value, valuelen)) { + return 0; + } + return 1; +} + +} // namespace http3 + +} // namespace nghttp2 diff --git a/src/http3.h b/src/http3.h new file mode 100644 index 0000000..81ee0d7 --- /dev/null +++ b/src/http3.h @@ -0,0 +1,123 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef HTTP3_H +#define HTTP3_H + +#include "nghttp2_config.h" + +#include <cstring> +#include <string> +#include <vector> + +#include <nghttp3/nghttp3.h> + +#include "http2.h" +#include "template.h" + +namespace nghttp2 { + +namespace http3 { + +// Creates nghttp3_nv using |name| and |value| and returns it. The +// returned value only references the data pointer to name.c_str() and +// value.c_str(). If |no_index| is true, nghttp3_nv flags member has +// NGHTTP3_NV_FLAG_NEVER_INDEX flag set. +nghttp3_nv make_nv(const std::string &name, const std::string &value, + bool never_index = false); + +nghttp3_nv make_nv(const StringRef &name, const StringRef &value, + bool never_index = false); + +nghttp3_nv make_nv_nocopy(const std::string &name, const std::string &value, + bool never_index = false); + +nghttp3_nv make_nv_nocopy(const StringRef &name, const StringRef &value, + bool never_index = false); + +// Create nghttp3_nv from string literal |name| and |value|. +template <size_t N, size_t M> +constexpr nghttp3_nv make_nv_ll(const char (&name)[N], const char (&value)[M]) { + return {(uint8_t *)name, (uint8_t *)value, N - 1, M - 1, + NGHTTP3_NV_FLAG_NO_COPY_NAME | NGHTTP3_NV_FLAG_NO_COPY_VALUE}; +} + +// Create nghttp3_nv from string literal |name| and c-string |value|. +template <size_t N> +nghttp3_nv make_nv_lc(const char (&name)[N], const char *value) { + return {(uint8_t *)name, (uint8_t *)value, N - 1, strlen(value), + NGHTTP3_NV_FLAG_NO_COPY_NAME}; +} + +template <size_t N> +nghttp3_nv make_nv_lc_nocopy(const char (&name)[N], const char *value) { + return {(uint8_t *)name, (uint8_t *)value, N - 1, strlen(value), + NGHTTP3_NV_FLAG_NO_COPY_NAME | NGHTTP3_NV_FLAG_NO_COPY_VALUE}; +} + +// Create nghttp3_nv from string literal |name| and std::string +// |value|. +template <size_t N> +nghttp3_nv make_nv_ls(const char (&name)[N], const std::string &value) { + return {(uint8_t *)name, (uint8_t *)value.c_str(), N - 1, value.size(), + NGHTTP3_NV_FLAG_NO_COPY_NAME}; +} + +template <size_t N> +nghttp3_nv make_nv_ls_nocopy(const char (&name)[N], const std::string &value) { + return {(uint8_t *)name, (uint8_t *)value.c_str(), N - 1, value.size(), + NGHTTP3_NV_FLAG_NO_COPY_NAME | NGHTTP3_NV_FLAG_NO_COPY_VALUE}; +} + +template <size_t N> +nghttp3_nv make_nv_ls_nocopy(const char (&name)[N], const StringRef &value) { + return {(uint8_t *)name, (uint8_t *)value.c_str(), N - 1, value.size(), + NGHTTP3_NV_FLAG_NO_COPY_NAME | NGHTTP3_NV_FLAG_NO_COPY_VALUE}; +} + +// Appends headers in |headers| to |nv|. |headers| must be indexed +// before this call (its element's token field is assigned). Certain +// headers, including disallowed headers in HTTP/3 spec and headers +// which require special handling (i.e. via), are not copied. |flags| +// is one or more of HeaderBuildOp flags. They tell function that +// certain header fields should not be added. +void copy_headers_to_nva(std::vector<nghttp3_nv> &nva, + const HeaderRefs &headers, uint32_t flags); + +// Just like copy_headers_to_nva(), but this adds +// NGHTTP3_NV_FLAG_NO_COPY_NAME and NGHTTP3_NV_FLAG_NO_COPY_VALUE. +void copy_headers_to_nva_nocopy(std::vector<nghttp3_nv> &nva, + const HeaderRefs &headers, uint32_t flags); + +// Checks the header name/value pair using nghttp3_check_header_name() +// and nghttp3_check_header_value(). If both function returns nonzero, +// this function returns nonzero. +int check_nv(const uint8_t *name, size_t namelen, const uint8_t *value, + size_t valuelen); + +} // namespace http3 + +} // namespace nghttp2 + +#endif // HTTP3_H diff --git a/src/inflatehd.cc b/src/inflatehd.cc new file mode 100644 index 0000000..f484042 --- /dev/null +++ b/src/inflatehd.cc @@ -0,0 +1,289 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <getopt.h> + +#include <cstdio> +#include <cstring> +#include <assert.h> +#include <cerrno> +#include <cstdlib> +#include <vector> +#include <iostream> + +#include <jansson.h> + +#include <nghttp2/nghttp2.h> + +#include "template.h" +#include "comp_helper.h" + +namespace nghttp2 { + +typedef struct { + int dump_header_table; +} inflate_config; + +static inflate_config config; + +static uint8_t to_ud(char c) { + if (c >= 'A' && c <= 'Z') { + return c - 'A' + 10; + } else if (c >= 'a' && c <= 'z') { + return c - 'a' + 10; + } else { + return c - '0'; + } +} + +static void decode_hex(uint8_t *dest, const char *src, size_t len) { + size_t i; + for (i = 0; i < len; i += 2) { + *dest++ = to_ud(src[i]) << 4 | to_ud(src[i + 1]); + } +} + +static void to_json(nghttp2_hd_inflater *inflater, json_t *headers, + json_t *wire, int seq, size_t old_settings_table_size) { + auto obj = json_object(); + json_object_set_new(obj, "seq", json_integer(seq)); + json_object_set(obj, "wire", wire); + json_object_set(obj, "headers", headers); + auto max_dyn_table_size = + nghttp2_hd_inflate_get_max_dynamic_table_size(inflater); + if (old_settings_table_size != max_dyn_table_size) { + json_object_set_new(obj, "header_table_size", + json_integer(max_dyn_table_size)); + } + if (config.dump_header_table) { + json_object_set_new(obj, "header_table", + dump_inflate_header_table(inflater)); + } + json_dumpf(obj, stdout, JSON_INDENT(2) | JSON_PRESERVE_ORDER); + json_decref(obj); + printf("\n"); +} + +static int inflate_hd(json_t *obj, nghttp2_hd_inflater *inflater, int seq) { + ssize_t rv; + nghttp2_nv nv; + int inflate_flags; + size_t old_settings_table_size = + nghttp2_hd_inflate_get_max_dynamic_table_size(inflater); + + auto wire = json_object_get(obj, "wire"); + + if (wire == nullptr) { + fprintf(stderr, "'wire' key is missing at %d\n", seq); + return -1; + } + + if (!json_is_string(wire)) { + fprintf(stderr, "'wire' value is not string at %d\n", seq); + return -1; + } + + auto table_size = json_object_get(obj, "header_table_size"); + + if (table_size) { + if (!json_is_integer(table_size)) { + fprintf(stderr, + "The value of 'header_table_size key' is not integer at %d\n", + seq); + return -1; + } + rv = nghttp2_hd_inflate_change_table_size(inflater, + json_integer_value(table_size)); + if (rv != 0) { + fprintf(stderr, + "nghttp2_hd_change_table_size() failed with error %s at %d\n", + nghttp2_strerror(rv), seq); + return -1; + } + } + + auto inputlen = strlen(json_string_value(wire)); + + if (inputlen & 1) { + fprintf(stderr, "Badly formatted output value at %d\n", seq); + exit(EXIT_FAILURE); + } + + auto buflen = inputlen / 2; + auto buf = std::vector<uint8_t>(buflen); + + decode_hex(buf.data(), json_string_value(wire), inputlen); + + auto headers = json_array(); + + auto p = buf.data(); + for (;;) { + inflate_flags = 0; + rv = nghttp2_hd_inflate_hd(inflater, &nv, &inflate_flags, p, buflen, 1); + if (rv < 0) { + fprintf(stderr, "inflate failed with error code %zd at %d\n", rv, seq); + exit(EXIT_FAILURE); + } + p += rv; + buflen -= rv; + if (inflate_flags & NGHTTP2_HD_INFLATE_EMIT) { + json_array_append_new( + headers, dump_header(nv.name, nv.namelen, nv.value, nv.valuelen)); + } + if (inflate_flags & NGHTTP2_HD_INFLATE_FINAL) { + break; + } + } + assert(buflen == 0); + nghttp2_hd_inflate_end_headers(inflater); + to_json(inflater, headers, wire, seq, old_settings_table_size); + json_decref(headers); + + return 0; +} + +static int perform(void) { + nghttp2_hd_inflater *inflater = nullptr; + json_error_t error; + + auto json = json_loadf(stdin, 0, &error); + + if (json == nullptr) { + fprintf(stderr, "JSON loading failed\n"); + exit(EXIT_FAILURE); + } + + auto cases = json_object_get(json, "cases"); + + if (cases == nullptr) { + fprintf(stderr, "Missing 'cases' key in root object\n"); + exit(EXIT_FAILURE); + } + + if (!json_is_array(cases)) { + fprintf(stderr, "'cases' must be JSON array\n"); + exit(EXIT_FAILURE); + } + + nghttp2_hd_inflate_new(&inflater); + output_json_header(); + auto len = json_array_size(cases); + + for (size_t i = 0; i < len; ++i) { + auto obj = json_array_get(cases, i); + if (!json_is_object(obj)) { + fprintf(stderr, "Unexpected JSON type at %zu. It should be object.\n", i); + continue; + } + if (inflate_hd(obj, inflater, i) != 0) { + continue; + } + if (i + 1 < len) { + printf(",\n"); + } + } + output_json_footer(); + nghttp2_hd_inflate_del(inflater); + json_decref(json); + + return 0; +} + +static void print_help(void) { + std::cout << R"(HPACK HTTP/2 header decoder +Usage: inflatehd [OPTIONS] < INPUT + +Reads JSON data from stdin and outputs inflated name/value pairs in +JSON. + +The root JSON object must contain "context" key, which indicates which +compression context is used. If it is "request", request compression +context is used. Otherwise, response compression context is used. +The value of "cases" key contains the sequence of compressed header +block. They share the same compression context and are processed in +the order they appear. Each item in the sequence is a JSON object and +it must have at least "wire" key. Its value is a string containing +compressed header block in hex string. + +Example: + +{ + "context": "request", + "cases": + [ + { "wire": "0284f77778ff" }, + { "wire": "0185fafd3c3c7f81" } + ] +} + +The output of this program can be used as input for deflatehd. + +OPTIONS: + -d, --dump-header-table + Output dynamic header table.)" + << std::endl; + ; +} + +constexpr static struct option long_options[] = { + {"dump-header-table", no_argument, nullptr, 'd'}, {nullptr, 0, nullptr, 0}}; + +int main(int argc, char **argv) { + config.dump_header_table = 0; + while (1) { + int option_index = 0; + int c = getopt_long(argc, argv, "dh", long_options, &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'h': + print_help(); + exit(EXIT_SUCCESS); + case 'd': + // --dump-header-table + config.dump_header_table = 1; + break; + case '?': + exit(EXIT_FAILURE); + default: + break; + } + } + perform(); + return 0; +} + +} // namespace nghttp2 + +int main(int argc, char **argv) { + return nghttp2::run_app(nghttp2::main, argc, argv); +} diff --git a/src/libevent_util.cc b/src/libevent_util.cc new file mode 100644 index 0000000..3b60b6d --- /dev/null +++ b/src/libevent_util.cc @@ -0,0 +1,162 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "libevent_util.h" + +#include <cstring> +#include <algorithm> + +namespace nghttp2 { + +namespace util { + +EvbufferBuffer::EvbufferBuffer() + : evbuffer_(nullptr), + bucket_(nullptr), + buf_(nullptr), + bufmax_(0), + buflen_(0), + limit_(0), + writelen_(0) {} + +EvbufferBuffer::EvbufferBuffer(evbuffer *evbuffer, uint8_t *buf, size_t bufmax, + ssize_t limit) + : evbuffer_(evbuffer), + bucket_(limit == -1 ? nullptr : evbuffer_new()), + buf_(buf), + bufmax_(bufmax), + buflen_(0), + limit_(limit), + writelen_(0) {} + +void EvbufferBuffer::reset(evbuffer *evbuffer, uint8_t *buf, size_t bufmax, + ssize_t limit) { + evbuffer_ = evbuffer; + buf_ = buf; + if (limit != -1 && !bucket_) { + bucket_ = evbuffer_new(); + } + bufmax_ = bufmax; + buflen_ = 0; + limit_ = limit; + writelen_ = 0; +} + +EvbufferBuffer::~EvbufferBuffer() { + if (bucket_) { + evbuffer_free(bucket_); + } +} + +int EvbufferBuffer::write_buffer() { + for (auto pos = buf_, end = buf_ + buflen_; pos < end;) { + // To avoid merging chunks in evbuffer, we first add to temporal + // buffer bucket_ and then move its chain to evbuffer_. + auto nwrite = std::min(end - pos, limit_); + auto rv = evbuffer_add(bucket_, pos, nwrite); + if (rv == -1) { + return -1; + } + rv = evbuffer_add_buffer(evbuffer_, bucket_); + if (rv == -1) { + return -1; + } + pos += nwrite; + } + return 0; +} + +int EvbufferBuffer::flush() { + int rv; + if (buflen_ > 0) { + if (limit_ == -1) { + rv = evbuffer_add(evbuffer_, buf_, buflen_); + } else { + rv = write_buffer(); + } + if (rv == -1) { + return -1; + } + writelen_ += buflen_; + buflen_ = 0; + } + return 0; +} + +int EvbufferBuffer::add(const uint8_t *data, size_t datalen) { + int rv; + if (buflen_ + datalen > bufmax_) { + if (buflen_ > 0) { + if (limit_ == -1) { + rv = evbuffer_add(evbuffer_, buf_, buflen_); + } else { + rv = write_buffer(); + } + if (rv == -1) { + return -1; + } + writelen_ += buflen_; + buflen_ = 0; + } + if (datalen > bufmax_) { + if (limit_ == -1) { + rv = evbuffer_add(evbuffer_, data, datalen); + } else { + rv = write_buffer(); + } + if (rv == -1) { + return -1; + } + writelen_ += buflen_; + return 0; + } + } + memcpy(buf_ + buflen_, data, datalen); + buflen_ += datalen; + return 0; +} + +size_t EvbufferBuffer::get_buflen() const { return buflen_; } + +size_t EvbufferBuffer::get_writelen() const { return writelen_; } + +void bev_enable_unless(bufferevent *bev, int events) { + if ((bufferevent_get_enabled(bev) & events) == events) { + return; + } + + bufferevent_enable(bev, events); +} + +void bev_disable_unless(bufferevent *bev, int events) { + if ((bufferevent_get_enabled(bev) & events) == 0) { + return; + } + + bufferevent_disable(bev, events); +} + +} // namespace util + +} // namespace nghttp2 diff --git a/src/libevent_util.h b/src/libevent_util.h new file mode 100644 index 0000000..1d1ee91 --- /dev/null +++ b/src/libevent_util.h @@ -0,0 +1,75 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef LIBEVENT_UTIL_H +#define LIBEVENT_UTIL_H + +#include "nghttp2_config.h" + +#include <event2/buffer.h> +#include <event2/bufferevent.h> + +namespace nghttp2 { + +namespace util { + +class EvbufferBuffer { +public: + EvbufferBuffer(); + // If |limit| is not -1, at most min(limit, bufmax) size bytes are + // added to evbuffer_. + EvbufferBuffer(evbuffer *evbuffer, uint8_t *buf, size_t bufmax, + ssize_t limit = -1); + ~EvbufferBuffer(); + void reset(evbuffer *evbuffer, uint8_t *buf, size_t bufmax, + ssize_t limit = -1); + int flush(); + int add(const uint8_t *data, size_t datalen); + size_t get_buflen() const; + int write_buffer(); + // Returns the number of written bytes to evbuffer_ so far. reset() + // resets this value to 0. + size_t get_writelen() const; + +private: + evbuffer *evbuffer_; + evbuffer *bucket_; + uint8_t *buf_; + size_t bufmax_; + size_t buflen_; + ssize_t limit_; + size_t writelen_; +}; + +// These functions are provided to reduce epoll_ctl syscall. Avoid +// calling bufferevent_enable/disable() unless it is required by +// sniffing current enabled events. +void bev_enable_unless(bufferevent *bev, int events); +void bev_disable_unless(bufferevent *bev, int events); + +} // namespace util + +} // namespace nghttp2 + +#endif // LIBEVENT_UTIL_H diff --git a/src/memchunk.h b/src/memchunk.h new file mode 100644 index 0000000..7a7f2e9 --- /dev/null +++ b/src/memchunk.h @@ -0,0 +1,664 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef MEMCHUNK_H +#define MEMCHUNK_H + +#include "nghttp2_config.h" + +#include <limits.h> +#ifdef _WIN32 +/* Structure for scatter/gather I/O. */ +struct iovec { + void *iov_base; /* Pointer to data. */ + size_t iov_len; /* Length of data. */ +}; +#else // !_WIN32 +# include <sys/uio.h> +#endif // !_WIN32 + +#include <cassert> +#include <cstring> +#include <memory> +#include <array> +#include <algorithm> +#include <string> +#include <utility> + +#include "template.h" + +namespace nghttp2 { + +#define DEFAULT_WR_IOVCNT 16 + +#if defined(IOV_MAX) && IOV_MAX < DEFAULT_WR_IOVCNT +# define MAX_WR_IOVCNT IOV_MAX +#else // !defined(IOV_MAX) || IOV_MAX >= DEFAULT_WR_IOVCNT +# define MAX_WR_IOVCNT DEFAULT_WR_IOVCNT +#endif // !defined(IOV_MAX) || IOV_MAX >= DEFAULT_WR_IOVCNT + +template <size_t N> struct Memchunk { + Memchunk(Memchunk *next_chunk) + : pos(std::begin(buf)), last(pos), knext(next_chunk), next(nullptr) {} + size_t len() const { return last - pos; } + size_t left() const { return std::end(buf) - last; } + void reset() { pos = last = std::begin(buf); } + std::array<uint8_t, N> buf; + uint8_t *pos, *last; + Memchunk *knext; + Memchunk *next; + static const size_t size = N; +}; + +template <typename T> struct Pool { + Pool() : pool(nullptr), freelist(nullptr), poolsize(0), freelistsize(0) {} + ~Pool() { clear(); } + T *get() { + if (freelist) { + auto m = freelist; + freelist = freelist->next; + m->next = nullptr; + m->reset(); + freelistsize -= T::size; + return m; + } + + pool = new T{pool}; + poolsize += T::size; + return pool; + } + void recycle(T *m) { + m->next = freelist; + freelist = m; + freelistsize += T::size; + } + void clear() { + freelist = nullptr; + freelistsize = 0; + for (auto p = pool; p;) { + auto knext = p->knext; + delete p; + p = knext; + } + pool = nullptr; + poolsize = 0; + } + using value_type = T; + T *pool; + T *freelist; + size_t poolsize; + size_t freelistsize; +}; + +template <typename Memchunk> struct Memchunks { + Memchunks(Pool<Memchunk> *pool) + : pool(pool), + head(nullptr), + tail(nullptr), + len(0), + mark(nullptr), + mark_pos(nullptr), + mark_offset(0) {} + Memchunks(const Memchunks &) = delete; + Memchunks(Memchunks &&other) noexcept + : pool{other.pool}, // keep other.pool + head{std::exchange(other.head, nullptr)}, + tail{std::exchange(other.tail, nullptr)}, + len{std::exchange(other.len, 0)}, + mark{std::exchange(other.mark, nullptr)}, + mark_pos{std::exchange(other.mark_pos, nullptr)}, + mark_offset{std::exchange(other.mark_offset, 0)} {} + Memchunks &operator=(const Memchunks &) = delete; + Memchunks &operator=(Memchunks &&other) noexcept { + if (this == &other) { + return *this; + } + + reset(); + + pool = other.pool; + head = std::exchange(other.head, nullptr); + tail = std::exchange(other.tail, nullptr); + len = std::exchange(other.len, 0); + mark = std::exchange(other.mark, nullptr); + mark_pos = std::exchange(other.mark_pos, nullptr); + mark_offset = std::exchange(other.mark_offset, 0); + + return *this; + } + ~Memchunks() { + if (!pool) { + return; + } + for (auto m = head; m;) { + auto next = m->next; + pool->recycle(m); + m = next; + } + } + size_t append(char c) { + if (!tail) { + head = tail = pool->get(); + } else if (tail->left() == 0) { + tail->next = pool->get(); + tail = tail->next; + } + *tail->last++ = c; + ++len; + return 1; + } + size_t append(const void *src, size_t count) { + if (count == 0) { + return 0; + } + + auto first = static_cast<const uint8_t *>(src); + auto last = first + count; + + if (!tail) { + head = tail = pool->get(); + } + + for (;;) { + auto n = std::min(static_cast<size_t>(last - first), tail->left()); + tail->last = std::copy_n(first, n, tail->last); + first += n; + len += n; + if (first == last) { + break; + } + + tail->next = pool->get(); + tail = tail->next; + } + + return count; + } + template <size_t N> size_t append(const char (&s)[N]) { + return append(s, N - 1); + } + size_t append(const std::string &s) { return append(s.c_str(), s.size()); } + size_t append(const StringRef &s) { return append(s.c_str(), s.size()); } + size_t append(const ImmutableString &s) { + return append(s.c_str(), s.size()); + } + size_t copy(Memchunks &dest) { + auto m = head; + while (m) { + dest.append(m->pos, m->len()); + m = m->next; + } + return len; + } + size_t remove(void *dest, size_t count) { + assert(mark == nullptr); + + if (!tail || count == 0) { + return 0; + } + + auto first = static_cast<uint8_t *>(dest); + auto last = first + count; + + auto m = head; + + while (m) { + auto next = m->next; + auto n = std::min(static_cast<size_t>(last - first), m->len()); + + assert(m->len()); + first = std::copy_n(m->pos, n, first); + m->pos += n; + len -= n; + if (m->len() > 0) { + break; + } + pool->recycle(m); + m = next; + } + head = m; + if (head == nullptr) { + tail = nullptr; + } + + return first - static_cast<uint8_t *>(dest); + } + size_t remove(Memchunks &dest, size_t count) { + assert(mark == nullptr); + + if (!tail || count == 0) { + return 0; + } + + auto left = count; + auto m = head; + + while (m) { + auto next = m->next; + auto n = std::min(left, m->len()); + + assert(m->len()); + dest.append(m->pos, n); + m->pos += n; + len -= n; + left -= n; + if (m->len() > 0) { + break; + } + pool->recycle(m); + m = next; + } + head = m; + if (head == nullptr) { + tail = nullptr; + } + + return count - left; + } + size_t remove(Memchunks &dest) { + assert(pool == dest.pool); + assert(mark == nullptr); + + if (head == nullptr) { + return 0; + } + + auto n = len; + + if (dest.tail == nullptr) { + dest.head = head; + } else { + dest.tail->next = head; + } + + dest.tail = tail; + dest.len += len; + + head = tail = nullptr; + len = 0; + + return n; + } + size_t drain(size_t count) { + assert(mark == nullptr); + + auto ndata = count; + auto m = head; + while (m) { + auto next = m->next; + auto n = std::min(count, m->len()); + m->pos += n; + count -= n; + len -= n; + if (m->len() > 0) { + break; + } + + pool->recycle(m); + m = next; + } + head = m; + if (head == nullptr) { + tail = nullptr; + } + return ndata - count; + } + size_t drain_mark(size_t count) { + auto ndata = count; + auto m = head; + while (m) { + auto next = m->next; + auto n = std::min(count, m->len()); + m->pos += n; + count -= n; + len -= n; + mark_offset -= n; + + if (m->len() > 0) { + assert(mark != m || m->pos <= mark_pos); + break; + } + if (mark == m) { + assert(m->pos <= mark_pos); + + mark = nullptr; + mark_pos = nullptr; + mark_offset = 0; + } + + pool->recycle(m); + m = next; + } + head = m; + if (head == nullptr) { + tail = nullptr; + } + return ndata - count; + } + int riovec(struct iovec *iov, int iovcnt) const { + if (!head) { + return 0; + } + auto m = head; + int i; + for (i = 0; i < iovcnt && m; ++i, m = m->next) { + iov[i].iov_base = m->pos; + iov[i].iov_len = m->len(); + } + return i; + } + int riovec_mark(struct iovec *iov, int iovcnt) { + if (!head || iovcnt == 0) { + return 0; + } + + int i = 0; + Memchunk *m; + if (mark) { + if (mark_pos != mark->last) { + iov[0].iov_base = mark_pos; + iov[0].iov_len = mark->len() - (mark_pos - mark->pos); + + mark_pos = mark->last; + mark_offset += iov[0].iov_len; + i = 1; + } + m = mark->next; + } else { + i = 0; + m = head; + } + + for (; i < iovcnt && m; ++i, m = m->next) { + iov[i].iov_base = m->pos; + iov[i].iov_len = m->len(); + + mark = m; + mark_pos = m->last; + mark_offset += m->len(); + } + + return i; + } + size_t rleft() const { return len; } + size_t rleft_mark() const { return len - mark_offset; } + void reset() { + for (auto m = head; m;) { + auto next = m->next; + pool->recycle(m); + m = next; + } + len = 0; + head = tail = mark = nullptr; + mark_pos = nullptr; + mark_offset = 0; + } + + Pool<Memchunk> *pool; + Memchunk *head, *tail; + size_t len; + Memchunk *mark; + uint8_t *mark_pos; + size_t mark_offset; +}; + +// Wrapper around Memchunks to offer "peeking" functionality. +template <typename Memchunk> struct PeekMemchunks { + PeekMemchunks(Pool<Memchunk> *pool) + : memchunks(pool), + cur(nullptr), + cur_pos(nullptr), + cur_last(nullptr), + len(0), + peeking(true) {} + PeekMemchunks(const PeekMemchunks &) = delete; + PeekMemchunks(PeekMemchunks &&other) noexcept + : memchunks{std::move(other.memchunks)}, + cur{std::exchange(other.cur, nullptr)}, + cur_pos{std::exchange(other.cur_pos, nullptr)}, + cur_last{std::exchange(other.cur_last, nullptr)}, + len{std::exchange(other.len, 0)}, + peeking{std::exchange(other.peeking, true)} {} + PeekMemchunks &operator=(const PeekMemchunks &) = delete; + PeekMemchunks &operator=(PeekMemchunks &&other) noexcept { + if (this == &other) { + return *this; + } + + memchunks = std::move(other.memchunks); + cur = std::exchange(other.cur, nullptr); + cur_pos = std::exchange(other.cur_pos, nullptr); + cur_last = std::exchange(other.cur_last, nullptr); + len = std::exchange(other.len, 0); + peeking = std::exchange(other.peeking, true); + + return *this; + } + size_t append(const void *src, size_t count) { + count = memchunks.append(src, count); + len += count; + return count; + } + size_t remove(void *dest, size_t count) { + if (!peeking) { + count = memchunks.remove(dest, count); + len -= count; + return count; + } + + if (count == 0 || len == 0) { + return 0; + } + + if (!cur) { + cur = memchunks.head; + cur_pos = cur->pos; + } + + // cur_last could be updated in append + cur_last = cur->last; + + if (cur_pos == cur_last) { + assert(cur->next); + cur = cur->next; + } + + auto first = static_cast<uint8_t *>(dest); + auto last = first + count; + + for (;;) { + auto n = std::min(last - first, cur_last - cur_pos); + + first = std::copy_n(cur_pos, n, first); + cur_pos += n; + len -= n; + + if (first == last) { + break; + } + assert(cur_pos == cur_last); + if (!cur->next) { + break; + } + cur = cur->next; + cur_pos = cur->pos; + cur_last = cur->last; + } + return first - static_cast<uint8_t *>(dest); + } + size_t rleft() const { return len; } + size_t rleft_buffered() const { return memchunks.rleft(); } + void disable_peek(bool drain) { + if (!peeking) { + return; + } + if (drain) { + auto n = rleft_buffered() - rleft(); + memchunks.drain(n); + assert(len == memchunks.rleft()); + } else { + len = memchunks.rleft(); + } + cur = nullptr; + cur_pos = cur_last = nullptr; + peeking = false; + } + void reset() { + memchunks.reset(); + cur = nullptr; + cur_pos = cur_last = nullptr; + len = 0; + peeking = true; + } + Memchunks<Memchunk> memchunks; + // Pointer to the Memchunk currently we are reading/writing. + Memchunk *cur; + // Region inside cur, we have processed to cur_pos. + uint8_t *cur_pos, *cur_last; + // This is the length we have left unprocessed. len <= + // memchunk.rleft() must hold. + size_t len; + // true if peeking is enabled. Initially it is true. + bool peeking; +}; + +using Memchunk16K = Memchunk<16_k>; +using MemchunkPool = Pool<Memchunk16K>; +using DefaultMemchunks = Memchunks<Memchunk16K>; +using DefaultPeekMemchunks = PeekMemchunks<Memchunk16K>; + +inline int limit_iovec(struct iovec *iov, int iovcnt, size_t max) { + if (max == 0) { + return 0; + } + for (int i = 0; i < iovcnt; ++i) { + auto d = std::min(max, iov[i].iov_len); + iov[i].iov_len = d; + max -= d; + if (max == 0) { + return i + 1; + } + } + return iovcnt; +} + +// MemchunkBuffer is similar to Buffer, but it uses pooled Memchunk +// for its underlying buffer. +template <typename Memchunk> struct MemchunkBuffer { + MemchunkBuffer(Pool<Memchunk> *pool) : pool(pool), chunk(nullptr) {} + MemchunkBuffer(const MemchunkBuffer &) = delete; + MemchunkBuffer(MemchunkBuffer &&other) noexcept + : pool(other.pool), chunk(other.chunk) { + other.chunk = nullptr; + } + MemchunkBuffer &operator=(const MemchunkBuffer &) = delete; + MemchunkBuffer &operator=(MemchunkBuffer &&other) noexcept { + if (this == &other) { + return *this; + } + + pool = other.pool; + chunk = other.chunk; + + other.chunk = nullptr; + + return *this; + } + + ~MemchunkBuffer() { + if (!pool || !chunk) { + return; + } + pool->recycle(chunk); + } + + // Ensures that the underlying buffer is allocated. + void ensure_chunk() { + if (chunk) { + return; + } + chunk = pool->get(); + } + + // Releases the underlying buffer. + void release_chunk() { + if (!chunk) { + return; + } + pool->recycle(chunk); + chunk = nullptr; + } + + // Returns true if the underlying buffer is allocated. + bool chunk_avail() const { return chunk != nullptr; } + + // The functions below must be called after the underlying buffer is + // allocated (use ensure_chunk). + + // MemchunkBuffer provides the same interface functions with Buffer. + // Since we has chunk as a member variable, pos and last are + // implemented as wrapper functions. + + uint8_t *pos() const { return chunk->pos; } + uint8_t *last() const { return chunk->last; } + + size_t rleft() const { return chunk->len(); } + size_t wleft() const { return chunk->left(); } + size_t write(const void *src, size_t count) { + count = std::min(count, wleft()); + auto p = static_cast<const uint8_t *>(src); + chunk->last = std::copy_n(p, count, chunk->last); + return count; + } + size_t write(size_t count) { + count = std::min(count, wleft()); + chunk->last += count; + return count; + } + size_t drain(size_t count) { + count = std::min(count, rleft()); + chunk->pos += count; + return count; + } + size_t drain_reset(size_t count) { + count = std::min(count, rleft()); + std::copy(chunk->pos + count, chunk->last, std::begin(chunk->buf)); + chunk->last = std::begin(chunk->buf) + (chunk->last - (chunk->pos + count)); + chunk->pos = std::begin(chunk->buf); + return count; + } + void reset() { chunk->reset(); } + uint8_t *begin() { return std::begin(chunk->buf); } + uint8_t &operator[](size_t n) { return chunk->buf[n]; } + const uint8_t &operator[](size_t n) const { return chunk->buf[n]; } + + Pool<Memchunk> *pool; + Memchunk *chunk; +}; + +using DefaultMemchunkBuffer = MemchunkBuffer<Memchunk16K>; + +} // namespace nghttp2 + +#endif // MEMCHUNK_H diff --git a/src/memchunk_test.cc b/src/memchunk_test.cc new file mode 100644 index 0000000..236d9ea --- /dev/null +++ b/src/memchunk_test.cc @@ -0,0 +1,340 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "memchunk_test.h" + +#include <CUnit/CUnit.h> + +#include <nghttp2/nghttp2.h> + +#include "memchunk.h" +#include "util.h" + +namespace nghttp2 { + +void test_pool_recycle(void) { + MemchunkPool pool; + + CU_ASSERT(!pool.pool); + CU_ASSERT(0 == pool.poolsize); + CU_ASSERT(nullptr == pool.freelist); + + auto m1 = pool.get(); + + CU_ASSERT(m1 == pool.pool); + CU_ASSERT(MemchunkPool::value_type::size == pool.poolsize); + CU_ASSERT(nullptr == pool.freelist); + + auto m2 = pool.get(); + + CU_ASSERT(m2 == pool.pool); + CU_ASSERT(2 * MemchunkPool::value_type::size == pool.poolsize); + CU_ASSERT(nullptr == pool.freelist); + CU_ASSERT(m1 == m2->knext); + CU_ASSERT(nullptr == m1->knext); + + auto m3 = pool.get(); + + CU_ASSERT(m3 == pool.pool); + CU_ASSERT(3 * MemchunkPool::value_type::size == pool.poolsize); + CU_ASSERT(nullptr == pool.freelist); + + pool.recycle(m3); + + CU_ASSERT(m3 == pool.pool); + CU_ASSERT(3 * MemchunkPool::value_type::size == pool.poolsize); + CU_ASSERT(m3 == pool.freelist); + + auto m4 = pool.get(); + + CU_ASSERT(m3 == m4); + CU_ASSERT(m4 == pool.pool); + CU_ASSERT(3 * MemchunkPool::value_type::size == pool.poolsize); + CU_ASSERT(nullptr == pool.freelist); + + pool.recycle(m2); + pool.recycle(m1); + + CU_ASSERT(m1 == pool.freelist); + CU_ASSERT(m2 == m1->next); + CU_ASSERT(nullptr == m2->next); +} + +using Memchunk16 = Memchunk<16>; +using MemchunkPool16 = Pool<Memchunk16>; +using Memchunks16 = Memchunks<Memchunk16>; +using PeekMemchunks16 = PeekMemchunks<Memchunk16>; + +void test_memchunks_append(void) { + MemchunkPool16 pool; + Memchunks16 chunks(&pool); + + chunks.append("012"); + + auto m = chunks.tail; + + CU_ASSERT(3 == m->len()); + CU_ASSERT(13 == m->left()); + + chunks.append("3456789abcdef@"); + + CU_ASSERT(16 == m->len()); + CU_ASSERT(0 == m->left()); + + m = chunks.tail; + + CU_ASSERT(1 == m->len()); + CU_ASSERT(15 == m->left()); + CU_ASSERT(17 == chunks.rleft()); + + char buf[16]; + size_t nread; + + nread = chunks.remove(buf, 8); + + CU_ASSERT(8 == nread); + CU_ASSERT(0 == memcmp("01234567", buf, nread)); + CU_ASSERT(9 == chunks.rleft()); + + nread = chunks.remove(buf, sizeof(buf)); + + CU_ASSERT(9 == nread); + CU_ASSERT(0 == memcmp("89abcdef@", buf, nread)); + CU_ASSERT(0 == chunks.rleft()); + CU_ASSERT(nullptr == chunks.head); + CU_ASSERT(nullptr == chunks.tail); + CU_ASSERT(32 == pool.poolsize); +} + +void test_memchunks_drain(void) { + MemchunkPool16 pool; + Memchunks16 chunks(&pool); + + chunks.append("0123456789"); + + size_t nread; + + nread = chunks.drain(3); + + CU_ASSERT(3 == nread); + + char buf[16]; + + nread = chunks.remove(buf, sizeof(buf)); + + CU_ASSERT(7 == nread); + CU_ASSERT(0 == memcmp("3456789", buf, nread)); +} + +void test_memchunks_riovec(void) { + MemchunkPool16 pool; + Memchunks16 chunks(&pool); + + std::array<char, 3 * 16> buf{}; + + chunks.append(buf.data(), buf.size()); + + std::array<struct iovec, 2> iov; + auto iovcnt = chunks.riovec(iov.data(), iov.size()); + + auto m = chunks.head; + + CU_ASSERT(2 == iovcnt); + CU_ASSERT(m->buf.data() == iov[0].iov_base); + CU_ASSERT(m->len() == iov[0].iov_len); + + m = m->next; + + CU_ASSERT(m->buf.data() == iov[1].iov_base); + CU_ASSERT(m->len() == iov[1].iov_len); + + chunks.drain(2 * 16); + + iovcnt = chunks.riovec(iov.data(), iov.size()); + + CU_ASSERT(1 == iovcnt); + + m = chunks.head; + CU_ASSERT(m->buf.data() == iov[0].iov_base); + CU_ASSERT(m->len() == iov[0].iov_len); +} + +void test_memchunks_recycle(void) { + MemchunkPool16 pool; + { + Memchunks16 chunks(&pool); + std::array<char, 32> buf{}; + chunks.append(buf.data(), buf.size()); + } + CU_ASSERT(32 == pool.poolsize); + CU_ASSERT(nullptr != pool.freelist); + + auto m = pool.freelist; + m = m->next; + + CU_ASSERT(nullptr != m); + CU_ASSERT(nullptr == m->next); +} + +void test_memchunks_reset(void) { + MemchunkPool16 pool; + Memchunks16 chunks(&pool); + + std::array<uint8_t, 32> b{}; + + chunks.append(b.data(), b.size()); + + CU_ASSERT(32 == chunks.rleft()); + + chunks.reset(); + + CU_ASSERT(0 == chunks.rleft()); + CU_ASSERT(nullptr == chunks.head); + CU_ASSERT(nullptr == chunks.tail); + + auto m = pool.freelist; + + CU_ASSERT(nullptr != m); + CU_ASSERT(nullptr != m->next); + CU_ASSERT(nullptr == m->next->next); +} + +void test_peek_memchunks_append(void) { + MemchunkPool16 pool; + PeekMemchunks16 pchunks(&pool); + + std::array<uint8_t, 32> b{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', + }, + d; + + pchunks.append(b.data(), b.size()); + + CU_ASSERT(32 == pchunks.rleft()); + CU_ASSERT(32 == pchunks.rleft_buffered()); + + CU_ASSERT(0 == pchunks.remove(nullptr, 0)); + + CU_ASSERT(32 == pchunks.rleft()); + CU_ASSERT(32 == pchunks.rleft_buffered()); + + CU_ASSERT(12 == pchunks.remove(d.data(), 12)); + + CU_ASSERT(std::equal(std::begin(b), std::begin(b) + 12, std::begin(d))); + + CU_ASSERT(20 == pchunks.rleft()); + CU_ASSERT(32 == pchunks.rleft_buffered()); + + CU_ASSERT(20 == pchunks.remove(d.data(), d.size())); + + CU_ASSERT(std::equal(std::begin(b) + 12, std::end(b), std::begin(d))); + + CU_ASSERT(0 == pchunks.rleft()); + CU_ASSERT(32 == pchunks.rleft_buffered()); +} + +void test_peek_memchunks_disable_peek_drain(void) { + MemchunkPool16 pool; + PeekMemchunks16 pchunks(&pool); + + std::array<uint8_t, 32> b{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', + }, + d; + + pchunks.append(b.data(), b.size()); + + CU_ASSERT(12 == pchunks.remove(d.data(), 12)); + + pchunks.disable_peek(true); + + CU_ASSERT(!pchunks.peeking); + CU_ASSERT(20 == pchunks.rleft()); + CU_ASSERT(20 == pchunks.rleft_buffered()); + + CU_ASSERT(20 == pchunks.remove(d.data(), d.size())); + + CU_ASSERT(std::equal(std::begin(b) + 12, std::end(b), std::begin(d))); + + CU_ASSERT(0 == pchunks.rleft()); + CU_ASSERT(0 == pchunks.rleft_buffered()); +} + +void test_peek_memchunks_disable_peek_no_drain(void) { + MemchunkPool16 pool; + PeekMemchunks16 pchunks(&pool); + + std::array<uint8_t, 32> b{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', + }, + d; + + pchunks.append(b.data(), b.size()); + + CU_ASSERT(12 == pchunks.remove(d.data(), 12)); + + pchunks.disable_peek(false); + + CU_ASSERT(!pchunks.peeking); + CU_ASSERT(32 == pchunks.rleft()); + CU_ASSERT(32 == pchunks.rleft_buffered()); + + CU_ASSERT(32 == pchunks.remove(d.data(), d.size())); + + CU_ASSERT(std::equal(std::begin(b), std::end(b), std::begin(d))); + + CU_ASSERT(0 == pchunks.rleft()); + CU_ASSERT(0 == pchunks.rleft_buffered()); +} + +void test_peek_memchunks_reset(void) { + MemchunkPool16 pool; + PeekMemchunks16 pchunks(&pool); + + std::array<uint8_t, 32> b{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', + }, + d; + + pchunks.append(b.data(), b.size()); + + CU_ASSERT(12 == pchunks.remove(d.data(), 12)); + + pchunks.disable_peek(true); + pchunks.reset(); + + CU_ASSERT(0 == pchunks.rleft()); + CU_ASSERT(0 == pchunks.rleft_buffered()); + + CU_ASSERT(nullptr == pchunks.cur); + CU_ASSERT(nullptr == pchunks.cur_pos); + CU_ASSERT(nullptr == pchunks.cur_last); + CU_ASSERT(pchunks.peeking); +} + +} // namespace nghttp2 diff --git a/src/memchunk_test.h b/src/memchunk_test.h new file mode 100644 index 0000000..7d677e7 --- /dev/null +++ b/src/memchunk_test.h @@ -0,0 +1,47 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef MEMCHUNK_TEST_H +#define MEMCHUNK_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace nghttp2 { + +void test_pool_recycle(void); +void test_memchunks_append(void); +void test_memchunks_drain(void); +void test_memchunks_riovec(void); +void test_memchunks_recycle(void); +void test_memchunks_reset(void); +void test_peek_memchunks_append(void); +void test_peek_memchunks_disable_peek_drain(void); +void test_peek_memchunks_disable_peek_no_drain(void); +void test_peek_memchunks_reset(void); + +} // namespace nghttp2 + +#endif // MEMCHUNK_TEST_H diff --git a/src/network.h b/src/network.h new file mode 100644 index 0000000..45311d8 --- /dev/null +++ b/src/network.h @@ -0,0 +1,67 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NETWORK_H +#define NETWORK_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef _WIN32 +# include <ws2tcpip.h> +#else // !_WIN32 +# include <sys/un.h> +#endif // !_WIN32 +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif // HAVE_ARPA_INET_H + +namespace nghttp2 { + +union sockaddr_union { + sockaddr_storage storage; + sockaddr sa; + sockaddr_in6 in6; + sockaddr_in in; +#ifndef _WIN32 + sockaddr_un un; +#endif // !_WIN32 +}; + +struct Address { + size_t len; + union sockaddr_union su; +}; + +} // namespace nghttp2 + +#endif // NETWORK_H diff --git a/src/nghttp.cc b/src/nghttp.cc new file mode 100644 index 0000000..41a88c6 --- /dev/null +++ b/src/nghttp.cc @@ -0,0 +1,3115 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp.h" + +#include <sys/stat.h> +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#include <netinet/tcp.h> +#include <getopt.h> + +#include <cassert> +#include <cstdio> +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <iomanip> +#include <sstream> +#include <tuple> + +#include <openssl/err.h> + +#ifdef HAVE_JANSSON +# include <jansson.h> +#endif // HAVE_JANSSON + +#include "app_helper.h" +#include "HtmlParser.h" +#include "util.h" +#include "base64.h" +#include "tls.h" +#include "template.h" +#include "ssl_compat.h" + +#ifndef O_BINARY +# define O_BINARY (0) +#endif // O_BINARY + +namespace nghttp2 { + +// The anchor stream nodes when --no-dep is not used. The stream ID = +// 1 is excluded since it is used as first stream in upgrade case. We +// follows the same dependency anchor nodes as Firefox does. +struct Anchor { + int32_t stream_id; + // stream ID this anchor depends on + int32_t dep_stream_id; + // .. with this weight. + int32_t weight; +}; + +// This is index into anchors. Firefox uses ANCHOR_FOLLOWERS for html +// file. +enum { + ANCHOR_LEADERS, + ANCHOR_UNBLOCKED, + ANCHOR_BACKGROUND, + ANCHOR_SPECULATIVE, + ANCHOR_FOLLOWERS, +}; + +namespace { +constexpr auto anchors = std::array<Anchor, 5>{{ + {3, 0, 201}, + {5, 0, 101}, + {7, 0, 1}, + {9, 7, 1}, + {11, 3, 1}, +}}; +} // namespace + +Config::Config() + : header_table_size(-1), + min_header_table_size(std::numeric_limits<uint32_t>::max()), + encoder_header_table_size(-1), + padding(0), + max_concurrent_streams(100), + peer_max_concurrent_streams(100), + multiply(1), + timeout(0.), + window_bits(-1), + connection_window_bits(-1), + verbose(0), + port_override(0), + null_out(false), + remote_name(false), + get_assets(false), + stat(false), + upgrade(false), + continuation(false), + no_content_length(false), + no_dep(false), + hexdump(false), + no_push(false), + expect_continue(false), + verify_peer(true), + ktls(false), + no_rfc7540_pri(false) { + nghttp2_option_new(&http2_option); + nghttp2_option_set_peer_max_concurrent_streams(http2_option, + peer_max_concurrent_streams); + nghttp2_option_set_builtin_recv_extension_type(http2_option, NGHTTP2_ALTSVC); + nghttp2_option_set_builtin_recv_extension_type(http2_option, NGHTTP2_ORIGIN); +} + +Config::~Config() { nghttp2_option_del(http2_option); } + +namespace { +Config config; +} // namespace + +namespace { +void print_protocol_nego_error() { + std::cerr << "[ERROR] HTTP/2 protocol was not selected." + << " (nghttp2 expects " << NGHTTP2_PROTO_VERSION_ID << ")" + << std::endl; +} +} // namespace + +namespace { +std::string strip_fragment(const char *raw_uri) { + const char *end; + for (end = raw_uri; *end && *end != '#'; ++end) + ; + size_t len = end - raw_uri; + return std::string(raw_uri, len); +} +} // namespace + +Request::Request(const std::string &uri, const http_parser_url &u, + const nghttp2_data_provider *data_prd, int64_t data_length, + const nghttp2_priority_spec &pri_spec, int level) + : uri(uri), + u(u), + pri_spec(pri_spec), + data_length(data_length), + data_offset(0), + response_len(0), + inflater(nullptr), + data_prd(data_prd), + header_buffer_size(0), + stream_id(-1), + status(0), + level(level), + expect_final_response(false) { + http2::init_hdidx(res_hdidx); + http2::init_hdidx(req_hdidx); +} + +Request::~Request() { nghttp2_gzip_inflate_del(inflater); } + +void Request::init_inflater() { + int rv; + // This is required with --disable-assert. + (void)rv; + rv = nghttp2_gzip_inflate_new(&inflater); + assert(rv == 0); +} + +StringRef Request::get_real_scheme() const { + return config.scheme_override.empty() + ? util::get_uri_field(uri.c_str(), u, UF_SCHEMA) + : StringRef{config.scheme_override}; +} + +StringRef Request::get_real_host() const { + return config.host_override.empty() + ? util::get_uri_field(uri.c_str(), u, UF_HOST) + : StringRef{config.host_override}; +} + +uint16_t Request::get_real_port() const { + auto scheme = get_real_scheme(); + return config.host_override.empty() ? util::has_uri_field(u, UF_PORT) ? u.port + : scheme == "https" ? 443 + : 80 + : config.port_override == 0 ? scheme == "https" ? 443 : 80 + : config.port_override; +} + +void Request::init_html_parser() { + // We crawl HTML using overridden scheme, host, and port. + auto scheme = get_real_scheme(); + auto host = get_real_host(); + auto port = get_real_port(); + auto ipv6_lit = + std::find(std::begin(host), std::end(host), ':') != std::end(host); + + auto base_uri = scheme.str(); + base_uri += "://"; + if (ipv6_lit) { + base_uri += '['; + } + base_uri += host; + if (ipv6_lit) { + base_uri += ']'; + } + if (!((scheme == "https" && port == 443) || + (scheme == "http" && port == 80))) { + base_uri += ':'; + base_uri += util::utos(port); + } + base_uri += util::get_uri_field(uri.c_str(), u, UF_PATH); + if (util::has_uri_field(u, UF_QUERY)) { + base_uri += '?'; + base_uri += util::get_uri_field(uri.c_str(), u, UF_QUERY); + } + + html_parser = std::make_unique<HtmlParser>(base_uri); +} + +int Request::update_html_parser(const uint8_t *data, size_t len, int fin) { + if (!html_parser) { + return 0; + } + return html_parser->parse_chunk(reinterpret_cast<const char *>(data), len, + fin); +} + +std::string Request::make_reqpath() const { + std::string path = util::has_uri_field(u, UF_PATH) + ? util::get_uri_field(uri.c_str(), u, UF_PATH).str() + : "/"; + if (util::has_uri_field(u, UF_QUERY)) { + path += '?'; + path.append(uri.c_str() + u.field_data[UF_QUERY].off, + u.field_data[UF_QUERY].len); + } + return path; +} + +namespace { +// Perform special handling |host| if it is IPv6 literal and includes +// zone ID per RFC 6874. +std::string decode_host(const StringRef &host) { + auto zone_start = std::find(std::begin(host), std::end(host), '%'); + if (zone_start == std::end(host) || + !util::ipv6_numeric_addr( + std::string(std::begin(host), zone_start).c_str())) { + return host.str(); + } + // case: ::1% + if (zone_start + 1 == std::end(host)) { + return StringRef{host.c_str(), host.size() - 1}.str(); + } + // case: ::1%12 or ::1%1 + if (zone_start + 3 >= std::end(host)) { + return host.str(); + } + // If we see "%25", followed by more characters, then decode %25 as + // '%'. + auto zone_id_src = (*(zone_start + 1) == '2' && *(zone_start + 2) == '5') + ? zone_start + 3 + : zone_start + 1; + auto zone_id = util::percent_decode(zone_id_src, std::end(host)); + auto res = std::string(std::begin(host), zone_start + 1); + res += zone_id; + return res; +} +} // namespace + +namespace { +nghttp2_priority_spec resolve_dep(int res_type) { + nghttp2_priority_spec pri_spec; + + if (config.no_dep) { + nghttp2_priority_spec_default_init(&pri_spec); + + return pri_spec; + } + + int32_t anchor_id; + int32_t weight; + switch (res_type) { + case REQ_CSS: + case REQ_JS: + anchor_id = anchors[ANCHOR_LEADERS].stream_id; + weight = 32; + break; + case REQ_UNBLOCK_JS: + anchor_id = anchors[ANCHOR_UNBLOCKED].stream_id; + weight = 32; + break; + case REQ_IMG: + anchor_id = anchors[ANCHOR_FOLLOWERS].stream_id; + weight = 12; + break; + default: + anchor_id = anchors[ANCHOR_FOLLOWERS].stream_id; + weight = 32; + } + + nghttp2_priority_spec_init(&pri_spec, anchor_id, weight, 0); + return pri_spec; +} +} // namespace + +bool Request::is_ipv6_literal_addr() const { + if (util::has_uri_field(u, UF_HOST)) { + return memchr(uri.c_str() + u.field_data[UF_HOST].off, ':', + u.field_data[UF_HOST].len); + } else { + return false; + } +} + +Headers::value_type *Request::get_res_header(int32_t token) { + auto idx = res_hdidx[token]; + if (idx == -1) { + return nullptr; + } + return &res_nva[idx]; +} + +Headers::value_type *Request::get_req_header(int32_t token) { + auto idx = req_hdidx[token]; + if (idx == -1) { + return nullptr; + } + return &req_nva[idx]; +} + +void Request::record_request_start_time() { + timing.state = RequestState::ON_REQUEST; + timing.request_start_time = get_time(); +} + +void Request::record_response_start_time() { + timing.state = RequestState::ON_RESPONSE; + timing.response_start_time = get_time(); +} + +void Request::record_response_end_time() { + timing.state = RequestState::ON_COMPLETE; + timing.response_end_time = get_time(); +} + +namespace { +void continue_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast<HttpClient *>(ev_userdata(loop)); + auto req = static_cast<Request *>(w->data); + int error; + + error = nghttp2_submit_data(client->session, NGHTTP2_FLAG_END_STREAM, + req->stream_id, req->data_prd); + + if (error) { + std::cerr << "[ERROR] nghttp2_submit_data() returned error: " + << nghttp2_strerror(error) << std::endl; + nghttp2_submit_rst_stream(client->session, NGHTTP2_FLAG_NONE, + req->stream_id, NGHTTP2_INTERNAL_ERROR); + } + + client->signal_write(); +} +} // namespace + +ContinueTimer::ContinueTimer(struct ev_loop *loop, Request *req) : loop(loop) { + ev_timer_init(&timer, continue_timeout_cb, 1., 0.); + timer.data = req; +} + +ContinueTimer::~ContinueTimer() { stop(); } + +void ContinueTimer::start() { ev_timer_start(loop, &timer); } + +void ContinueTimer::stop() { ev_timer_stop(loop, &timer); } + +void ContinueTimer::dispatch_continue() { + // Only dispatch the timeout callback if it hasn't already been called. + if (ev_is_active(&timer)) { + ev_feed_event(loop, &timer, 0); + } +} + +namespace { +int htp_msg_begincb(llhttp_t *htp) { + if (config.verbose) { + print_timer(); + std::cout << " HTTP Upgrade response" << std::endl; + } + return 0; +} +} // namespace + +namespace { +int htp_msg_completecb(llhttp_t *htp) { + auto client = static_cast<HttpClient *>(htp->data); + client->upgrade_response_status_code = htp->status_code; + client->upgrade_response_complete = true; + return 0; +} +} // namespace + +namespace { +constexpr llhttp_settings_t htp_hooks = { + htp_msg_begincb, // llhttp_cb on_message_begin; + nullptr, // llhttp_data_cb on_url; + nullptr, // llhttp_data_cb on_status; + nullptr, // llhttp_data_cb on_method; + nullptr, // llhttp_data_cb on_version; + nullptr, // llhttp_data_cb on_header_field; + nullptr, // llhttp_data_cb on_header_value; + nullptr, // llhttp_data_cb on_chunk_extension_name; + nullptr, // llhttp_data_cb on_chunk_extension_value; + nullptr, // llhttp_cb on_headers_complete; + nullptr, // llhttp_data_cb on_body; + htp_msg_completecb, // llhttp_cb on_message_complete; + nullptr, // llhttp_cb on_url_complete; + nullptr, // llhttp_cb on_status_complete; + nullptr, // llhttp_cb on_method_complete; + nullptr, // llhttp_cb on_version_complete; + nullptr, // llhttp_cb on_header_field_complete; + nullptr, // llhttp_cb on_header_value_complete; + nullptr, // llhttp_cb on_chunk_extension_name_complete; + nullptr, // llhttp_cb on_chunk_extension_value_complete; + nullptr, // llhttp_cb on_chunk_header; + nullptr, // llhttp_cb on_chunk_complete; + nullptr, // llhttp_cb on_reset; +}; +} // namespace + +namespace { +int submit_request(HttpClient *client, const Headers &headers, Request *req) { + auto scheme = util::get_uri_field(req->uri.c_str(), req->u, UF_SCHEMA); + auto build_headers = Headers{{":method", req->data_prd ? "POST" : "GET"}, + {":path", req->make_reqpath()}, + {":scheme", scheme.str()}, + {":authority", client->hostport}, + {"accept", "*/*"}, + {"accept-encoding", "gzip, deflate"}, + {"user-agent", "nghttp2/" NGHTTP2_VERSION}}; + bool expect_continue = false; + + if (config.continuation) { + for (size_t i = 0; i < 6; ++i) { + build_headers.emplace_back("continuation-test-" + util::utos(i + 1), + std::string(4_k, '-')); + } + } + + auto num_initial_headers = build_headers.size(); + + if (req->data_prd) { + if (!config.no_content_length) { + build_headers.emplace_back("content-length", + util::utos(req->data_length)); + } + if (config.expect_continue) { + expect_continue = true; + build_headers.emplace_back("expect", "100-continue"); + } + } + + for (auto &kv : headers) { + size_t i; + for (i = 0; i < num_initial_headers; ++i) { + if (kv.name == build_headers[i].name) { + build_headers[i].value = kv.value; + break; + } + } + if (i < num_initial_headers) { + continue; + } + + build_headers.emplace_back(kv.name, kv.value, kv.no_index); + } + + auto nva = std::vector<nghttp2_nv>(); + nva.reserve(build_headers.size()); + + for (auto &kv : build_headers) { + nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); + } + + auto method = http2::get_header(build_headers, ":method"); + assert(method); + + req->method = method->value; + + std::string trailer_names; + if (!config.trailer.empty()) { + trailer_names = config.trailer[0].name; + for (size_t i = 1; i < config.trailer.size(); ++i) { + trailer_names += ", "; + trailer_names += config.trailer[i].name; + } + nva.push_back(http2::make_nv_ls("trailer", trailer_names)); + } + + int32_t stream_id; + + if (expect_continue) { + stream_id = nghttp2_submit_headers(client->session, 0, -1, &req->pri_spec, + nva.data(), nva.size(), req); + } else { + stream_id = + nghttp2_submit_request(client->session, &req->pri_spec, nva.data(), + nva.size(), req->data_prd, req); + } + + if (stream_id < 0) { + std::cerr << "[ERROR] nghttp2_submit_" + << (expect_continue ? "headers" : "request") + << "() returned error: " << nghttp2_strerror(stream_id) + << std::endl; + return -1; + } + + req->stream_id = stream_id; + client->request_done(req); + + req->req_nva = std::move(build_headers); + + if (expect_continue) { + auto timer = std::make_unique<ContinueTimer>(client->loop, req); + req->continue_timer = std::move(timer); + } + + return 0; +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto client = static_cast<HttpClient *>(w->data); + if (client->do_read() != 0) { + client->disconnect(); + } +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto client = static_cast<HttpClient *>(w->data); + auto rv = client->do_write(); + if (rv == HttpClient::ERR_CONNECT_FAIL) { + client->connect_fail(); + return; + } + if (rv != 0) { + client->disconnect(); + } +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast<HttpClient *>(w->data); + std::cerr << "[ERROR] Timeout" << std::endl; + client->disconnect(); +} +} // namespace + +namespace { +void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast<HttpClient *>(w->data); + ev_timer_stop(loop, w); + + nghttp2_session_terminate_session(client->session, NGHTTP2_SETTINGS_TIMEOUT); + + client->signal_write(); +} +} // namespace + +HttpClient::HttpClient(const nghttp2_session_callbacks *callbacks, + struct ev_loop *loop, SSL_CTX *ssl_ctx) + : wb(&mcpool), + session(nullptr), + callbacks(callbacks), + loop(loop), + ssl_ctx(ssl_ctx), + ssl(nullptr), + addrs(nullptr), + next_addr(nullptr), + cur_addr(nullptr), + complete(0), + success(0), + settings_payloadlen(0), + state(ClientState::IDLE), + upgrade_response_status_code(0), + fd(-1), + upgrade_response_complete(false) { + ev_io_init(&wev, writecb, 0, EV_WRITE); + ev_io_init(&rev, readcb, 0, EV_READ); + + wev.data = this; + rev.data = this; + + ev_timer_init(&wt, timeoutcb, 0., config.timeout); + ev_timer_init(&rt, timeoutcb, 0., config.timeout); + + wt.data = this; + rt.data = this; + + ev_timer_init(&settings_timer, settings_timeout_cb, 0., 10.); + + settings_timer.data = this; +} + +HttpClient::~HttpClient() { + disconnect(); + + if (addrs) { + freeaddrinfo(addrs); + addrs = nullptr; + next_addr = nullptr; + } +} + +bool HttpClient::need_upgrade() const { + return config.upgrade && scheme == "http"; +} + +int HttpClient::resolve_host(const std::string &host, uint16_t port) { + int rv; + this->host = host; + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + hints.ai_flags = AI_ADDRCONFIG; + rv = getaddrinfo(host.c_str(), util::utos(port).c_str(), &hints, &addrs); + if (rv != 0) { + std::cerr << "[ERROR] getaddrinfo() failed: " << gai_strerror(rv) + << std::endl; + return -1; + } + if (addrs == nullptr) { + std::cerr << "[ERROR] No address returned" << std::endl; + return -1; + } + next_addr = addrs; + return 0; +} + +namespace { +// Just returns 1 to continue handshake. +int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { return 1; } +} // namespace + +int HttpClient::initiate_connection() { + int rv; + + cur_addr = nullptr; + while (next_addr) { + cur_addr = next_addr; + next_addr = next_addr->ai_next; + fd = util::create_nonblock_socket(cur_addr->ai_family); + if (fd == -1) { + continue; + } + + if (ssl_ctx) { + // We are establishing TLS connection. + ssl = SSL_new(ssl_ctx); + if (!ssl) { + std::cerr << "[ERROR] SSL_new() failed: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + SSL_set_connect_state(ssl); + + // If the user overrode the :authority or host header, use that + // value for the SNI extension + const auto &host_string = + config.host_override.empty() ? host : config.host_override; + + auto param = SSL_get0_param(ssl); + X509_VERIFY_PARAM_set_hostflags(param, 0); + X509_VERIFY_PARAM_set1_host(param, host_string.c_str(), + host_string.size()); + SSL_set_verify(ssl, SSL_VERIFY_PEER, verify_cb); + + if (!util::numeric_host(host_string.c_str())) { + SSL_set_tlsext_host_name(ssl, host_string.c_str()); + } + } + + rv = connect(fd, cur_addr->ai_addr, cur_addr->ai_addrlen); + + if (rv != 0 && errno != EINPROGRESS) { + if (ssl) { + SSL_free(ssl); + ssl = nullptr; + } + close(fd); + fd = -1; + continue; + } + break; + } + + if (fd == -1) { + return -1; + } + + writefn = &HttpClient::connected; + + if (need_upgrade()) { + on_readfn = &HttpClient::on_upgrade_read; + on_writefn = &HttpClient::on_upgrade_connect; + } else { + on_readfn = &HttpClient::on_read; + on_writefn = &HttpClient::on_write; + } + + ev_io_set(&rev, fd, EV_READ); + ev_io_set(&wev, fd, EV_WRITE); + + ev_io_start(loop, &wev); + + ev_timer_again(loop, &wt); + + return 0; +} + +void HttpClient::disconnect() { + state = ClientState::IDLE; + + for (auto req = std::begin(reqvec); req != std::end(reqvec); ++req) { + if ((*req)->continue_timer) { + (*req)->continue_timer->stop(); + } + } + + ev_timer_stop(loop, &settings_timer); + + ev_timer_stop(loop, &rt); + ev_timer_stop(loop, &wt); + + ev_io_stop(loop, &rev); + ev_io_stop(loop, &wev); + + nghttp2_session_del(session); + session = nullptr; + + if (ssl) { + SSL_set_shutdown(ssl, SSL_get_shutdown(ssl) | SSL_RECEIVED_SHUTDOWN); + ERR_clear_error(); + SSL_shutdown(ssl); + SSL_free(ssl); + ssl = nullptr; + } + + if (fd != -1) { + shutdown(fd, SHUT_WR); + close(fd); + fd = -1; + } +} + +int HttpClient::read_clear() { + ev_timer_again(loop, &rt); + + std::array<uint8_t, 8_k> buf; + + for (;;) { + ssize_t nread; + while ((nread = read(fd, buf.data(), buf.size())) == -1 && errno == EINTR) + ; + if (nread == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return -1; + } + + if (nread == 0) { + return -1; + } + + if (on_readfn(*this, buf.data(), nread) != 0) { + return -1; + } + } + + return 0; +} + +int HttpClient::write_clear() { + ev_timer_again(loop, &rt); + + std::array<struct iovec, 2> iov; + + for (;;) { + if (on_writefn(*this) != 0) { + return -1; + } + + auto iovcnt = wb.riovec(iov.data(), iov.size()); + + if (iovcnt == 0) { + break; + } + + ssize_t nwrite; + while ((nwrite = writev(fd, iov.data(), iovcnt)) == -1 && errno == EINTR) + ; + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + ev_io_start(loop, &wev); + ev_timer_again(loop, &wt); + return 0; + } + return -1; + } + + wb.drain(nwrite); + } + + ev_io_stop(loop, &wev); + ev_timer_stop(loop, &wt); + + return 0; +} + +int HttpClient::noop() { return 0; } + +void HttpClient::connect_fail() { + if (state == ClientState::IDLE) { + std::cerr << "[ERROR] Could not connect to the address " + << util::numeric_name(cur_addr->ai_addr, cur_addr->ai_addrlen) + << std::endl; + } + auto cur_state = state; + disconnect(); + if (cur_state == ClientState::IDLE) { + if (initiate_connection() == 0) { + std::cerr << "Trying next address " + << util::numeric_name(cur_addr->ai_addr, cur_addr->ai_addrlen) + << std::endl; + } + } +} + +int HttpClient::connected() { + if (!util::check_socket_connected(fd)) { + return ERR_CONNECT_FAIL; + } + + if (config.verbose) { + print_timer(); + std::cout << " Connected" << std::endl; + } + + state = ClientState::CONNECTED; + + ev_io_start(loop, &rev); + ev_io_stop(loop, &wev); + + ev_timer_again(loop, &rt); + ev_timer_stop(loop, &wt); + + if (ssl) { + SSL_set_fd(ssl, fd); + + readfn = &HttpClient::tls_handshake; + writefn = &HttpClient::tls_handshake; + + return do_write(); + } + + readfn = &HttpClient::read_clear; + writefn = &HttpClient::write_clear; + + if (need_upgrade()) { + htp = std::make_unique<llhttp_t>(); + llhttp_init(htp.get(), HTTP_RESPONSE, &htp_hooks); + htp->data = this; + + return do_write(); + } + + if (connection_made() != 0) { + return -1; + } + + return 0; +} + +namespace { +size_t populate_settings(nghttp2_settings_entry *iv) { + size_t niv = 2; + + iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + iv[0].value = config.max_concurrent_streams; + + iv[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + if (config.window_bits != -1) { + iv[1].value = (1 << config.window_bits) - 1; + } else { + iv[1].value = NGHTTP2_INITIAL_WINDOW_SIZE; + } + + if (config.header_table_size >= 0) { + if (config.min_header_table_size < config.header_table_size) { + iv[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; + iv[niv].value = config.min_header_table_size; + ++niv; + } + + iv[niv].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; + iv[niv].value = config.header_table_size; + ++niv; + } + + if (config.no_push) { + iv[niv].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; + iv[niv].value = 0; + ++niv; + } + + if (config.no_rfc7540_pri) { + iv[niv].settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; + iv[niv].value = 1; + ++niv; + } + + return niv; +} +} // namespace + +int HttpClient::on_upgrade_connect() { + ssize_t rv; + record_connect_end_time(); + assert(!reqvec.empty()); + std::array<nghttp2_settings_entry, 16> iv; + size_t niv = populate_settings(iv.data()); + assert(settings_payload.size() >= 8 * niv); + rv = nghttp2_pack_settings_payload(settings_payload.data(), + settings_payload.size(), iv.data(), niv); + if (rv < 0) { + return -1; + } + settings_payloadlen = rv; + auto token68 = + base64::encode(std::begin(settings_payload), + std::begin(settings_payload) + settings_payloadlen); + util::to_token68(token68); + + std::string req; + if (reqvec[0]->data_prd) { + // If the request contains upload data, use OPTIONS * to upgrade + req = "OPTIONS *"; + } else { + auto meth = std::find_if( + std::begin(config.headers), std::end(config.headers), + [](const Header &kv) { return util::streq_l(":method", kv.name); }); + + if (meth == std::end(config.headers)) { + req = "GET "; + reqvec[0]->method = "GET"; + } else { + req = (*meth).value; + req += ' '; + reqvec[0]->method = (*meth).value; + } + req += reqvec[0]->make_reqpath(); + } + + auto headers = Headers{{"host", hostport}, + {"connection", "Upgrade, HTTP2-Settings"}, + {"upgrade", NGHTTP2_CLEARTEXT_PROTO_VERSION_ID}, + {"http2-settings", std::move(token68)}, + {"accept", "*/*"}, + {"user-agent", "nghttp2/" NGHTTP2_VERSION}}; + auto initial_headerslen = headers.size(); + + for (auto &kv : config.headers) { + size_t i; + if (kv.name.empty() || kv.name[0] == ':') { + continue; + } + for (i = 0; i < initial_headerslen; ++i) { + if (kv.name == headers[i].name) { + headers[i].value = kv.value; + break; + } + } + if (i < initial_headerslen) { + continue; + } + headers.emplace_back(kv.name, kv.value, kv.no_index); + } + + req += " HTTP/1.1\r\n"; + + for (auto &kv : headers) { + req += kv.name; + req += ": "; + req += kv.value; + req += "\r\n"; + } + req += "\r\n"; + + wb.append(req); + + if (config.verbose) { + print_timer(); + std::cout << " HTTP Upgrade request\n" << req << std::endl; + } + + if (!reqvec[0]->data_prd) { + // record request time if this is a part of real request. + reqvec[0]->record_request_start_time(); + reqvec[0]->req_nva = std::move(headers); + } + + on_writefn = &HttpClient::noop; + + signal_write(); + + return 0; +} + +int HttpClient::on_upgrade_read(const uint8_t *data, size_t len) { + int rv; + + auto htperr = + llhttp_execute(htp.get(), reinterpret_cast<const char *>(data), len); + auto nread = htperr == HPE_OK + ? len + : static_cast<size_t>(reinterpret_cast<const uint8_t *>( + llhttp_get_error_pos(htp.get())) - + data); + + if (config.verbose) { + std::cout.write(reinterpret_cast<const char *>(data), nread); + } + + if (htperr != HPE_OK && htperr != HPE_PAUSED_UPGRADE) { + std::cerr << "[ERROR] Failed to parse HTTP Upgrade response header: " + << "(" << llhttp_errno_name(htperr) << ") " + << llhttp_get_error_reason(htp.get()) << std::endl; + return -1; + } + + if (!upgrade_response_complete) { + return 0; + } + + if (config.verbose) { + std::cout << std::endl; + } + + if (upgrade_response_status_code != 101) { + std::cerr << "[ERROR] HTTP Upgrade failed" << std::endl; + + return -1; + } + + if (config.verbose) { + print_timer(); + std::cout << " HTTP Upgrade success" << std::endl; + } + + on_readfn = &HttpClient::on_read; + on_writefn = &HttpClient::on_write; + + rv = connection_made(); + if (rv != 0) { + return rv; + } + + // Read remaining data in the buffer because it is not notified + // callback anymore. + rv = on_readfn(*this, data + nread, len - nread); + if (rv != 0) { + return rv; + } + + return 0; +} + +int HttpClient::do_read() { return readfn(*this); } +int HttpClient::do_write() { return writefn(*this); } + +int HttpClient::connection_made() { + int rv; + + if (!need_upgrade()) { + record_connect_end_time(); + } + + if (ssl) { + // Check ALPN result + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len; + + SSL_get0_alpn_selected(ssl, &next_proto, &next_proto_len); + if (next_proto) { + auto proto = StringRef{next_proto, next_proto_len}; + if (config.verbose) { + std::cout << "The negotiated protocol: " << proto << std::endl; + } + if (!util::check_h2_is_selected(proto)) { + next_proto = nullptr; + } + } + if (!next_proto) { + print_protocol_nego_error(); + return -1; + } + } + + rv = nghttp2_session_client_new2(&session, callbacks, this, + config.http2_option); + + if (rv != 0) { + return -1; + } + if (need_upgrade()) { + // Adjust stream user-data depending on the existence of upload + // data + Request *stream_user_data = nullptr; + if (!reqvec[0]->data_prd) { + stream_user_data = reqvec[0].get(); + } + // If HEAD is used, that is only when user specified it with -H + // option. + auto head_request = stream_user_data && stream_user_data->method == "HEAD"; + rv = nghttp2_session_upgrade2(session, settings_payload.data(), + settings_payloadlen, head_request, + stream_user_data); + if (rv != 0) { + std::cerr << "[ERROR] nghttp2_session_upgrade() returned error: " + << nghttp2_strerror(rv) << std::endl; + return -1; + } + if (stream_user_data) { + stream_user_data->stream_id = 1; + request_done(stream_user_data); + } + } + // If upgrade succeeds, the SETTINGS value sent with + // HTTP2-Settings header field has already been submitted to + // session object. + if (!need_upgrade()) { + std::array<nghttp2_settings_entry, 16> iv; + auto niv = populate_settings(iv.data()); + rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv.data(), niv); + if (rv != 0) { + return -1; + } + } + if (!config.no_dep) { + // Create anchor stream nodes + nghttp2_priority_spec pri_spec; + + for (auto &anchor : anchors) { + nghttp2_priority_spec_init(&pri_spec, anchor.dep_stream_id, anchor.weight, + 0); + rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, anchor.stream_id, + &pri_spec); + if (rv != 0) { + return -1; + } + } + + rv = nghttp2_session_set_next_stream_id( + session, anchors[ANCHOR_FOLLOWERS].stream_id + 2); + if (rv != 0) { + return -1; + } + + if (need_upgrade() && !reqvec[0]->data_prd) { + // Amend the priority because we cannot send priority in + // HTTP/1.1 Upgrade. + auto &anchor = anchors[ANCHOR_FOLLOWERS]; + nghttp2_priority_spec_init(&pri_spec, anchor.stream_id, + reqvec[0]->pri_spec.weight, 0); + + rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec); + if (rv != 0) { + return -1; + } + } + } else if (need_upgrade() && !reqvec[0]->data_prd && + reqvec[0]->pri_spec.weight != NGHTTP2_DEFAULT_WEIGHT) { + // Amend the priority because we cannot send priority in HTTP/1.1 + // Upgrade. + nghttp2_priority_spec pri_spec; + + nghttp2_priority_spec_init(&pri_spec, 0, reqvec[0]->pri_spec.weight, 0); + + rv = nghttp2_submit_priority(session, NGHTTP2_FLAG_NONE, 1, &pri_spec); + if (rv != 0) { + return -1; + } + } + + ev_timer_again(loop, &settings_timer); + + if (config.connection_window_bits != -1) { + int32_t window_size = (1 << config.connection_window_bits) - 1; + rv = nghttp2_session_set_local_window_size(session, NGHTTP2_FLAG_NONE, 0, + window_size); + if (rv != 0) { + return -1; + } + } + // Adjust first request depending on the existence of the upload + // data + for (auto i = std::begin(reqvec) + (need_upgrade() && !reqvec[0]->data_prd); + i != std::end(reqvec); ++i) { + if (submit_request(this, config.headers, (*i).get()) != 0) { + return -1; + } + } + + signal_write(); + + return 0; +} + +int HttpClient::on_read(const uint8_t *data, size_t len) { + if (config.hexdump) { + util::hexdump(stdout, data, len); + } + + auto rv = nghttp2_session_mem_recv(session, data, len); + if (rv < 0) { + std::cerr << "[ERROR] nghttp2_session_mem_recv() returned error: " + << nghttp2_strerror(rv) << std::endl; + return -1; + } + + assert(static_cast<size_t>(rv) == len); + + if (nghttp2_session_want_read(session) == 0 && + nghttp2_session_want_write(session) == 0 && wb.rleft() == 0) { + return -1; + } + + signal_write(); + + return 0; +} + +int HttpClient::on_write() { + for (;;) { + if (wb.rleft() >= 16384) { + return 0; + } + + const uint8_t *data; + auto len = nghttp2_session_mem_send(session, &data); + if (len < 0) { + std::cerr << "[ERROR] nghttp2_session_send() returned error: " + << nghttp2_strerror(len) << std::endl; + return -1; + } + + if (len == 0) { + break; + } + + wb.append(data, len); + } + + if (nghttp2_session_want_read(session) == 0 && + nghttp2_session_want_write(session) == 0 && wb.rleft() == 0) { + return -1; + } + + return 0; +} + +int HttpClient::tls_handshake() { + ev_timer_again(loop, &rt); + + ERR_clear_error(); + + auto rv = SSL_do_handshake(ssl); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + ev_io_stop(loop, &wev); + ev_timer_stop(loop, &wt); + return 0; + case SSL_ERROR_WANT_WRITE: + ev_io_start(loop, &wev); + ev_timer_again(loop, &wt); + return 0; + default: + return -1; + } + } + + ev_io_stop(loop, &wev); + ev_timer_stop(loop, &wt); + + readfn = &HttpClient::read_tls; + writefn = &HttpClient::write_tls; + + if (config.verify_peer) { + auto verify_res = SSL_get_verify_result(ssl); + if (verify_res != X509_V_OK) { + std::cerr << "[WARNING] Certificate verification failed: " + << X509_verify_cert_error_string(verify_res) << std::endl; + } + } + + if (connection_made() != 0) { + return -1; + } + + return 0; +} + +int HttpClient::read_tls() { + ev_timer_again(loop, &rt); + + ERR_clear_error(); + + std::array<uint8_t, 8_k> buf; + for (;;) { + auto rv = SSL_read(ssl, buf.data(), buf.size()); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + return 0; + case SSL_ERROR_WANT_WRITE: + // renegotiation started + return -1; + default: + return -1; + } + } + + if (on_readfn(*this, buf.data(), rv) != 0) { + return -1; + } + } +} + +int HttpClient::write_tls() { + ev_timer_again(loop, &rt); + + ERR_clear_error(); + + struct iovec iov; + + for (;;) { + if (on_writefn(*this) != 0) { + return -1; + } + + auto iovcnt = wb.riovec(&iov, 1); + + if (iovcnt == 0) { + break; + } + + auto rv = SSL_write(ssl, iov.iov_base, iov.iov_len); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + // renegotiation started + return -1; + case SSL_ERROR_WANT_WRITE: + ev_io_start(loop, &wev); + ev_timer_again(loop, &wt); + return 0; + default: + return -1; + } + } + + wb.drain(rv); + } + + ev_io_stop(loop, &wev); + ev_timer_stop(loop, &wt); + + return 0; +} + +void HttpClient::signal_write() { ev_io_start(loop, &wev); } + +bool HttpClient::all_requests_processed() const { + return complete == reqvec.size(); +} + +void HttpClient::update_hostport() { + if (reqvec.empty()) { + return; + } + scheme = util::get_uri_field(reqvec[0]->uri.c_str(), reqvec[0]->u, UF_SCHEMA) + .str(); + std::stringstream ss; + if (reqvec[0]->is_ipv6_literal_addr()) { + // we may have zone ID, which must start with "%25", or "%". RFC + // 6874 defines "%25" only, and just "%" is allowed for just + // convenience to end-user input. + auto host = + util::get_uri_field(reqvec[0]->uri.c_str(), reqvec[0]->u, UF_HOST); + auto end = std::find(std::begin(host), std::end(host), '%'); + ss << "["; + ss.write(host.c_str(), end - std::begin(host)); + ss << "]"; + } else { + util::write_uri_field(ss, reqvec[0]->uri.c_str(), reqvec[0]->u, UF_HOST); + } + if (util::has_uri_field(reqvec[0]->u, UF_PORT) && + reqvec[0]->u.port != + util::get_default_port(reqvec[0]->uri.c_str(), reqvec[0]->u)) { + ss << ":" << reqvec[0]->u.port; + } + hostport = ss.str(); +} + +bool HttpClient::add_request(const std::string &uri, + const nghttp2_data_provider *data_prd, + int64_t data_length, + const nghttp2_priority_spec &pri_spec, int level) { + http_parser_url u{}; + if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { + return false; + } + if (path_cache.count(uri)) { + return false; + } + + if (config.multiply == 1) { + path_cache.insert(uri); + } + + reqvec.push_back(std::make_unique<Request>(uri, u, data_prd, data_length, + pri_spec, level)); + return true; +} + +void HttpClient::record_start_time() { + timing.system_start_time = std::chrono::system_clock::now(); + timing.start_time = get_time(); +} + +void HttpClient::record_domain_lookup_end_time() { + timing.domain_lookup_end_time = get_time(); +} + +void HttpClient::record_connect_end_time() { + timing.connect_end_time = get_time(); +} + +void HttpClient::request_done(Request *req) { + if (req->stream_id % 2 == 0) { + return; + } +} + +#ifdef HAVE_JANSSON +void HttpClient::output_har(FILE *outfile) { + static auto PAGE_ID = "page_0"; + + auto root = json_object(); + auto log = json_object(); + json_object_set_new(root, "log", log); + json_object_set_new(log, "version", json_string("1.2")); + + auto creator = json_object(); + json_object_set_new(log, "creator", creator); + + json_object_set_new(creator, "name", json_string("nghttp")); + json_object_set_new(creator, "version", json_string(NGHTTP2_VERSION)); + + auto pages = json_array(); + json_object_set_new(log, "pages", pages); + + auto page = json_object(); + json_array_append_new(pages, page); + + json_object_set_new( + page, "startedDateTime", + json_string(util::format_iso8601(timing.system_start_time).c_str())); + json_object_set_new(page, "id", json_string(PAGE_ID)); + json_object_set_new(page, "title", json_string("")); + + json_object_set_new(page, "pageTimings", json_object()); + + auto entries = json_array(); + json_object_set_new(log, "entries", entries); + + auto dns_delta = std::chrono::duration_cast<std::chrono::microseconds>( + timing.domain_lookup_end_time - timing.start_time) + .count() / + 1000.0; + auto connect_delta = + std::chrono::duration_cast<std::chrono::microseconds>( + timing.connect_end_time - timing.domain_lookup_end_time) + .count() / + 1000.0; + + for (size_t i = 0; i < reqvec.size(); ++i) { + auto &req = reqvec[i]; + + if (req->timing.state != RequestState::ON_COMPLETE) { + continue; + } + + auto entry = json_object(); + json_array_append_new(entries, entry); + + auto &req_timing = req->timing; + auto request_time = + (i == 0) ? timing.system_start_time + : timing.system_start_time + + std::chrono::duration_cast< + std::chrono::system_clock::duration>( + req_timing.request_start_time - timing.start_time); + + auto wait_delta = + std::chrono::duration_cast<std::chrono::microseconds>( + req_timing.response_start_time - req_timing.request_start_time) + .count() / + 1000.0; + auto receive_delta = + std::chrono::duration_cast<std::chrono::microseconds>( + req_timing.response_end_time - req_timing.response_start_time) + .count() / + 1000.0; + + auto time_sum = + std::chrono::duration_cast<std::chrono::microseconds>( + (i == 0) ? (req_timing.response_end_time - timing.start_time) + : (req_timing.response_end_time - + req_timing.request_start_time)) + .count() / + 1000.0; + + json_object_set_new( + entry, "startedDateTime", + json_string(util::format_iso8601(request_time).c_str())); + json_object_set_new(entry, "time", json_real(time_sum)); + + auto pushed = req->stream_id % 2 == 0; + + json_object_set_new(entry, "comment", + json_string(pushed ? "Pushed Object" : "")); + + auto request = json_object(); + json_object_set_new(entry, "request", request); + + auto req_headers = json_array(); + json_object_set_new(request, "headers", req_headers); + + for (auto &nv : req->req_nva) { + auto hd = json_object(); + json_array_append_new(req_headers, hd); + + json_object_set_new(hd, "name", json_string(nv.name.c_str())); + json_object_set_new(hd, "value", json_string(nv.value.c_str())); + } + + json_object_set_new(request, "method", json_string(req->method.c_str())); + json_object_set_new(request, "url", json_string(req->uri.c_str())); + json_object_set_new(request, "httpVersion", json_string("HTTP/2.0")); + json_object_set_new(request, "cookies", json_array()); + json_object_set_new(request, "queryString", json_array()); + json_object_set_new(request, "headersSize", json_integer(-1)); + json_object_set_new(request, "bodySize", json_integer(-1)); + + auto response = json_object(); + json_object_set_new(entry, "response", response); + + auto res_headers = json_array(); + json_object_set_new(response, "headers", res_headers); + + for (auto &nv : req->res_nva) { + auto hd = json_object(); + json_array_append_new(res_headers, hd); + + json_object_set_new(hd, "name", json_string(nv.name.c_str())); + json_object_set_new(hd, "value", json_string(nv.value.c_str())); + } + + json_object_set_new(response, "status", json_integer(req->status)); + json_object_set_new(response, "statusText", json_string("")); + json_object_set_new(response, "httpVersion", json_string("HTTP/2.0")); + json_object_set_new(response, "cookies", json_array()); + + auto content = json_object(); + json_object_set_new(response, "content", content); + + json_object_set_new(content, "size", json_integer(req->response_len)); + + auto content_type_ptr = http2::get_header(req->res_nva, "content-type"); + + const char *content_type = ""; + if (content_type_ptr) { + content_type = content_type_ptr->value.c_str(); + } + + json_object_set_new(content, "mimeType", json_string(content_type)); + + json_object_set_new(response, "redirectURL", json_string("")); + json_object_set_new(response, "headersSize", json_integer(-1)); + json_object_set_new(response, "bodySize", json_integer(-1)); + json_object_set_new(entry, "cache", json_object()); + + auto timings = json_object(); + json_object_set_new(entry, "timings", timings); + + auto dns_timing = (i == 0) ? dns_delta : 0; + auto connect_timing = (i == 0) ? connect_delta : 0; + + json_object_set_new(timings, "dns", json_real(dns_timing)); + json_object_set_new(timings, "connect", json_real(connect_timing)); + + json_object_set_new(timings, "blocked", json_real(0.0)); + json_object_set_new(timings, "send", json_real(0.0)); + json_object_set_new(timings, "wait", json_real(wait_delta)); + json_object_set_new(timings, "receive", json_real(receive_delta)); + + json_object_set_new(entry, "pageref", json_string(PAGE_ID)); + json_object_set_new(entry, "connection", + json_string(util::utos(req->stream_id).c_str())); + } + + json_dumpf(root, outfile, JSON_PRESERVE_ORDER | JSON_INDENT(2)); + json_decref(root); +} +#endif // HAVE_JANSSON + +namespace { +void update_html_parser(HttpClient *client, Request *req, const uint8_t *data, + size_t len, int fin) { + if (!req->html_parser) { + return; + } + req->update_html_parser(data, len, fin); + + auto scheme = req->get_real_scheme(); + auto host = req->get_real_host(); + auto port = req->get_real_port(); + + for (auto &p : req->html_parser->get_links()) { + auto uri = strip_fragment(p.first.c_str()); + auto res_type = p.second; + + http_parser_url u{}; + if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { + continue; + } + + if (!util::fieldeq(uri.c_str(), u, UF_SCHEMA, scheme) || + !util::fieldeq(uri.c_str(), u, UF_HOST, host)) { + continue; + } + + auto link_port = util::has_uri_field(u, UF_PORT) ? u.port + : scheme == "https" ? 443 + : 80; + + if (port != link_port) { + continue; + } + + // No POST data for assets + auto pri_spec = resolve_dep(res_type); + + if (client->add_request(uri, nullptr, 0, pri_spec, req->level + 1)) { + submit_request(client, config.headers, client->reqvec.back().get()); + } + } + req->html_parser->clear_links(); +} +} // namespace + +namespace { +HttpClient *get_client(void *user_data) { + return static_cast<HttpClient *>(user_data); +} +} // namespace + +namespace { +int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data) { + auto client = get_client(user_data); + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + + if (!req) { + return 0; + } + + if (config.verbose >= 2) { + verbose_on_data_chunk_recv_callback(session, flags, stream_id, data, len, + user_data); + } + + req->response_len += len; + + if (req->inflater) { + while (len > 0) { + const size_t MAX_OUTLEN = 4_k; + std::array<uint8_t, MAX_OUTLEN> out; + size_t outlen = MAX_OUTLEN; + size_t tlen = len; + int rv = + nghttp2_gzip_inflate(req->inflater, out.data(), &outlen, data, &tlen); + if (rv != 0) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, stream_id, + NGHTTP2_INTERNAL_ERROR); + break; + } + + if (!config.null_out) { + std::cout.write(reinterpret_cast<const char *>(out.data()), outlen); + } + + update_html_parser(client, req, out.data(), outlen, 0); + data += tlen; + len -= tlen; + } + + return 0; + } + + if (!config.null_out) { + std::cout.write(reinterpret_cast<const char *>(data), len); + } + + update_html_parser(client, req, data, len, 0); + + return 0; +} +} // namespace + +namespace { +ssize_t select_padding_callback(nghttp2_session *session, + const nghttp2_frame *frame, size_t max_payload, + void *user_data) { + return std::min(max_payload, frame->hd.length + config.padding); +} +} // namespace + +namespace { +void check_response_header(nghttp2_session *session, Request *req) { + bool gzip = false; + + req->expect_final_response = false; + + auto status_hd = req->get_res_header(http2::HD__STATUS); + + if (!status_hd) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, req->stream_id, + NGHTTP2_PROTOCOL_ERROR); + return; + } + + auto status = http2::parse_http_status_code(StringRef{status_hd->value}); + if (status == -1) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, req->stream_id, + NGHTTP2_PROTOCOL_ERROR); + return; + } + + req->status = status; + + for (auto &nv : req->res_nva) { + if ("content-encoding" == nv.name) { + gzip = util::strieq_l("gzip", nv.value) || + util::strieq_l("deflate", nv.value); + continue; + } + } + + if (req->status / 100 == 1) { + if (req->continue_timer && (req->status == 100)) { + // If the request is waiting for a 100 Continue, complete the handshake. + req->continue_timer->dispatch_continue(); + } + + req->expect_final_response = true; + req->status = 0; + req->res_nva.clear(); + http2::init_hdidx(req->res_hdidx); + return; + } else if (req->continue_timer) { + // A final response stops any pending Expect/Continue handshake. + req->continue_timer->stop(); + } + + if (gzip) { + if (!req->inflater) { + req->init_inflater(); + } + } + if (config.get_assets && req->level == 0) { + if (!req->html_parser) { + req->init_html_parser(); + } + } +} +} // namespace + +namespace { +int on_begin_headers_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + auto client = get_client(user_data); + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!req) { + break; + } + + switch (frame->headers.cat) { + case NGHTTP2_HCAT_RESPONSE: + case NGHTTP2_HCAT_PUSH_RESPONSE: + req->record_response_start_time(); + break; + default: + break; + } + + break; + } + case NGHTTP2_PUSH_PROMISE: { + auto stream_id = frame->push_promise.promised_stream_id; + http_parser_url u{}; + // TODO Set pri and level + nghttp2_priority_spec pri_spec; + + nghttp2_priority_spec_default_init(&pri_spec); + + auto req = std::make_unique<Request>("", u, nullptr, 0, pri_spec); + req->stream_id = stream_id; + + nghttp2_session_set_stream_user_data(session, stream_id, req.get()); + + client->request_done(req.get()); + req->record_request_start_time(); + client->reqvec.push_back(std::move(req)); + + break; + } + } + return 0; +} +} // namespace + +namespace { +int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, + const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen, uint8_t flags, + void *user_data) { + if (config.verbose) { + verbose_on_header_callback(session, frame, name, namelen, value, valuelen, + flags, user_data); + } + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + + if (!req) { + break; + } + + /* ignore trailer header */ + if (frame->headers.cat == NGHTTP2_HCAT_HEADERS && + !req->expect_final_response) { + break; + } + + if (req->header_buffer_size + namelen + valuelen > 64_k) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + return 0; + } + + req->header_buffer_size += namelen + valuelen; + + auto token = http2::lookup_token(name, namelen); + + http2::index_header(req->res_hdidx, token, req->res_nva.size()); + http2::add_header(req->res_nva, name, namelen, value, valuelen, + flags & NGHTTP2_NV_FLAG_NO_INDEX, token); + break; + } + case NGHTTP2_PUSH_PROMISE: { + auto req = static_cast<Request *>(nghttp2_session_get_stream_user_data( + session, frame->push_promise.promised_stream_id)); + + if (!req) { + break; + } + + if (req->header_buffer_size + namelen + valuelen > 64_k) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + frame->push_promise.promised_stream_id, + NGHTTP2_INTERNAL_ERROR); + return 0; + } + + req->header_buffer_size += namelen + valuelen; + + auto token = http2::lookup_token(name, namelen); + + http2::index_header(req->req_hdidx, token, req->req_nva.size()); + http2::add_header(req->req_nva, name, namelen, value, valuelen, + flags & NGHTTP2_NV_FLAG_NO_INDEX, token); + break; + } + } + return 0; +} +} // namespace + +namespace { +int on_frame_recv_callback2(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + int rv = 0; + + if (config.verbose) { + verbose_on_frame_recv_callback(session, frame, user_data); + } + + auto client = get_client(user_data); + switch (frame->hd.type) { + case NGHTTP2_DATA: { + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!req) { + return 0; + ; + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + req->record_response_end_time(); + ++client->success; + } + + break; + } + case NGHTTP2_HEADERS: { + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + // If this is the HTTP Upgrade with OPTIONS method to avoid POST, + // req is nullptr. + if (!req) { + return 0; + ; + } + + switch (frame->headers.cat) { + case NGHTTP2_HCAT_RESPONSE: + case NGHTTP2_HCAT_PUSH_RESPONSE: + check_response_header(session, req); + break; + case NGHTTP2_HCAT_HEADERS: + if (req->expect_final_response) { + check_response_header(session, req); + break; + } + if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + frame->hd.stream_id, NGHTTP2_PROTOCOL_ERROR); + return 0; + } + break; + default: + assert(0); + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + req->record_response_end_time(); + ++client->success; + } + + break; + } + case NGHTTP2_SETTINGS: + if ((frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { + break; + } + ev_timer_stop(client->loop, &client->settings_timer); + break; + case NGHTTP2_PUSH_PROMISE: { + auto req = static_cast<Request *>(nghttp2_session_get_stream_user_data( + session, frame->push_promise.promised_stream_id)); + if (!req) { + break; + } + + // Reset for response header field reception + req->header_buffer_size = 0; + + auto scheme = req->get_req_header(http2::HD__SCHEME); + auto authority = req->get_req_header(http2::HD__AUTHORITY); + auto path = req->get_req_header(http2::HD__PATH); + + if (!authority) { + authority = req->get_req_header(http2::HD_HOST); + } + + // libnghttp2 guarantees :scheme, :method, :path and (:authority | + // host) exist and non-empty. + if (path->value[0] != '/') { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + frame->push_promise.promised_stream_id, + NGHTTP2_PROTOCOL_ERROR); + break; + } + std::string uri = scheme->value; + uri += "://"; + uri += authority->value; + uri += path->value; + http_parser_url u{}; + if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + frame->push_promise.promised_stream_id, + NGHTTP2_PROTOCOL_ERROR); + break; + } + req->uri = uri; + req->u = u; + + if (client->path_cache.count(uri)) { + nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE, + frame->push_promise.promised_stream_id, + NGHTTP2_CANCEL); + break; + } + + if (config.multiply == 1) { + client->path_cache.insert(uri); + } + + break; + } + } + return rv; +} +} // namespace + +namespace { +int before_frame_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + assert(req); + req->record_request_start_time(); + return 0; +} + +} // namespace + +namespace { +int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + if (config.verbose) { + verbose_on_frame_send_callback(session, frame, user_data); + } + + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!req) { + return 0; + } + + // If this request is using Expect/Continue, start its ContinueTimer. + if (req->continue_timer) { + req->continue_timer->start(); + } + + return 0; +} +} // namespace + +namespace { +int on_frame_not_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, int lib_error_code, + void *user_data) { + if (frame->hd.type != NGHTTP2_HEADERS || + frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!req) { + return 0; + } + + std::cerr << "[ERROR] request " << req->uri + << " failed: " << nghttp2_strerror(lib_error_code) << std::endl; + + return 0; +} +} // namespace + +namespace { +int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *user_data) { + auto client = get_client(user_data); + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + + if (!req) { + return 0; + } + + // If this request is using Expect/Continue, stop its ContinueTimer. + if (req->continue_timer) { + req->continue_timer->stop(); + } + + update_html_parser(client, req, nullptr, 0, 1); + ++client->complete; + + if (client->all_requests_processed()) { + nghttp2_session_terminate_session(session, NGHTTP2_NO_ERROR); + } + + return 0; +} +} // namespace + +struct RequestResult { + std::chrono::microseconds time; +}; + +namespace { +void print_stats(const HttpClient &client) { + std::cout << "***** Statistics *****" << std::endl; + + std::vector<Request *> reqs; + reqs.reserve(client.reqvec.size()); + for (const auto &req : client.reqvec) { + if (req->timing.state == RequestState::ON_COMPLETE) { + reqs.push_back(req.get()); + } + } + + std::sort(std::begin(reqs), std::end(reqs), + [](const Request *lhs, const Request *rhs) { + const auto <iming = lhs->timing; + const auto &rtiming = rhs->timing; + return ltiming.response_end_time < rtiming.response_end_time || + (ltiming.response_end_time == rtiming.response_end_time && + ltiming.request_start_time < rtiming.request_start_time); + }); + + std::cout << R"( +Request timing: + responseEnd: the time when last byte of response was received + relative to connectEnd + requestStart: the time just before first byte of request was sent + relative to connectEnd. If '*' is shown, this was + pushed by server. + process: responseEnd - requestStart + code: HTTP status code + size: number of bytes received as response body without + inflation. + URI: request URI + +see http://www.w3.org/TR/resource-timing/#processing-model + +sorted by 'complete' + +id responseEnd requestStart process code size request path)" + << std::endl; + + const auto &base = client.timing.connect_end_time; + for (const auto &req : reqs) { + auto response_end = std::chrono::duration_cast<std::chrono::microseconds>( + req->timing.response_end_time - base); + auto request_start = std::chrono::duration_cast<std::chrono::microseconds>( + req->timing.request_start_time - base); + auto total = std::chrono::duration_cast<std::chrono::microseconds>( + req->timing.response_end_time - req->timing.request_start_time); + auto pushed = req->stream_id % 2 == 0; + + std::cout << std::setw(3) << req->stream_id << " " << std::setw(11) + << ("+" + util::format_duration(response_end)) << " " + << (pushed ? "*" : " ") << std::setw(11) + << ("+" + util::format_duration(request_start)) << " " + << std::setw(8) << util::format_duration(total) << " " + << std::setw(4) << req->status << " " << std::setw(4) + << util::utos_unit(req->response_len) << " " + << req->make_reqpath() << std::endl; + } +} +} // namespace + +namespace { +int communicate( + const std::string &scheme, const std::string &host, uint16_t port, + std::vector< + std::tuple<std::string, nghttp2_data_provider *, int64_t, int32_t>> + requests, + const nghttp2_session_callbacks *callbacks) { + int result = 0; + auto loop = EV_DEFAULT; + SSL_CTX *ssl_ctx = nullptr; + if (scheme == "https") { + ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx) { + std::cerr << "[ERROR] Failed to create SSL_CTX: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + result = -1; + goto fin; + } + + auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; + +#ifdef SSL_OP_ENABLE_KTLS + if (config.ktls) { + ssl_opts |= SSL_OP_ENABLE_KTLS; + } +#endif // SSL_OP_ENABLE_KTLS + + SSL_CTX_set_options(ssl_ctx, ssl_opts); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + if (SSL_CTX_set_default_verify_paths(ssl_ctx) != 1) { + std::cerr << "[WARNING] Could not load system trusted CA certificates: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + } + + if (nghttp2::tls::ssl_ctx_set_proto_versions( + ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, + nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { + std::cerr << "[ERROR] Could not set TLS versions" << std::endl; + result = -1; + goto fin; + } + + if (SSL_CTX_set_cipher_list(ssl_ctx, tls::DEFAULT_CIPHER_LIST) == 0) { + std::cerr << "[ERROR] " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + result = -1; + goto fin; + } + if (!config.keyfile.empty()) { + if (SSL_CTX_use_PrivateKey_file(ssl_ctx, config.keyfile.c_str(), + SSL_FILETYPE_PEM) != 1) { + std::cerr << "[ERROR] " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + result = -1; + goto fin; + } + } + if (!config.certfile.empty()) { + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, + config.certfile.c_str()) != 1) { + std::cerr << "[ERROR] " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + result = -1; + goto fin; + } + } + + auto proto_list = util::get_default_alpn(); + + SSL_CTX_set_alpn_protos(ssl_ctx, proto_list.data(), proto_list.size()); + } + { + HttpClient client{callbacks, loop, ssl_ctx}; + + int32_t dep_stream_id = 0; + + if (!config.no_dep) { + dep_stream_id = anchors[ANCHOR_FOLLOWERS].stream_id; + } + + for (auto &req : requests) { + nghttp2_priority_spec pri_spec; + + nghttp2_priority_spec_init(&pri_spec, dep_stream_id, std::get<3>(req), 0); + + for (int i = 0; i < config.multiply; ++i) { + client.add_request(std::get<0>(req), std::get<1>(req), std::get<2>(req), + pri_spec); + } + } + client.update_hostport(); + + client.record_start_time(); + + if (client.resolve_host(host, port) != 0) { + goto fin; + } + + client.record_domain_lookup_end_time(); + + if (client.initiate_connection() != 0) { + std::cerr << "[ERROR] Could not connect to " << host << ", port " << port + << std::endl; + goto fin; + } + + ev_set_userdata(loop, &client); + ev_run(loop, 0); + ev_set_userdata(loop, nullptr); + +#ifdef HAVE_JANSSON + if (!config.harfile.empty()) { + FILE *outfile; + if (config.harfile == "-") { + outfile = stdout; + } else { + outfile = fopen(config.harfile.c_str(), "wb"); + } + + if (outfile) { + client.output_har(outfile); + + if (outfile != stdout) { + fclose(outfile); + } + } else { + std::cerr << "Cannot open file " << config.harfile << ". " + << "har file could not be created." << std::endl; + } + } +#endif // HAVE_JANSSON + + if (client.success != client.reqvec.size()) { + std::cerr << "Some requests were not processed. total=" + << client.reqvec.size() << ", processed=" << client.success + << std::endl; + } + if (config.stat) { + print_stats(client); + } + } +fin: + if (ssl_ctx) { + SSL_CTX_free(ssl_ctx); + } + return result; +} +} // namespace + +namespace { +ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, + uint8_t *buf, size_t length, uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) { + int rv; + auto req = static_cast<Request *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + assert(req); + int fd = source->fd; + ssize_t nread; + + while ((nread = pread(fd, buf, length, req->data_offset)) == -1 && + errno == EINTR) + ; + + if (nread == -1) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + req->data_offset += nread; + + if (req->data_offset == req->data_length) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + if (!config.trailer.empty()) { + std::vector<nghttp2_nv> nva; + nva.reserve(config.trailer.size()); + for (auto &kv : config.trailer) { + nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); + } + rv = nghttp2_submit_trailer(session, stream_id, nva.data(), nva.size()); + if (rv != 0) { + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } else { + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + } + } + + return nread; + } + + if (req->data_offset > req->data_length || nread == 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + return nread; +} +} // namespace + +namespace { +int run(char **uris, int n) { + nghttp2_session_callbacks *callbacks; + + nghttp2_session_callbacks_new(&callbacks); + auto cbsdel = defer(nghttp2_session_callbacks_del, callbacks); + + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback2); + + if (config.verbose) { + nghttp2_session_callbacks_set_on_invalid_frame_recv_callback( + callbacks, verbose_on_invalid_frame_recv_callback); + + nghttp2_session_callbacks_set_error_callback2(callbacks, + verbose_error_callback); + } + + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); + + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, on_begin_headers_callback); + + nghttp2_session_callbacks_set_on_header_callback(callbacks, + on_header_callback); + + nghttp2_session_callbacks_set_before_frame_send_callback( + callbacks, before_frame_send_callback); + + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, + on_frame_send_callback); + + nghttp2_session_callbacks_set_on_frame_not_send_callback( + callbacks, on_frame_not_send_callback); + + if (config.padding) { + nghttp2_session_callbacks_set_select_padding_callback( + callbacks, select_padding_callback); + } + + std::string prev_scheme; + std::string prev_host; + uint16_t prev_port = 0; + int failures = 0; + int data_fd = -1; + nghttp2_data_provider data_prd; + struct stat data_stat; + + if (!config.datafile.empty()) { + if (config.datafile == "-") { + if (fstat(0, &data_stat) == 0 && + (data_stat.st_mode & S_IFMT) == S_IFREG) { + // use STDIN if it is a regular file + data_fd = 0; + } else { + // copy the contents of STDIN to a temporary file + char tempfn[] = "/tmp/nghttp.temp.XXXXXX"; + data_fd = mkstemp(tempfn); + if (data_fd == -1) { + std::cerr << "[ERROR] Could not create a temporary file in /tmp" + << std::endl; + return 1; + } + if (unlink(tempfn) != 0) { + std::cerr << "[WARNING] failed to unlink temporary file:" << tempfn + << std::endl; + } + while (1) { + std::array<char, 1_k> buf; + ssize_t rret, wret; + while ((rret = read(0, buf.data(), buf.size())) == -1 && + errno == EINTR) + ; + if (rret == 0) + break; + if (rret == -1) { + std::cerr << "[ERROR] I/O error while reading from STDIN" + << std::endl; + return 1; + } + while ((wret = write(data_fd, buf.data(), rret)) == -1 && + errno == EINTR) + ; + if (wret != rret) { + std::cerr << "[ERROR] I/O error while writing to temporary file" + << std::endl; + return 1; + } + } + if (fstat(data_fd, &data_stat) == -1) { + close(data_fd); + std::cerr << "[ERROR] Could not stat temporary file" << std::endl; + return 1; + } + } + } else { + data_fd = open(config.datafile.c_str(), O_RDONLY | O_BINARY); + if (data_fd == -1) { + std::cerr << "[ERROR] Could not open file " << config.datafile + << std::endl; + return 1; + } + if (fstat(data_fd, &data_stat) == -1) { + close(data_fd); + std::cerr << "[ERROR] Could not stat file " << config.datafile + << std::endl; + return 1; + } + } + data_prd.source.fd = data_fd; + data_prd.read_callback = file_read_callback; + } + std::vector< + std::tuple<std::string, nghttp2_data_provider *, int64_t, int32_t>> + requests; + + size_t next_weight_idx = 0; + + for (int i = 0; i < n; ++i) { + http_parser_url u{}; + auto uri = strip_fragment(uris[i]); + if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { + ++next_weight_idx; + std::cerr << "[ERROR] Could not parse URI " << uri << std::endl; + continue; + } + if (!util::has_uri_field(u, UF_SCHEMA)) { + ++next_weight_idx; + std::cerr << "[ERROR] URI " << uri << " does not have scheme part" + << std::endl; + continue; + } + auto port = util::has_uri_field(u, UF_PORT) + ? u.port + : util::get_default_port(uri.c_str(), u); + auto host = decode_host(util::get_uri_field(uri.c_str(), u, UF_HOST)); + if (!util::fieldeq(uri.c_str(), u, UF_SCHEMA, prev_scheme.c_str()) || + host != prev_host || port != prev_port) { + if (!requests.empty()) { + if (communicate(prev_scheme, prev_host, prev_port, std::move(requests), + callbacks) != 0) { + ++failures; + } + requests.clear(); + } + prev_scheme = util::get_uri_field(uri.c_str(), u, UF_SCHEMA).str(); + prev_host = std::move(host); + prev_port = port; + } + requests.emplace_back(uri, data_fd == -1 ? nullptr : &data_prd, + data_stat.st_size, config.weight[next_weight_idx++]); + } + if (!requests.empty()) { + if (communicate(prev_scheme, prev_host, prev_port, std::move(requests), + callbacks) != 0) { + ++failures; + } + } + return failures; +} +} // namespace + +namespace { +void print_version(std::ostream &out) { + out << "nghttp nghttp2/" NGHTTP2_VERSION << std::endl; +} +} // namespace + +namespace { +void print_usage(std::ostream &out) { + out << R"(Usage: nghttp [OPTIONS]... <URI>... +HTTP/2 client)" + << std::endl; +} +} // namespace + +namespace { +void print_help(std::ostream &out) { + print_usage(out); + out << R"( + <URI> Specify URI to access. +Options: + -v, --verbose + Print debug information such as reception and + transmission of frames and name/value pairs. Specifying + this option multiple times increases verbosity. + -n, --null-out + Discard downloaded data. + -O, --remote-name + Save download data in the current directory. The + filename is derived from URI. If URI ends with '/', + 'index.html' is used as a filename. Not implemented + yet. + -t, --timeout=<DURATION> + Timeout each request after <DURATION>. Set 0 to disable + timeout. + -w, --window-bits=<N> + Sets the stream level initial window size to 2**<N>-1. + -W, --connection-window-bits=<N> + Sets the connection level initial window size to + 2**<N>-1. + -a, --get-assets + Download assets such as stylesheets, images and script + files linked from the downloaded resource. Only links + whose origins are the same with the linking resource + will be downloaded. nghttp prioritizes resources using + HTTP/2 dependency based priority. The priority order, + from highest to lowest, is html itself, css, javascript + and images. + -s, --stat Print statistics. + -H, --header=<HEADER> + Add a header to the requests. Example: -H':method: PUT' + --trailer=<HEADER> + Add a trailer header to the requests. <HEADER> must not + include pseudo header field (header field name starting + with ':'). To send trailer, one must use -d option to + send request body. Example: --trailer 'foo: bar'. + --cert=<CERT> + Use the specified client certificate file. The file + must be in PEM format. + --key=<KEY> Use the client private key file. The file must be in + PEM format. + -d, --data=<PATH> + Post FILE to server. If '-' is given, data will be read + from stdin. + -m, --multiply=<N> + Request each URI <N> times. By default, same URI is not + requested twice. This option disables it too. + -u, --upgrade + Perform HTTP Upgrade for HTTP/2. This option is ignored + if the request URI has https scheme. If -d is used, the + HTTP upgrade request is performed with OPTIONS method. + -p, --weight=<WEIGHT> + Sets weight of given URI. This option can be used + multiple times, and N-th -p option sets weight of N-th + URI in the command line. If the number of -p option is + less than the number of URI, the last -p option value is + repeated. If there is no -p option, default weight, 16, + is assumed. The valid value range is + [)" + << NGHTTP2_MIN_WEIGHT << ", " << NGHTTP2_MAX_WEIGHT << R"(], inclusive. + -M, --peer-max-concurrent-streams=<N> + Use <N> as SETTINGS_MAX_CONCURRENT_STREAMS value of + remote endpoint as if it is received in SETTINGS frame. + Default: 100 + -c, --header-table-size=<SIZE> + Specify decoder header table size. If this option is + used multiple times, and the minimum value among the + given values except for last one is strictly less than + the last value, that minimum value is set in SETTINGS + frame payload before the last value, to simulate + multiple header table size change. + --encoder-header-table-size=<SIZE> + Specify encoder header table size. The decoder (server) + specifies the maximum dynamic table size it accepts. + Then the negotiated dynamic table size is the minimum of + this option value and the value which server specified. + -b, --padding=<N> + Add at most <N> bytes to a frame payload as padding. + Specify 0 to disable padding. + -r, --har=<PATH> + Output HTTP transactions <PATH> in HAR format. If '-' + is given, data is written to stdout. + --color Force colored log output. + --continuation + Send large header to test CONTINUATION. + --no-content-length + Don't send content-length header field. + --no-dep Don't send dependency based priority hint to server. + --hexdump Display the incoming traffic in hexadecimal (Canonical + hex+ASCII display). If SSL/TLS is used, decrypted data + are used. + --no-push Disable server push. + --max-concurrent-streams=<N> + The number of concurrent pushed streams this client + accepts. + --expect-continue + Perform an Expect/Continue handshake: wait to send DATA + (up to a short timeout) until the server sends a 100 + Continue interim response. This option is ignored unless + combined with the -d option. + -y, --no-verify-peer + Suppress warning on server certificate verification + failure. + --ktls Enable ktls. + --no-rfc7540-pri + Disable RFC7540 priorities. + --version Display version information and exit. + -h, --help Display this help and exit. + +-- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms + (hours, minutes, seconds and milliseconds, respectively). If a unit + is omitted, a second is used as unit.)" + << std::endl; +} +} // namespace + +int main(int argc, char **argv) { + bool color = false; + while (1) { + static int flag = 0; + constexpr static option long_options[] = { + {"verbose", no_argument, nullptr, 'v'}, + {"null-out", no_argument, nullptr, 'n'}, + {"remote-name", no_argument, nullptr, 'O'}, + {"timeout", required_argument, nullptr, 't'}, + {"window-bits", required_argument, nullptr, 'w'}, + {"connection-window-bits", required_argument, nullptr, 'W'}, + {"get-assets", no_argument, nullptr, 'a'}, + {"stat", no_argument, nullptr, 's'}, + {"help", no_argument, nullptr, 'h'}, + {"header", required_argument, nullptr, 'H'}, + {"data", required_argument, nullptr, 'd'}, + {"multiply", required_argument, nullptr, 'm'}, + {"upgrade", no_argument, nullptr, 'u'}, + {"weight", required_argument, nullptr, 'p'}, + {"peer-max-concurrent-streams", required_argument, nullptr, 'M'}, + {"header-table-size", required_argument, nullptr, 'c'}, + {"padding", required_argument, nullptr, 'b'}, + {"har", required_argument, nullptr, 'r'}, + {"no-verify-peer", no_argument, nullptr, 'y'}, + {"cert", required_argument, &flag, 1}, + {"key", required_argument, &flag, 2}, + {"color", no_argument, &flag, 3}, + {"continuation", no_argument, &flag, 4}, + {"version", no_argument, &flag, 5}, + {"no-content-length", no_argument, &flag, 6}, + {"no-dep", no_argument, &flag, 7}, + {"trailer", required_argument, &flag, 9}, + {"hexdump", no_argument, &flag, 10}, + {"no-push", no_argument, &flag, 11}, + {"max-concurrent-streams", required_argument, &flag, 12}, + {"expect-continue", no_argument, &flag, 13}, + {"encoder-header-table-size", required_argument, &flag, 14}, + {"ktls", no_argument, &flag, 15}, + {"no-rfc7540-pri", no_argument, &flag, 16}, + {nullptr, 0, nullptr, 0}}; + int option_index = 0; + int c = + getopt_long(argc, argv, "M:Oab:c:d:m:np:r:hH:vst:uw:yW:", long_options, + &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'M': { + // peer-max-concurrent-streams option + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-M: Bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.peer_max_concurrent_streams = n; + break; + } + case 'O': + config.remote_name = true; + break; + case 'h': + print_help(std::cout); + exit(EXIT_SUCCESS); + case 'b': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-b: Bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.padding = n; + break; + } + case 'n': + config.null_out = true; + break; + case 'p': { + auto n = util::parse_uint(optarg); + if (n == -1 || NGHTTP2_MIN_WEIGHT > n || n > NGHTTP2_MAX_WEIGHT) { + std::cerr << "-p: specify the integer in the range [" + << NGHTTP2_MIN_WEIGHT << ", " << NGHTTP2_MAX_WEIGHT + << "], inclusive" << std::endl; + exit(EXIT_FAILURE); + } + config.weight.push_back(n); + break; + } + case 'r': +#ifdef HAVE_JANSSON + config.harfile = optarg; +#else // !HAVE_JANSSON + std::cerr << "[WARNING]: -r, --har option is ignored because\n" + << "the binary was not compiled with libjansson." << std::endl; +#endif // !HAVE_JANSSON + break; + case 'v': + ++config.verbose; + break; + case 't': + config.timeout = util::parse_duration_with_unit(optarg); + if (config.timeout == std::numeric_limits<double>::infinity()) { + std::cerr << "-t: bad timeout value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 'u': + config.upgrade = true; + break; + case 'w': + case 'W': { + auto n = util::parse_uint(optarg); + if (n == -1 || n > 30) { + std::cerr << "-" << static_cast<char>(c) + << ": specify the integer in the range [0, 30], inclusive" + << std::endl; + exit(EXIT_FAILURE); + } + if (c == 'w') { + config.window_bits = n; + } else { + config.connection_window_bits = n; + } + break; + } + case 'H': { + char *header = optarg; + // Skip first possible ':' in the header name + char *value = strchr(optarg + 1, ':'); + if (!value || (header[0] == ':' && header + 1 == value)) { + std::cerr << "-H: invalid header: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + *value = 0; + value++; + while (isspace(*value)) { + value++; + } + if (*value == 0) { + // This could also be a valid case for suppressing a header + // similar to curl + std::cerr << "-H: invalid header - value missing: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + config.headers.emplace_back(header, value, false); + util::inp_strlower(config.headers.back().name); + break; + } + case 'a': +#ifdef HAVE_LIBXML2 + config.get_assets = true; +#else // !HAVE_LIBXML2 + std::cerr << "[WARNING]: -a, --get-assets option is ignored because\n" + << "the binary was not compiled with libxml2." << std::endl; +#endif // !HAVE_LIBXML2 + break; + case 's': + config.stat = true; + break; + case 'd': + config.datafile = optarg; + break; + case 'm': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-m: Bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.multiply = n; + break; + } + case 'c': { + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "-c: Bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + if (n > std::numeric_limits<uint32_t>::max()) { + std::cerr << "-c: Value too large. It should be less than or equal to " + << std::numeric_limits<uint32_t>::max() << std::endl; + exit(EXIT_FAILURE); + } + config.header_table_size = n; + config.min_header_table_size = std::min(config.min_header_table_size, n); + break; + } + case 'y': + config.verify_peer = false; + break; + case '?': + util::show_candidates(argv[optind - 1], long_options); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // cert option + config.certfile = optarg; + break; + case 2: + // key option + config.keyfile = optarg; + break; + case 3: + // color option + color = true; + break; + case 4: + // continuation option + config.continuation = true; + break; + case 5: + // version option + print_version(std::cout); + exit(EXIT_SUCCESS); + case 6: + // no-content-length option + config.no_content_length = true; + break; + case 7: + // no-dep option + config.no_dep = true; + break; + case 9: { + // trailer option + auto header = optarg; + auto value = strchr(optarg, ':'); + if (!value) { + std::cerr << "--trailer: invalid header: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + *value = 0; + value++; + while (isspace(*value)) { + value++; + } + if (*value == 0) { + // This could also be a valid case for suppressing a header + // similar to curl + std::cerr << "--trailer: invalid header - value missing: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + config.trailer.emplace_back(header, value, false); + util::inp_strlower(config.trailer.back().name); + break; + } + case 10: + // hexdump option + config.hexdump = true; + break; + case 11: + // no-push option + config.no_push = true; + break; + case 12: { + // max-concurrent-streams option + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "--max-concurrent-streams: Bad option value: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + config.max_concurrent_streams = n; + break; + } + case 13: + // expect-continue option + config.expect_continue = true; + break; + case 14: { + // encoder-header-table-size option + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--encoder-header-table-size: Bad option value: " + << optarg << std::endl; + exit(EXIT_FAILURE); + } + if (n > std::numeric_limits<uint32_t>::max()) { + std::cerr << "--encoder-header-table-size: Value too large. It " + "should be less than or equal to " + << std::numeric_limits<uint32_t>::max() << std::endl; + exit(EXIT_FAILURE); + } + config.encoder_header_table_size = n; + break; + } + case 15: + // ktls option + config.ktls = true; + break; + case 16: + // no-rfc7540-pri option + config.no_rfc7540_pri = true; + break; + } + break; + default: + break; + } + } + + int32_t weight_to_fill; + if (config.weight.empty()) { + weight_to_fill = NGHTTP2_DEFAULT_WEIGHT; + } else { + weight_to_fill = config.weight.back(); + } + config.weight.insert(std::end(config.weight), argc - optind, weight_to_fill); + + // Find scheme overridden by extra header fields. + auto scheme_it = + std::find_if(std::begin(config.headers), std::end(config.headers), + [](const Header &nv) { return nv.name == ":scheme"; }); + if (scheme_it != std::end(config.headers)) { + config.scheme_override = (*scheme_it).value; + } + + // Find host and port overridden by extra header fields. + auto authority_it = + std::find_if(std::begin(config.headers), std::end(config.headers), + [](const Header &nv) { return nv.name == ":authority"; }); + if (authority_it == std::end(config.headers)) { + authority_it = + std::find_if(std::begin(config.headers), std::end(config.headers), + [](const Header &nv) { return nv.name == "host"; }); + } + + if (authority_it != std::end(config.headers)) { + // authority_it may looks like "host:port". + auto uri = "https://" + (*authority_it).value; + http_parser_url u{}; + if (http_parser_parse_url(uri.c_str(), uri.size(), 0, &u) != 0) { + std::cerr << "[ERROR] Could not parse authority in " + << (*authority_it).name << ": " << (*authority_it).value + << std::endl; + exit(EXIT_FAILURE); + } + + config.host_override = util::get_uri_field(uri.c_str(), u, UF_HOST).str(); + if (util::has_uri_field(u, UF_PORT)) { + config.port_override = u.port; + } + } + + set_color_output(color || isatty(fileno(stdout))); + + nghttp2_option_set_peer_max_concurrent_streams( + config.http2_option, config.peer_max_concurrent_streams); + + if (config.encoder_header_table_size != -1) { + nghttp2_option_set_max_deflate_dynamic_table_size( + config.http2_option, config.encoder_header_table_size); + } + + struct sigaction act {}; + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, nullptr); + reset_timer(); + return run(argv + optind, argc - optind); +} + +} // namespace nghttp2 + +int main(int argc, char **argv) { + return nghttp2::run_app(nghttp2::main, argc, argv); +} diff --git a/src/nghttp.h b/src/nghttp.h new file mode 100644 index 0000000..a880414 --- /dev/null +++ b/src/nghttp.h @@ -0,0 +1,310 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP_H +#define NGHTTP_H + +#include "nghttp2_config.h" + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H + +#include <string> +#include <vector> +#include <set> +#include <chrono> +#include <memory> + +#include <openssl/ssl.h> + +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#include "llhttp.h" + +#include "memchunk.h" +#include "http2.h" +#include "nghttp2_gzip.h" +#include "template.h" + +namespace nghttp2 { + +class HtmlParser; + +struct Config { + Config(); + ~Config(); + + Headers headers; + Headers trailer; + std::vector<int32_t> weight; + std::string certfile; + std::string keyfile; + std::string datafile; + std::string harfile; + std::string scheme_override; + std::string host_override; + nghttp2_option *http2_option; + int64_t header_table_size; + int64_t min_header_table_size; + int64_t encoder_header_table_size; + size_t padding; + size_t max_concurrent_streams; + ssize_t peer_max_concurrent_streams; + int multiply; + // milliseconds + ev_tstamp timeout; + int window_bits; + int connection_window_bits; + int verbose; + uint16_t port_override; + bool null_out; + bool remote_name; + bool get_assets; + bool stat; + bool upgrade; + bool continuation; + bool no_content_length; + bool no_dep; + bool hexdump; + bool no_push; + bool expect_continue; + bool verify_peer; + bool ktls; + bool no_rfc7540_pri; +}; + +enum class RequestState { INITIAL, ON_REQUEST, ON_RESPONSE, ON_COMPLETE }; + +struct RequestTiming { + // The point in time when request is started to be sent. + // Corresponds to requestStart in Resource Timing TR. + std::chrono::steady_clock::time_point request_start_time; + // The point in time when first byte of response is received. + // Corresponds to responseStart in Resource Timing TR. + std::chrono::steady_clock::time_point response_start_time; + // The point in time when last byte of response is received. + // Corresponds to responseEnd in Resource Timing TR. + std::chrono::steady_clock::time_point response_end_time; + RequestState state; + RequestTiming() : state(RequestState::INITIAL) {} +}; + +struct Request; // forward declaration for ContinueTimer + +struct ContinueTimer { + ContinueTimer(struct ev_loop *loop, Request *req); + ~ContinueTimer(); + + void start(); + void stop(); + + // Schedules an immediate run of the continue callback on the loop, if the + // callback has not already been run + void dispatch_continue(); + + struct ev_loop *loop; + ev_timer timer; +}; + +struct Request { + // For pushed request, |uri| is empty and |u| is zero-cleared. + Request(const std::string &uri, const http_parser_url &u, + const nghttp2_data_provider *data_prd, int64_t data_length, + const nghttp2_priority_spec &pri_spec, int level = 0); + ~Request(); + + void init_inflater(); + + void init_html_parser(); + int update_html_parser(const uint8_t *data, size_t len, int fin); + + std::string make_reqpath() const; + + bool is_ipv6_literal_addr() const; + + Headers::value_type *get_res_header(int32_t token); + Headers::value_type *get_req_header(int32_t token); + + void record_request_start_time(); + void record_response_start_time(); + void record_response_end_time(); + + // Returns scheme taking into account overridden scheme. + StringRef get_real_scheme() const; + // Returns request host, without port, taking into account + // overridden host. + StringRef get_real_host() const; + // Returns request port, taking into account overridden host, port, + // and scheme. + uint16_t get_real_port() const; + + Headers res_nva; + Headers req_nva; + std::string method; + // URI without fragment + std::string uri; + http_parser_url u; + nghttp2_priority_spec pri_spec; + RequestTiming timing; + int64_t data_length; + int64_t data_offset; + // Number of bytes received from server + int64_t response_len; + nghttp2_gzip *inflater; + std::unique_ptr<HtmlParser> html_parser; + const nghttp2_data_provider *data_prd; + size_t header_buffer_size; + int32_t stream_id; + int status; + // Recursion level: 0: first entity, 1: entity linked from first entity + int level; + http2::HeaderIndex res_hdidx; + // used for incoming PUSH_PROMISE + http2::HeaderIndex req_hdidx; + bool expect_final_response; + // only assigned if this request is using Expect/Continue + std::unique_ptr<ContinueTimer> continue_timer; +}; + +struct SessionTiming { + // The point in time when operation was started. Corresponds to + // startTime in Resource Timing TR, but recorded in system clock time. + std::chrono::system_clock::time_point system_start_time; + // Same as above, but recorded in steady clock time. + std::chrono::steady_clock::time_point start_time; + // The point in time when DNS resolution was completed. Corresponds + // to domainLookupEnd in Resource Timing TR. + std::chrono::steady_clock::time_point domain_lookup_end_time; + // The point in time when connection was established or SSL/TLS + // handshake was completed. Corresponds to connectEnd in Resource + // Timing TR. + std::chrono::steady_clock::time_point connect_end_time; +}; + +enum class ClientState { IDLE, CONNECTED }; + +struct HttpClient { + HttpClient(const nghttp2_session_callbacks *callbacks, struct ev_loop *loop, + SSL_CTX *ssl_ctx); + ~HttpClient(); + + bool need_upgrade() const; + int resolve_host(const std::string &host, uint16_t port); + int initiate_connection(); + void disconnect(); + + int noop(); + int read_clear(); + int write_clear(); + int connected(); + int tls_handshake(); + int read_tls(); + int write_tls(); + + int do_read(); + int do_write(); + + int on_upgrade_connect(); + int on_upgrade_read(const uint8_t *data, size_t len); + int on_read(const uint8_t *data, size_t len); + int on_write(); + + int connection_made(); + void connect_fail(); + void request_done(Request *req); + + void signal_write(); + + bool all_requests_processed() const; + void update_hostport(); + bool add_request(const std::string &uri, + const nghttp2_data_provider *data_prd, int64_t data_length, + const nghttp2_priority_spec &pri_spec, int level = 0); + + void record_start_time(); + void record_domain_lookup_end_time(); + void record_connect_end_time(); + +#ifdef HAVE_JANSSON + void output_har(FILE *outfile); +#endif // HAVE_JANSSON + + MemchunkPool mcpool; + DefaultMemchunks wb; + std::vector<std::unique_ptr<Request>> reqvec; + // Insert path already added in reqvec to prevent multiple request + // for 1 resource. + std::set<std::string> path_cache; + std::string scheme; + std::string host; + std::string hostport; + // Used for parse the HTTP upgrade response from server + std::unique_ptr<llhttp_t> htp; + SessionTiming timing; + ev_io wev; + ev_io rev; + ev_timer wt; + ev_timer rt; + ev_timer settings_timer; + std::function<int(HttpClient &)> readfn, writefn; + std::function<int(HttpClient &, const uint8_t *, size_t)> on_readfn; + std::function<int(HttpClient &)> on_writefn; + nghttp2_session *session; + const nghttp2_session_callbacks *callbacks; + struct ev_loop *loop; + SSL_CTX *ssl_ctx; + SSL *ssl; + addrinfo *addrs; + addrinfo *next_addr; + addrinfo *cur_addr; + // The number of completed requests, including failed ones. + size_t complete; + // The number of requests that local endpoint received END_STREAM + // from peer. + size_t success; + // The length of settings_payload + size_t settings_payloadlen; + ClientState state; + // The HTTP status code of the response message of HTTP Upgrade. + unsigned int upgrade_response_status_code; + int fd; + // true if the response message of HTTP Upgrade request is fully + // received. It is not relevant the upgrade succeeds, or not. + bool upgrade_response_complete; + // SETTINGS payload sent as token68 in HTTP Upgrade + std::array<uint8_t, 128> settings_payload; + + enum { ERR_CONNECT_FAIL = -100 }; +}; + +} // namespace nghttp2 + +#endif // NGHTTP_H diff --git a/src/nghttp2_config.h b/src/nghttp2_config.h new file mode 100644 index 0000000..8e6cd05 --- /dev/null +++ b/src/nghttp2_config.h @@ -0,0 +1,32 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP2_CONFIG_H +#define NGHTTP2_CONFIG_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#endif // NGHTTP2_CONFIG_H diff --git a/src/nghttp2_gzip.c b/src/nghttp2_gzip.c new file mode 100644 index 0000000..5f17ab2 --- /dev/null +++ b/src/nghttp2_gzip.c @@ -0,0 +1,87 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp2_gzip.h" + +#include <assert.h> + +int nghttp2_gzip_inflate_new(nghttp2_gzip **inflater_ptr) { + int rv; + *inflater_ptr = calloc(1, sizeof(nghttp2_gzip)); + if (*inflater_ptr == NULL) { + return -1; + } + rv = inflateInit2(&(*inflater_ptr)->zst, 47); + if (rv != Z_OK) { + free(*inflater_ptr); + return -1; + } + return 0; +} + +void nghttp2_gzip_inflate_del(nghttp2_gzip *inflater) { + if (inflater != NULL) { + inflateEnd(&inflater->zst); + free(inflater); + } +} + +int nghttp2_gzip_inflate(nghttp2_gzip *inflater, uint8_t *out, + size_t *outlen_ptr, const uint8_t *in, + size_t *inlen_ptr) { + int rv; + if (inflater->finished) { + return -1; + } + inflater->zst.avail_in = (unsigned int)*inlen_ptr; + inflater->zst.next_in = (unsigned char *)in; + inflater->zst.avail_out = (unsigned int)*outlen_ptr; + inflater->zst.next_out = out; + + rv = inflate(&inflater->zst, Z_NO_FLUSH); + + *inlen_ptr -= inflater->zst.avail_in; + *outlen_ptr -= inflater->zst.avail_out; + switch (rv) { + case Z_STREAM_END: + inflater->finished = 1; + /* FALL THROUGH */ + case Z_OK: + case Z_BUF_ERROR: + return 0; + case Z_DATA_ERROR: + case Z_STREAM_ERROR: + case Z_NEED_DICT: + case Z_MEM_ERROR: + return -1; + default: + assert(0); + /* We need this for some compilers */ + return 0; + } +} + +int nghttp2_gzip_inflate_finished(nghttp2_gzip *inflater) { + return inflater->finished; +} diff --git a/src/nghttp2_gzip.h b/src/nghttp2_gzip.h new file mode 100644 index 0000000..a40352c --- /dev/null +++ b/src/nghttp2_gzip.h @@ -0,0 +1,122 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP2_GZIP_H + +# ifdef HAVE_CONFIG_H +# include <config.h> +# endif /* HAVE_CONFIG_H */ +# include <zlib.h> + +# include <nghttp2/nghttp2.h> + +# ifdef __cplusplus +extern "C" { +# endif + +/** + * @struct + * + * The gzip stream to inflate data. + */ +typedef struct { + z_stream zst; + int8_t finished; +} nghttp2_gzip; + +/** + * @function + * + * A helper function to set up a per request gzip stream to inflate + * data. + * + * This function returns 0 if it succeeds, or -1. + */ +int nghttp2_gzip_inflate_new(nghttp2_gzip **inflater_ptr); + +/** + * @function + * + * Frees the inflate stream. The |inflater| may be ``NULL``. + */ +void nghttp2_gzip_inflate_del(nghttp2_gzip *inflater); + +/** + * @function + * + * Inflates data in |in| with the length |*inlen_ptr| and stores the + * inflated data to |out| which has allocated size at least + * |*outlen_ptr|. On return, |*outlen_ptr| is updated to represent + * the number of data written in |out|. Similarly, |*inlen_ptr| is + * updated to represent the number of input bytes processed. + * + * This function returns 0 if it succeeds, or -1. + * + * The example follows:: + * + * void on_data_chunk_recv_callback(nghttp2_session *session, + * uint8_t flags, + * int32_t stream_id, + * const uint8_t *data, size_t len, + * void *user_data) + * { + * ... + * req = nghttp2_session_get_stream_user_data(session, stream_id); + * nghttp2_gzip *inflater = req->inflater; + * while(len > 0) { + * uint8_t out[MAX_OUTLEN]; + * size_t outlen = MAX_OUTLEN; + * size_t tlen = len; + * int rv; + * rv = nghttp2_gzip_inflate(inflater, out, &outlen, data, &tlen); + * if(rv != 0) { + * nghttp2_submit_rst_stream(session, stream_id, + * NGHTTP2_INTERNAL_ERROR); + * break; + * } + * ... Do stuff ... + * data += tlen; + * len -= tlen; + * } + * .... + * } + */ +int nghttp2_gzip_inflate(nghttp2_gzip *inflater, uint8_t *out, + size_t *outlen_ptr, const uint8_t *in, + size_t *inlen_ptr); + +/** + * @function + * + * Returns nonzero if |inflater| sees the end of deflate stream. + * After this function returns nonzero, `nghttp2_gzip_inflate()` with + * |inflater| gets to return error. + */ +int nghttp2_gzip_inflate_finished(nghttp2_gzip *inflater); + +# ifdef __cplusplus +} +# endif + +#endif /* NGHTTP2_GZIP_H */ diff --git a/src/nghttp2_gzip_test.c b/src/nghttp2_gzip_test.c new file mode 100644 index 0000000..de19d5d --- /dev/null +++ b/src/nghttp2_gzip_test.c @@ -0,0 +1,111 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp2_gzip_test.h" + +#include <stdio.h> +#include <assert.h> + +#include <CUnit/CUnit.h> + +#include <zlib.h> + +#include "nghttp2_gzip.h" + +static size_t deflate_data(uint8_t *out, size_t outlen, const uint8_t *in, + size_t inlen) { + int rv; + z_stream zst = {0}; + + rv = deflateInit(&zst, Z_DEFAULT_COMPRESSION); + CU_ASSERT(rv == Z_OK); + + zst.avail_in = (unsigned int)inlen; + zst.next_in = (uint8_t *)in; + zst.avail_out = (unsigned int)outlen; + zst.next_out = out; + rv = deflate(&zst, Z_SYNC_FLUSH); + CU_ASSERT(rv == Z_OK); + + deflateEnd(&zst); + + return outlen - zst.avail_out; +} + +static const char input[] = + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND " + "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF " + "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND " + "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE " + "LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION " + "OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " + "WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."; + +void test_nghttp2_gzip_inflate(void) { + nghttp2_gzip *inflater; + uint8_t in[4096], out[4096], *inptr; + size_t inlen = sizeof(in); + size_t inproclen, outproclen; + const char *inputptr = input; + + inlen = deflate_data(in, inlen, (const uint8_t *)input, sizeof(input) - 1); + + CU_ASSERT(0 == nghttp2_gzip_inflate_new(&inflater)); + /* First 16 bytes */ + inptr = in; + inproclen = inlen; + outproclen = 16; + CU_ASSERT( + 0 == nghttp2_gzip_inflate(inflater, out, &outproclen, inptr, &inproclen)); + CU_ASSERT(16 == outproclen); + CU_ASSERT(inproclen > 0); + CU_ASSERT(0 == memcmp(inputptr, out, outproclen)); + /* Next 32 bytes */ + inptr += inproclen; + inlen -= inproclen; + inproclen = inlen; + inputptr += outproclen; + outproclen = 32; + CU_ASSERT( + 0 == nghttp2_gzip_inflate(inflater, out, &outproclen, inptr, &inproclen)); + CU_ASSERT(32 == outproclen); + CU_ASSERT(inproclen > 0); + CU_ASSERT(0 == memcmp(inputptr, out, outproclen)); + /* Rest */ + inptr += inproclen; + inlen -= inproclen; + inproclen = inlen; + inputptr += outproclen; + outproclen = sizeof(out); + CU_ASSERT( + 0 == nghttp2_gzip_inflate(inflater, out, &outproclen, inptr, &inproclen)); + CU_ASSERT(sizeof(input) - 49 == outproclen); + CU_ASSERT(inproclen > 0); + CU_ASSERT(0 == memcmp(inputptr, out, outproclen)); + + inlen -= inproclen; + CU_ASSERT(0 == inlen); + + nghttp2_gzip_inflate_del(inflater); +} diff --git a/src/nghttp2_gzip_test.h b/src/nghttp2_gzip_test.h new file mode 100644 index 0000000..8d554f7 --- /dev/null +++ b/src/nghttp2_gzip_test.h @@ -0,0 +1,42 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGHTTP2_GZIP_TEST_H +#define NGHTTP2_GZIP_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#ifdef __cplusplus +extern "C" { +#endif + +void test_nghttp2_gzip_inflate(void); + +#ifdef __cplusplus +} +#endif + +#endif /* NGHTTP2_GZIP_TEST_H */ diff --git a/src/nghttpd.cc b/src/nghttpd.cc new file mode 100644 index 0000000..9cad874 --- /dev/null +++ b/src/nghttpd.cc @@ -0,0 +1,502 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "nghttp2_config.h" + +#ifdef __sgi +# define daemon _daemonize +#endif + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <signal.h> +#include <getopt.h> + +#include <cstdlib> +#include <cstring> +#include <cassert> +#include <string> +#include <iostream> +#include <string> + +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <nghttp2/nghttp2.h> + +#include "app_helper.h" +#include "HttpServer.h" +#include "util.h" +#include "tls.h" + +namespace nghttp2 { + +namespace { +int parse_push_config(Config &config, const char *optarg) { + const char *eq = strchr(optarg, '='); + if (eq == nullptr) { + return -1; + } + auto &paths = config.push[std::string(optarg, eq)]; + auto optarg_end = optarg + strlen(optarg); + auto i = eq + 1; + for (;;) { + const char *j = strchr(i, ','); + if (j == nullptr) { + j = optarg_end; + } + paths.emplace_back(i, j); + if (j == optarg_end) { + break; + } + i = j; + ++i; + } + + return 0; +} +} // namespace + +namespace { +void print_version(std::ostream &out) { + out << "nghttpd nghttp2/" NGHTTP2_VERSION << std::endl; +} +} // namespace + +namespace { +void print_usage(std::ostream &out) { + out << "Usage: nghttpd [OPTION]... <PORT> [<PRIVATE_KEY> <CERT>]\n" + << "HTTP/2 server" << std::endl; +} +} // namespace + +namespace { +void print_help(std::ostream &out) { + Config config; + print_usage(out); + out << R"( + <PORT> Specify listening port number. + <PRIVATE_KEY> + Set path to server's private key. Required unless + --no-tls is specified. + <CERT> Set path to server's certificate. Required unless + --no-tls is specified. +Options: + -a, --address=<ADDR> + The address to bind to. If not specified the default IP + address determined by getaddrinfo is used. + -D, --daemon + Run in a background. If -D is used, the current working + directory is changed to '/'. Therefore if this option + is used, -d option must be specified. + -V, --verify-client + The server sends a client certificate request. If the + client did not return a certificate, the handshake is + terminated. Currently, this option just requests a + client certificate and does not verify it. + -d, --htdocs=<PATH> + Specify document root. If this option is not specified, + the document root is the current working directory. + -v, --verbose + Print debug information such as reception/ transmission + of frames and name/value pairs. + --no-tls Disable SSL/TLS. + -c, --header-table-size=<SIZE> + Specify decoder header table size. + --encoder-header-table-size=<SIZE> + Specify encoder header table size. The decoder (client) + specifies the maximum dynamic table size it accepts. + Then the negotiated dynamic table size is the minimum of + this option value and the value which client specified. + --color Force colored log output. + -p, --push=<PATH>=<PUSH_PATH,...> + Push resources <PUSH_PATH>s when <PATH> is requested. + This option can be used repeatedly to specify multiple + push configurations. <PATH> and <PUSH_PATH>s are + relative to document root. See --htdocs option. + Example: -p/=/foo.png -p/doc=/bar.css + -b, --padding=<N> + Add at most <N> bytes to a frame payload as padding. + Specify 0 to disable padding. + -m, --max-concurrent-streams=<N> + Set the maximum number of the concurrent streams in one + HTTP/2 session. + Default: )" + << config.max_concurrent_streams << R"( + -n, --workers=<N> + Set the number of worker threads. + Default: 1 + -e, --error-gzip + Make error response gzipped. + -w, --window-bits=<N> + Sets the stream level initial window size to 2**<N>-1. + -W, --connection-window-bits=<N> + Sets the connection level initial window size to + 2**<N>-1. + --dh-param-file=<PATH> + Path to file that contains DH parameters in PEM format. + Without this option, DHE cipher suites are not + available. + --early-response + Start sending response when request HEADERS is received, + rather than complete request is received. + --trailer=<HEADER> + Add a trailer header to a response. <HEADER> must not + include pseudo header field (header field name starting + with ':'). The trailer is sent only if a response has + body part. Example: --trailer 'foo: bar'. + --hexdump Display the incoming traffic in hexadecimal (Canonical + hex+ASCII display). If SSL/TLS is used, decrypted data + are used. + --echo-upload + Send back uploaded content if method is POST or PUT. + --mime-types-file=<PATH> + Path to file that contains MIME media types and the + extensions that represent them. + Default: )" + << config.mime_types_file << R"( + --no-content-length + Don't send content-length header field. + --ktls Enable ktls. + --no-rfc7540-pri + Disable RFC7540 priorities. + --version Display version information and exit. + -h, --help Display this help and exit. + +-- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024).)" + << std::endl; +} +} // namespace + +int main(int argc, char **argv) { + Config config; + bool color = false; + auto mime_types_file_set_manually = false; + + while (1) { + static int flag = 0; + constexpr static option long_options[] = { + {"address", required_argument, nullptr, 'a'}, + {"daemon", no_argument, nullptr, 'D'}, + {"htdocs", required_argument, nullptr, 'd'}, + {"help", no_argument, nullptr, 'h'}, + {"verbose", no_argument, nullptr, 'v'}, + {"verify-client", no_argument, nullptr, 'V'}, + {"header-table-size", required_argument, nullptr, 'c'}, + {"push", required_argument, nullptr, 'p'}, + {"padding", required_argument, nullptr, 'b'}, + {"max-concurrent-streams", required_argument, nullptr, 'm'}, + {"workers", required_argument, nullptr, 'n'}, + {"error-gzip", no_argument, nullptr, 'e'}, + {"window-bits", required_argument, nullptr, 'w'}, + {"connection-window-bits", required_argument, nullptr, 'W'}, + {"no-tls", no_argument, &flag, 1}, + {"color", no_argument, &flag, 2}, + {"version", no_argument, &flag, 3}, + {"dh-param-file", required_argument, &flag, 4}, + {"early-response", no_argument, &flag, 5}, + {"trailer", required_argument, &flag, 6}, + {"hexdump", no_argument, &flag, 7}, + {"echo-upload", no_argument, &flag, 8}, + {"mime-types-file", required_argument, &flag, 9}, + {"no-content-length", no_argument, &flag, 10}, + {"encoder-header-table-size", required_argument, &flag, 11}, + {"ktls", no_argument, &flag, 12}, + {"no-rfc7540-pri", no_argument, &flag, 13}, + {nullptr, 0, nullptr, 0}}; + int option_index = 0; + int c = getopt_long(argc, argv, "DVb:c:d:ehm:n:p:va:w:W:", long_options, + &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'a': + config.address = optarg; + break; + case 'D': + config.daemon = true; + break; + case 'V': + config.verify_client = true; + break; + case 'b': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-b: Bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.padding = n; + break; + } + case 'd': + config.htdocs = optarg; + break; + case 'e': + config.error_gzip = true; + break; + case 'm': { + // max-concurrent-streams option + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-m: invalid argument: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.max_concurrent_streams = n; + break; + } + case 'n': { +#ifdef NOTHREADS + std::cerr << "-n: WARNING: Threading disabled at build time, " + << "no threads created." << std::endl; +#else + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-n: Bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.num_worker = n; +#endif // NOTHREADS + break; + } + case 'h': + print_help(std::cout); + exit(EXIT_SUCCESS); + case 'v': + config.verbose = true; + break; + case 'c': { + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "-c: Bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + if (n > std::numeric_limits<uint32_t>::max()) { + std::cerr << "-c: Value too large. It should be less than or equal to " + << std::numeric_limits<uint32_t>::max() << std::endl; + exit(EXIT_FAILURE); + } + config.header_table_size = n; + break; + } + case 'p': + if (parse_push_config(config, optarg) != 0) { + std::cerr << "-p: Bad option value: " << optarg << std::endl; + } + break; + case 'w': + case 'W': { + auto n = util::parse_uint(optarg); + if (n == -1 || n > 30) { + std::cerr << "-" << static_cast<char>(c) + << ": specify the integer in the range [0, 30], inclusive" + << std::endl; + exit(EXIT_FAILURE); + } + + if (c == 'w') { + config.window_bits = n; + } else { + config.connection_window_bits = n; + } + + break; + } + case '?': + util::show_candidates(argv[optind - 1], long_options); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // no-tls option + config.no_tls = true; + break; + case 2: + // color option + color = true; + break; + case 3: + // version + print_version(std::cout); + exit(EXIT_SUCCESS); + case 4: + // dh-param-file + config.dh_param_file = optarg; + break; + case 5: + // early-response + config.early_response = true; + break; + case 6: { + // trailer option + auto header = optarg; + auto value = strchr(optarg, ':'); + if (!value) { + std::cerr << "--trailer: invalid header: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + *value = 0; + value++; + while (isspace(*value)) { + value++; + } + if (*value == 0) { + // This could also be a valid case for suppressing a header + // similar to curl + std::cerr << "--trailer: invalid header - value missing: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + config.trailer.emplace_back(header, value, false); + util::inp_strlower(config.trailer.back().name); + break; + } + case 7: + // hexdump option + config.hexdump = true; + break; + case 8: + // echo-upload option + config.echo_upload = true; + break; + case 9: + // mime-types-file option + mime_types_file_set_manually = true; + config.mime_types_file = optarg; + break; + case 10: + // no-content-length option + config.no_content_length = true; + break; + case 11: { + // encoder-header-table-size option + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--encoder-header-table-size: Bad option value: " + << optarg << std::endl; + exit(EXIT_FAILURE); + } + if (n > std::numeric_limits<uint32_t>::max()) { + std::cerr << "--encoder-header-table-size: Value too large. It " + "should be less than or equal to " + << std::numeric_limits<uint32_t>::max() << std::endl; + exit(EXIT_FAILURE); + } + config.encoder_header_table_size = n; + break; + } + case 12: + // tls option + config.ktls = true; + break; + case 13: + // no-rfc7540-pri option + config.no_rfc7540_pri = true; + break; + } + break; + default: + break; + } + } + if (argc - optind < (config.no_tls ? 1 : 3)) { + print_usage(std::cerr); + std::cerr << "Too few arguments" << std::endl; + exit(EXIT_FAILURE); + } + + { + auto portStr = argv[optind++]; + auto n = util::parse_uint(portStr); + if (n == -1 || n > std::numeric_limits<uint16_t>::max()) { + std::cerr << "<PORT>: Bad value: " << portStr << std::endl; + exit(EXIT_FAILURE); + } + config.port = n; + } + + if (!config.no_tls) { + config.private_key_file = argv[optind++]; + config.cert_file = argv[optind++]; + } + + if (config.daemon) { + if (config.htdocs.empty()) { + print_usage(std::cerr); + std::cerr << "-d option must be specified when -D is used." << std::endl; + exit(EXIT_FAILURE); + } +#ifdef __sgi + if (daemon(0, 0, 0, 0) == -1) { +#else + if (util::daemonize(0, 0) == -1) { +#endif + perror("daemon"); + exit(EXIT_FAILURE); + } + } + if (config.htdocs.empty()) { + config.htdocs = "./"; + } + + if (util::read_mime_types(config.mime_types, + config.mime_types_file.c_str()) != 0) { + if (mime_types_file_set_manually) { + std::cerr << "--mime-types-file: Could not open mime types file: " + << config.mime_types_file << std::endl; + } + } + + auto &trailer_names = config.trailer_names; + for (auto &h : config.trailer) { + trailer_names += h.name; + trailer_names += ", "; + } + if (trailer_names.size() >= 2) { + trailer_names.resize(trailer_names.size() - 2); + } + + set_color_output(color || isatty(fileno(stdout))); + + struct sigaction act {}; + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, nullptr); + + reset_timer(); + + HttpServer server(&config); + if (server.run() != 0) { + exit(EXIT_FAILURE); + } + return 0; +} + +} // namespace nghttp2 + +int main(int argc, char **argv) { + return nghttp2::run_app(nghttp2::main, argc, argv); +} diff --git a/src/quic.cc b/src/quic.cc new file mode 100644 index 0000000..1c68a5b --- /dev/null +++ b/src/quic.cc @@ -0,0 +1,60 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "quic.h" + +#include <cassert> + +#include <ngtcp2/ngtcp2.h> +#include <nghttp3/nghttp3.h> + +#include "template.h" + +using namespace nghttp2; + +namespace quic { + +Error err_transport(int liberr) { + if (liberr == NGTCP2_ERR_RECV_VERSION_NEGOTIATION) { + return {ErrorType::TransportVersionNegotiation, 0}; + } + return {ErrorType::Transport, + ngtcp2_err_infer_quic_transport_error_code(liberr)}; +} + +Error err_transport_idle_timeout() { + return {ErrorType::TransportIdleTimeout, 0}; +} + +Error err_transport_tls(int alert) { + return {ErrorType::Transport, ngtcp2_err_infer_quic_transport_error_code( + NGTCP2_CRYPTO_ERROR | alert)}; +} + +Error err_application(int liberr) { + return {ErrorType::Application, + nghttp3_err_infer_quic_app_error_code(liberr)}; +} + +} // namespace quic diff --git a/src/quic.h b/src/quic.h new file mode 100644 index 0000000..d72ae1f --- /dev/null +++ b/src/quic.h @@ -0,0 +1,56 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2019 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef QUIC_H +#define QUIC_H + +#include "nghttp2_config.h" + +#include "stdint.h" + +namespace quic { + +enum class ErrorType { + Transport, + TransportVersionNegotiation, + TransportIdleTimeout, + Application, +}; + +struct Error { + Error(ErrorType type, uint64_t code) : type(type), code(code) {} + Error() : type(ErrorType::Transport), code(0) {} + + ErrorType type; + uint64_t code; +}; + +Error err_transport(int liberr); +Error err_transport_idle_timeout(); +Error err_transport_tls(int alert); +Error err_application(int liberr); + +} // namespace quic + +#endif // QUIC_H diff --git a/src/shrpx-unittest.cc b/src/shrpx-unittest.cc new file mode 100644 index 0000000..f089adf --- /dev/null +++ b/src/shrpx-unittest.cc @@ -0,0 +1,246 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <stdio.h> +#include <string.h> +#include <CUnit/Basic.h> +// include test cases' include files here +#include "shrpx_tls_test.h" +#include "shrpx_downstream_test.h" +#include "shrpx_config_test.h" +#include "shrpx_worker_test.h" +#include "http2_test.h" +#include "util_test.h" +#include "nghttp2_gzip_test.h" +#include "buffer_test.h" +#include "memchunk_test.h" +#include "template_test.h" +#include "shrpx_http_test.h" +#include "base64_test.h" +#include "shrpx_config.h" +#include "tls.h" +#include "shrpx_router_test.h" +#include "shrpx_log.h" + +static int init_suite1(void) { return 0; } + +static int clean_suite1(void) { return 0; } + +int main(int argc, char *argv[]) { + CU_pSuite pSuite = nullptr; + unsigned int num_tests_failed; + + shrpx::create_config(); + + // initialize the CUnit test registry + if (CUE_SUCCESS != CU_initialize_registry()) + return CU_get_error(); + + // add a suite to the registry + pSuite = CU_add_suite("shrpx_TestSuite", init_suite1, clean_suite1); + if (nullptr == pSuite) { + CU_cleanup_registry(); + return CU_get_error(); + } + + // add the tests to the suite + if (!CU_add_test(pSuite, "tls_create_lookup_tree", + shrpx::test_shrpx_tls_create_lookup_tree) || + !CU_add_test(pSuite, "tls_cert_lookup_tree_add_ssl_ctx", + shrpx::test_shrpx_tls_cert_lookup_tree_add_ssl_ctx) || + !CU_add_test(pSuite, "tls_tls_hostname_match", + shrpx::test_shrpx_tls_tls_hostname_match) || + !CU_add_test(pSuite, "tls_tls_verify_numeric_hostname", + shrpx::test_shrpx_tls_verify_numeric_hostname) || + !CU_add_test(pSuite, "tls_tls_verify_dns_hostname", + shrpx::test_shrpx_tls_verify_dns_hostname) || + !CU_add_test(pSuite, "http2_add_header", shrpx::test_http2_add_header) || + !CU_add_test(pSuite, "http2_get_header", shrpx::test_http2_get_header) || + !CU_add_test(pSuite, "http2_copy_headers_to_nva", + shrpx::test_http2_copy_headers_to_nva) || + !CU_add_test(pSuite, "http2_build_http1_headers_from_headers", + shrpx::test_http2_build_http1_headers_from_headers) || + !CU_add_test(pSuite, "http2_lws", shrpx::test_http2_lws) || + !CU_add_test(pSuite, "http2_rewrite_location_uri", + shrpx::test_http2_rewrite_location_uri) || + !CU_add_test(pSuite, "http2_parse_http_status_code", + shrpx::test_http2_parse_http_status_code) || + !CU_add_test(pSuite, "http2_index_header", + shrpx::test_http2_index_header) || + !CU_add_test(pSuite, "http2_lookup_token", + shrpx::test_http2_lookup_token) || + !CU_add_test(pSuite, "http2_parse_link_header", + shrpx::test_http2_parse_link_header) || + !CU_add_test(pSuite, "http2_path_join", shrpx::test_http2_path_join) || + !CU_add_test(pSuite, "http2_normalize_path", + shrpx::test_http2_normalize_path) || + !CU_add_test(pSuite, "http2_rewrite_clean_path", + shrpx::test_http2_rewrite_clean_path) || + !CU_add_test(pSuite, "http2_get_pure_path_component", + shrpx::test_http2_get_pure_path_component) || + !CU_add_test(pSuite, "http2_construct_push_component", + shrpx::test_http2_construct_push_component) || + !CU_add_test(pSuite, "http2_contains_trailers", + shrpx::test_http2_contains_trailers) || + !CU_add_test(pSuite, "http2_check_transfer_encoding", + shrpx::test_http2_check_transfer_encoding) || + !CU_add_test(pSuite, "downstream_field_store_append_last_header", + shrpx::test_downstream_field_store_append_last_header) || + !CU_add_test(pSuite, "downstream_field_store_header", + shrpx::test_downstream_field_store_header) || + !CU_add_test(pSuite, "downstream_crumble_request_cookie", + shrpx::test_downstream_crumble_request_cookie) || + !CU_add_test(pSuite, "downstream_assemble_request_cookie", + shrpx::test_downstream_assemble_request_cookie) || + !CU_add_test(pSuite, "downstream_rewrite_location_response_header", + shrpx::test_downstream_rewrite_location_response_header) || + !CU_add_test(pSuite, "downstream_supports_non_final_response", + shrpx::test_downstream_supports_non_final_response) || + !CU_add_test(pSuite, "downstream_find_affinity_cookie", + shrpx::test_downstream_find_affinity_cookie) || + !CU_add_test(pSuite, "config_parse_header", + shrpx::test_shrpx_config_parse_header) || + !CU_add_test(pSuite, "config_parse_log_format", + shrpx::test_shrpx_config_parse_log_format) || + !CU_add_test(pSuite, "config_read_tls_ticket_key_file", + shrpx::test_shrpx_config_read_tls_ticket_key_file) || + !CU_add_test(pSuite, "config_read_tls_ticket_key_file_aes_256", + shrpx::test_shrpx_config_read_tls_ticket_key_file_aes_256) || + !CU_add_test(pSuite, "worker_match_downstream_addr_group", + shrpx::test_shrpx_worker_match_downstream_addr_group) || + !CU_add_test(pSuite, "http_create_forwarded", + shrpx::test_shrpx_http_create_forwarded) || + !CU_add_test(pSuite, "http_create_via_header_value", + shrpx::test_shrpx_http_create_via_header_value) || + !CU_add_test(pSuite, "http_create_affinity_cookie", + shrpx::test_shrpx_http_create_affinity_cookie) || + !CU_add_test(pSuite, "http_create_atlsvc_header_field_value", + shrpx::test_shrpx_http_create_altsvc_header_value) || + !CU_add_test(pSuite, "http_check_http_scheme", + shrpx::test_shrpx_http_check_http_scheme) || + !CU_add_test(pSuite, "router_match", shrpx::test_shrpx_router_match) || + !CU_add_test(pSuite, "router_match_wildcard", + shrpx::test_shrpx_router_match_wildcard) || + !CU_add_test(pSuite, "router_match_prefix", + shrpx::test_shrpx_router_match_prefix) || + !CU_add_test(pSuite, "util_streq", shrpx::test_util_streq) || + !CU_add_test(pSuite, "util_strieq", shrpx::test_util_strieq) || + !CU_add_test(pSuite, "util_inp_strlower", + shrpx::test_util_inp_strlower) || + !CU_add_test(pSuite, "util_to_base64", shrpx::test_util_to_base64) || + !CU_add_test(pSuite, "util_to_token68", shrpx::test_util_to_token68) || + !CU_add_test(pSuite, "util_percent_encode_token", + shrpx::test_util_percent_encode_token) || + !CU_add_test(pSuite, "util_percent_decode", + shrpx::test_util_percent_decode) || + !CU_add_test(pSuite, "util_quote_string", + shrpx::test_util_quote_string) || + !CU_add_test(pSuite, "util_utox", shrpx::test_util_utox) || + !CU_add_test(pSuite, "util_http_date", shrpx::test_util_http_date) || + !CU_add_test(pSuite, "util_select_h2", shrpx::test_util_select_h2) || + !CU_add_test(pSuite, "util_ipv6_numeric_addr", + shrpx::test_util_ipv6_numeric_addr) || + !CU_add_test(pSuite, "util_utos", shrpx::test_util_utos) || + !CU_add_test(pSuite, "util_make_string_ref_uint", + shrpx::test_util_make_string_ref_uint) || + !CU_add_test(pSuite, "util_utos_unit", shrpx::test_util_utos_unit) || + !CU_add_test(pSuite, "util_utos_funit", shrpx::test_util_utos_funit) || + !CU_add_test(pSuite, "util_parse_uint_with_unit", + shrpx::test_util_parse_uint_with_unit) || + !CU_add_test(pSuite, "util_parse_uint", shrpx::test_util_parse_uint) || + !CU_add_test(pSuite, "util_parse_duration_with_unit", + shrpx::test_util_parse_duration_with_unit) || + !CU_add_test(pSuite, "util_duration_str", + shrpx::test_util_duration_str) || + !CU_add_test(pSuite, "util_format_duration", + shrpx::test_util_format_duration) || + !CU_add_test(pSuite, "util_starts_with", shrpx::test_util_starts_with) || + !CU_add_test(pSuite, "util_ends_with", shrpx::test_util_ends_with) || + !CU_add_test(pSuite, "util_parse_http_date", + shrpx::test_util_parse_http_date) || + !CU_add_test(pSuite, "util_localtime_date", + shrpx::test_util_localtime_date) || + !CU_add_test(pSuite, "util_get_uint64", shrpx::test_util_get_uint64) || + !CU_add_test(pSuite, "util_parse_config_str_list", + shrpx::test_util_parse_config_str_list) || + !CU_add_test(pSuite, "util_make_http_hostport", + shrpx::test_util_make_http_hostport) || + !CU_add_test(pSuite, "util_make_hostport", + shrpx::test_util_make_hostport) || + !CU_add_test(pSuite, "util_strifind", shrpx::test_util_strifind) || + !CU_add_test(pSuite, "util_random_alpha_digit", + shrpx::test_util_random_alpha_digit) || + !CU_add_test(pSuite, "util_format_hex", shrpx::test_util_format_hex) || + !CU_add_test(pSuite, "util_is_hex_string", + shrpx::test_util_is_hex_string) || + !CU_add_test(pSuite, "util_decode_hex", shrpx::test_util_decode_hex) || + !CU_add_test(pSuite, "util_extract_host", + shrpx::test_util_extract_host) || + !CU_add_test(pSuite, "util_split_hostport", + shrpx::test_util_split_hostport) || + !CU_add_test(pSuite, "util_split_str", shrpx::test_util_split_str) || + !CU_add_test(pSuite, "util_rstrip", shrpx::test_util_rstrip) || + !CU_add_test(pSuite, "gzip_inflate", test_nghttp2_gzip_inflate) || + !CU_add_test(pSuite, "buffer_write", nghttp2::test_buffer_write) || + !CU_add_test(pSuite, "pool_recycle", nghttp2::test_pool_recycle) || + !CU_add_test(pSuite, "memchunk_append", nghttp2::test_memchunks_append) || + !CU_add_test(pSuite, "memchunk_drain", nghttp2::test_memchunks_drain) || + !CU_add_test(pSuite, "memchunk_riovec", nghttp2::test_memchunks_riovec) || + !CU_add_test(pSuite, "memchunk_recycle", + nghttp2::test_memchunks_recycle) || + !CU_add_test(pSuite, "memchunk_reset", nghttp2::test_memchunks_reset) || + !CU_add_test(pSuite, "peek_memchunk_append", + nghttp2::test_peek_memchunks_append) || + !CU_add_test(pSuite, "peek_memchunk_disable_peek_drain", + nghttp2::test_peek_memchunks_disable_peek_drain) || + !CU_add_test(pSuite, "peek_memchunk_disable_peek_no_drain", + nghttp2::test_peek_memchunks_disable_peek_no_drain) || + !CU_add_test(pSuite, "peek_memchunk_reset", + nghttp2::test_peek_memchunks_reset) || + !CU_add_test(pSuite, "template_immutable_string", + nghttp2::test_template_immutable_string) || + !CU_add_test(pSuite, "template_string_ref", + nghttp2::test_template_string_ref) || + !CU_add_test(pSuite, "base64_encode", nghttp2::test_base64_encode) || + !CU_add_test(pSuite, "base64_decode", nghttp2::test_base64_decode)) { + CU_cleanup_registry(); + return CU_get_error(); + } + + // Run all tests using the CUnit Basic interface + CU_basic_set_mode(CU_BRM_VERBOSE); + CU_basic_run_tests(); + num_tests_failed = CU_get_number_of_tests_failed(); + CU_cleanup_registry(); + if (CU_get_error() == CUE_SUCCESS) { + return num_tests_failed; + } else { + printf("CUnit Error: %s\n", CU_get_error_msg()); + return CU_get_error(); + } +} diff --git a/src/shrpx.cc b/src/shrpx.cc new file mode 100644 index 0000000..b42054c --- /dev/null +++ b/src/shrpx.cc @@ -0,0 +1,5329 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx.h" + +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/stat.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#include <sys/un.h> +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H +#include <signal.h> +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#include <netinet/tcp.h> +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif // HAVE_ARPA_INET_H +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <getopt.h> +#ifdef HAVE_SYSLOG_H +# include <syslog.h> +#endif // HAVE_SYSLOG_H +#ifdef HAVE_LIMITS_H +# include <limits.h> +#endif // HAVE_LIMITS_H +#ifdef HAVE_SYS_TIME_H +# include <sys/time.h> +#endif // HAVE_SYS_TIME_H +#include <sys/resource.h> +#ifdef HAVE_LIBSYSTEMD +# include <systemd/sd-daemon.h> +#endif // HAVE_LIBSYSTEMD +#ifdef HAVE_LIBBPF +# include <bpf/libbpf.h> +#endif // HAVE_LIBBPF + +#include <cinttypes> +#include <limits> +#include <cstdlib> +#include <iostream> +#include <fstream> +#include <vector> +#include <initializer_list> +#include <random> + +#include <openssl/ssl.h> +#include <openssl/err.h> +#include <openssl/rand.h> +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#ifdef ENABLE_HTTP3 +# include <ngtcp2/ngtcp2.h> +# include <nghttp3/nghttp3.h> +#endif // ENABLE_HTTP3 + +#include "shrpx_config.h" +#include "shrpx_tls.h" +#include "shrpx_log_config.h" +#include "shrpx_worker.h" +#include "shrpx_http2_upstream.h" +#include "shrpx_http2_session.h" +#include "shrpx_worker_process.h" +#include "shrpx_process.h" +#include "shrpx_signal.h" +#include "shrpx_connection.h" +#include "shrpx_log.h" +#include "shrpx_http.h" +#include "util.h" +#include "app_helper.h" +#include "tls.h" +#include "template.h" +#include "allocator.h" +#include "ssl_compat.h" +#include "xsi_strerror.h" + +extern char **environ; + +using namespace nghttp2; + +namespace shrpx { + +// Deprecated: Environment variables to tell new binary the listening +// socket's file descriptors. They are not close-on-exec. +constexpr auto ENV_LISTENER4_FD = StringRef::from_lit("NGHTTPX_LISTENER4_FD"); +constexpr auto ENV_LISTENER6_FD = StringRef::from_lit("NGHTTPX_LISTENER6_FD"); + +// Deprecated: Environment variable to tell new binary the port number +// the current binary is listening to. +constexpr auto ENV_PORT = StringRef::from_lit("NGHTTPX_PORT"); + +// Deprecated: Environment variable to tell new binary the listening +// socket's file descriptor if frontend listens UNIX domain socket. +constexpr auto ENV_UNIX_FD = StringRef::from_lit("NGHTTP2_UNIX_FD"); +// Deprecated: Environment variable to tell new binary the UNIX domain +// socket path. +constexpr auto ENV_UNIX_PATH = StringRef::from_lit("NGHTTP2_UNIX_PATH"); + +// Prefix of environment variables to tell new binary the listening +// socket's file descriptor. They are not close-on-exec. For TCP +// socket, the value must be comma separated 2 parameters: tcp,<FD>. +// <FD> is file descriptor. For UNIX domain socket, the value must be +// comma separated 3 parameters: unix,<FD>,<PATH>. <FD> is file +// descriptor. <PATH> is a path to UNIX domain socket. +constexpr auto ENV_ACCEPT_PREFIX = StringRef::from_lit("NGHTTPX_ACCEPT_"); + +// This environment variable contains PID of the original main +// process, assuming that it created this main process as a result of +// SIGUSR2. The new main process is expected to send QUIT signal to +// the original main process to shut it down gracefully. +constexpr auto ENV_ORIG_PID = StringRef::from_lit("NGHTTPX_ORIG_PID"); + +// Prefix of environment variables to tell new binary the QUIC IPC +// file descriptor and CID prefix of the lingering worker process. +// The value must be comma separated parameters: +// <FD>,<CID_PREFIX_0>,<CID_PREFIX_1>,... <FD> is the file +// descriptor. <CID_PREFIX_I> is the I-th CID prefix in hex encoded +// string. +constexpr auto ENV_QUIC_WORKER_PROCESS_PREFIX = + StringRef::from_lit("NGHTTPX_QUIC_WORKER_PROCESS_"); + +#ifndef _KERNEL_FASTOPEN +# define _KERNEL_FASTOPEN +// conditional define for TCP_FASTOPEN mostly on ubuntu +# ifndef TCP_FASTOPEN +# define TCP_FASTOPEN 23 +# endif + +// conditional define for SOL_TCP mostly on ubuntu +# ifndef SOL_TCP +# define SOL_TCP 6 +# endif +#endif + +// This configuration is fixed at the first startup of the main +// process, and does not change after subsequent reloadings. +struct StartupConfig { + // This contains all options given in command-line. + std::vector<std::pair<StringRef, StringRef>> cmdcfgs; + // The current working directory where this process started. + char *cwd; + // The pointer to original argv (not sure why we have this?) + char **original_argv; + // The pointer to argv, this is a deep copy of original argv. + char **argv; + // The number of elements in argv. + int argc; +}; + +namespace { +StartupConfig suconfig; +} // namespace + +struct InheritedAddr { + // IP address if TCP socket. Otherwise, UNIX domain socket path. + StringRef host; + uint16_t port; + // true if UNIX domain socket path + bool host_unix; + int fd; + bool used; +}; + +namespace { +void signal_cb(struct ev_loop *loop, ev_signal *w, int revents); +} // namespace + +namespace { +void worker_process_child_cb(struct ev_loop *loop, ev_child *w, int revents); +} // namespace + +struct WorkerProcess { + WorkerProcess(struct ev_loop *loop, pid_t worker_pid, int ipc_fd +#ifdef ENABLE_HTTP3 + , + int quic_ipc_fd, + const std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> + &cid_prefixes +#endif // ENABLE_HTTP3 + ) + : loop(loop), + worker_pid(worker_pid), + ipc_fd(ipc_fd) +#ifdef ENABLE_HTTP3 + , + quic_ipc_fd(quic_ipc_fd), + cid_prefixes(cid_prefixes) +#endif // ENABLE_HTTP3 + { + ev_child_init(&worker_process_childev, worker_process_child_cb, worker_pid, + 0); + worker_process_childev.data = this; + ev_child_start(loop, &worker_process_childev); + } + + ~WorkerProcess() { + ev_child_stop(loop, &worker_process_childev); + +#ifdef ENABLE_HTTP3 + if (quic_ipc_fd != -1) { + close(quic_ipc_fd); + } +#endif // ENABLE_HTTP3 + + if (ipc_fd != -1) { + shutdown(ipc_fd, SHUT_WR); + close(ipc_fd); + } + } + + ev_child worker_process_childev; + struct ev_loop *loop; + pid_t worker_pid; + int ipc_fd; + std::chrono::steady_clock::time_point termination_deadline; +#ifdef ENABLE_HTTP3 + int quic_ipc_fd; + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes; +#endif // ENABLE_HTTP3 +}; + +namespace { +void reload_config(); +} // namespace + +namespace { +std::deque<std::unique_ptr<WorkerProcess>> worker_processes; +} // namespace + +namespace { +ev_timer worker_process_grace_period_timer; +} // namespace + +namespace { +void worker_process_grace_period_timercb(struct ev_loop *loop, ev_timer *w, + int revents) { + auto now = std::chrono::steady_clock::now(); + auto next_repeat = std::chrono::steady_clock::duration::zero(); + + for (auto it = std::begin(worker_processes); + it != std::end(worker_processes);) { + auto &wp = *it; + if (wp->termination_deadline.time_since_epoch().count() == 0) { + ++it; + + continue; + } + + auto d = wp->termination_deadline - now; + if (d.count() > 0) { + if (next_repeat == std::chrono::steady_clock::duration::zero() || + d < next_repeat) { + next_repeat = d; + } + + ++it; + + continue; + } + + LOG(NOTICE) << "Deleting worker process pid=" << wp->worker_pid + << " because its grace shutdown period is over"; + + it = worker_processes.erase(it); + } + + if (next_repeat.count() > 0) { + w->repeat = util::ev_tstamp_from(next_repeat); + ev_timer_again(loop, w); + + return; + } + + ev_timer_stop(loop, w); +} +} // namespace + +namespace { +void worker_process_set_termination_deadline(WorkerProcess *wp, + struct ev_loop *loop) { + auto config = get_config(); + + if (!(config->worker_process_grace_shutdown_period > 0.)) { + return; + } + + wp->termination_deadline = + std::chrono::steady_clock::now() + + util::duration_from(config->worker_process_grace_shutdown_period); + + if (!ev_is_active(&worker_process_grace_period_timer)) { + worker_process_grace_period_timer.repeat = + config->worker_process_grace_shutdown_period; + + ev_timer_again(loop, &worker_process_grace_period_timer); + } +} +} // namespace + +namespace { +void worker_process_add(std::unique_ptr<WorkerProcess> wp) { + worker_processes.push_back(std::move(wp)); +} +} // namespace + +namespace { +void worker_process_remove(const WorkerProcess *wp, struct ev_loop *loop) { + for (auto it = std::begin(worker_processes); it != std::end(worker_processes); + ++it) { + auto &s = *it; + + if (s.get() != wp) { + continue; + } + + worker_processes.erase(it); + + if (worker_processes.empty()) { + ev_timer_stop(loop, &worker_process_grace_period_timer); + } + + break; + } +} +} // namespace + +namespace { +void worker_process_adjust_limit() { + auto config = get_config(); + + if (config->max_worker_processes && + worker_processes.size() > config->max_worker_processes) { + worker_processes.pop_front(); + } +} +} // namespace + +namespace { +void worker_process_remove_all(struct ev_loop *loop) { + std::deque<std::unique_ptr<WorkerProcess>>().swap(worker_processes); + + ev_timer_stop(loop, &worker_process_grace_period_timer); +} +} // namespace + +namespace { +// Send signal |signum| to all worker processes, and clears +// worker_processes. +void worker_process_kill(int signum, struct ev_loop *loop) { + for (auto &s : worker_processes) { + if (s->worker_pid == -1) { + continue; + } + kill(s->worker_pid, signum); + } + worker_process_remove_all(loop); +} +} // namespace + +namespace { +int save_pid() { + std::array<char, STRERROR_BUFSIZE> errbuf; + auto config = get_config(); + + constexpr auto SUFFIX = StringRef::from_lit(".XXXXXX"); + auto &pid_file = config->pid_file; + + auto len = config->pid_file.size() + SUFFIX.size(); + auto buf = std::make_unique<char[]>(len + 1); + auto p = buf.get(); + + p = std::copy(std::begin(pid_file), std::end(pid_file), p); + p = std::copy(std::begin(SUFFIX), std::end(SUFFIX), p); + *p = '\0'; + + auto temp_path = buf.get(); + + auto fd = mkstemp(temp_path); + if (fd == -1) { + auto error = errno; + LOG(ERROR) << "Could not save PID to file " << pid_file << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + auto content = util::utos(config->pid) + '\n'; + + if (write(fd, content.c_str(), content.size()) == -1) { + auto error = errno; + LOG(ERROR) << "Could not save PID to file " << pid_file << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + if (fsync(fd) == -1) { + auto error = errno; + LOG(ERROR) << "Could not save PID to file " << pid_file << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + close(fd); + + if (rename(temp_path, pid_file.c_str()) == -1) { + auto error = errno; + LOG(ERROR) << "Could not save PID to file " << pid_file << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + unlink(temp_path); + + return -1; + } + + if (config->uid != 0) { + if (chown(pid_file.c_str(), config->uid, config->gid) == -1) { + auto error = errno; + LOG(WARN) << "Changing owner of pid file " << pid_file << " failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } + + return 0; +} +} // namespace + +namespace { +void shrpx_sd_notifyf(int unset_environment, const char *format, ...) { +#ifdef HAVE_LIBSYSTEMD + va_list args; + + va_start(args, format); + sd_notifyf(unset_environment, format, va_arg(args, char *)); + va_end(args); +#endif // HAVE_LIBSYSTEMD +} +} // namespace + +namespace { +void exec_binary() { + int rv; + sigset_t oldset; + std::array<char, STRERROR_BUFSIZE> errbuf; + + LOG(NOTICE) << "Executing new binary"; + + shrpx_sd_notifyf(0, "RELOADING=1"); + + rv = shrpx_signal_block_all(&oldset); + if (rv != 0) { + auto error = errno; + LOG(ERROR) << "Blocking all signals failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + return; + } + + auto pid = fork(); + + if (pid != 0) { + if (pid == -1) { + auto error = errno; + LOG(ERROR) << "fork() failed errno=" << error; + } else { + // update PID tracking information in systemd + shrpx_sd_notifyf(0, "MAINPID=%d\n", pid); + } + + rv = shrpx_signal_set(&oldset); + + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Restoring signal mask failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + exit(EXIT_FAILURE); + } + + return; + } + + // child process + + shrpx_signal_unset_main_proc_ign_handler(); + + rv = shrpx_signal_unblock_all(); + if (rv != 0) { + auto error = errno; + LOG(ERROR) << "Unblocking all signals failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + nghttp2_Exit(EXIT_FAILURE); + } + + auto exec_path = + util::get_exec_path(suconfig.argc, suconfig.argv, suconfig.cwd); + + if (!exec_path) { + LOG(ERROR) << "Could not resolve the executable path"; + nghttp2_Exit(EXIT_FAILURE); + } + + auto argv = std::make_unique<char *[]>(suconfig.argc + 1); + + argv[0] = exec_path; + for (int i = 1; i < suconfig.argc; ++i) { + argv[i] = suconfig.argv[i]; + } + argv[suconfig.argc] = nullptr; + + size_t envlen = 0; + for (char **p = environ; *p; ++p, ++envlen) + ; + + auto config = get_config(); + auto &listenerconf = config->conn.listener; + + // 2 for ENV_ORIG_PID and terminal nullptr. + auto envp = std::make_unique<char *[]>(envlen + listenerconf.addrs.size() + + worker_processes.size() + 2); + size_t envidx = 0; + + std::vector<ImmutableString> fd_envs; + for (size_t i = 0; i < listenerconf.addrs.size(); ++i) { + auto &addr = listenerconf.addrs[i]; + auto s = ENV_ACCEPT_PREFIX.str(); + s += util::utos(i + 1); + s += '='; + if (addr.host_unix) { + s += "unix,"; + s += util::utos(addr.fd); + s += ','; + s += addr.host; + } else { + s += "tcp,"; + s += util::utos(addr.fd); + } + + fd_envs.emplace_back(s); + envp[envidx++] = const_cast<char *>(fd_envs.back().c_str()); + } + + auto ipc_fd_str = ENV_ORIG_PID.str(); + ipc_fd_str += '='; + ipc_fd_str += util::utos(config->pid); + envp[envidx++] = const_cast<char *>(ipc_fd_str.c_str()); + +#ifdef ENABLE_HTTP3 + std::vector<ImmutableString> quic_lwps; + for (size_t i = 0; i < worker_processes.size(); ++i) { + auto &wp = worker_processes[i]; + auto s = ENV_QUIC_WORKER_PROCESS_PREFIX.str(); + s += util::utos(i + 1); + s += '='; + s += util::utos(wp->quic_ipc_fd); + for (auto &cid_prefix : wp->cid_prefixes) { + s += ','; + s += util::format_hex(cid_prefix); + } + + quic_lwps.emplace_back(s); + envp[envidx++] = const_cast<char *>(quic_lwps.back().c_str()); + } +#endif // ENABLE_HTTP3 + + for (size_t i = 0; i < envlen; ++i) { + auto env = StringRef{environ[i]}; + if (util::starts_with(env, ENV_ACCEPT_PREFIX) || + util::starts_with(env, ENV_LISTENER4_FD) || + util::starts_with(env, ENV_LISTENER6_FD) || + util::starts_with(env, ENV_PORT) || + util::starts_with(env, ENV_UNIX_FD) || + util::starts_with(env, ENV_UNIX_PATH) || + util::starts_with(env, ENV_ORIG_PID) || + util::starts_with(env, ENV_QUIC_WORKER_PROCESS_PREFIX)) { + continue; + } + + envp[envidx++] = environ[i]; + } + + envp[envidx++] = nullptr; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "cmdline"; + for (int i = 0; argv[i]; ++i) { + LOG(INFO) << i << ": " << argv[i]; + } + LOG(INFO) << "environ"; + for (int i = 0; envp[i]; ++i) { + LOG(INFO) << i << ": " << envp[i]; + } + } + + // restores original stderr + restore_original_fds(); + + // reloading finished + shrpx_sd_notifyf(0, "READY=1"); + + if (execve(argv[0], argv.get(), envp.get()) == -1) { + auto error = errno; + LOG(ERROR) << "execve failed: errno=" << error; + nghttp2_Exit(EXIT_FAILURE); + } +} +} // namespace + +namespace { +void ipc_send(WorkerProcess *wp, uint8_t ipc_event) { + std::array<char, STRERROR_BUFSIZE> errbuf; + ssize_t nwrite; + while ((nwrite = write(wp->ipc_fd, &ipc_event, 1)) == -1 && errno == EINTR) + ; + + if (nwrite < 0) { + auto error = errno; + LOG(ERROR) << "Could not send IPC event to worker process: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return; + } + + if (nwrite == 0) { + LOG(ERROR) << "Could not send IPC event due to pipe overflow"; + return; + } +} +} // namespace + +namespace { +void reopen_log(WorkerProcess *wp) { + LOG(NOTICE) << "Reopening log files: main process"; + + auto config = get_config(); + auto &loggingconf = config->logging; + + (void)reopen_log_files(loggingconf); + redirect_stderr_to_errorlog(loggingconf); + ipc_send(wp, SHRPX_IPC_REOPEN_LOG); +} +} // namespace + +namespace { +void signal_cb(struct ev_loop *loop, ev_signal *w, int revents) { + switch (w->signum) { + case REOPEN_LOG_SIGNAL: + for (auto &wp : worker_processes) { + reopen_log(wp.get()); + } + + return; + case EXEC_BINARY_SIGNAL: + exec_binary(); + return; + case GRACEFUL_SHUTDOWN_SIGNAL: { + auto &listenerconf = get_config()->conn.listener; + for (auto &addr : listenerconf.addrs) { + close(addr.fd); + } + + for (auto &wp : worker_processes) { + ipc_send(wp.get(), SHRPX_IPC_GRACEFUL_SHUTDOWN); + worker_process_set_termination_deadline(wp.get(), loop); + } + + return; + } + case RELOAD_SIGNAL: + reload_config(); + + return; + default: + worker_process_kill(w->signum, loop); + ev_break(loop); + return; + } +} +} // namespace + +namespace { +void worker_process_child_cb(struct ev_loop *loop, ev_child *w, int revents) { + auto wp = static_cast<WorkerProcess *>(w->data); + + log_chld(w->rpid, w->rstatus, "Worker process"); + + worker_process_remove(wp, loop); + + if (worker_processes.empty()) { + ev_break(loop); + } +} +} // namespace + +namespace { +int create_unix_domain_server_socket(UpstreamAddr &faddr, + std::vector<InheritedAddr> &iaddrs) { + std::array<char, STRERROR_BUFSIZE> errbuf; + auto found = std::find_if( + std::begin(iaddrs), std::end(iaddrs), [&faddr](const InheritedAddr &ia) { + return !ia.used && ia.host_unix && ia.host == faddr.host; + }); + + if (found != std::end(iaddrs)) { + LOG(NOTICE) << "Listening on UNIX domain socket " << faddr.host + << (faddr.tls ? ", tls" : ""); + (*found).used = true; + faddr.fd = (*found).fd; + faddr.hostport = StringRef::from_lit("localhost"); + + return 0; + } + +#ifdef SOCK_NONBLOCK + auto fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (fd == -1) { + auto error = errno; + LOG(FATAL) << "socket() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } +#else // !SOCK_NONBLOCK + auto fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + auto error = errno; + LOG(FATAL) << "socket() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + util::make_socket_nonblocking(fd); +#endif // !SOCK_NONBLOCK + int val = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(FATAL) << "Failed to set SO_REUSEADDR option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + sockaddr_union addr; + addr.un.sun_family = AF_UNIX; + if (faddr.host.size() + 1 > sizeof(addr.un.sun_path)) { + LOG(FATAL) << "UNIX domain socket path " << faddr.host << " is too long > " + << sizeof(addr.un.sun_path); + close(fd); + return -1; + } + // copy path including terminal NULL + std::copy_n(faddr.host.c_str(), faddr.host.size() + 1, addr.un.sun_path); + + // unlink (remove) already existing UNIX domain socket path + unlink(faddr.host.c_str()); + + if (bind(fd, &addr.sa, sizeof(addr.un)) != 0) { + auto error = errno; + LOG(FATAL) << "Failed to bind UNIX domain socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + auto &listenerconf = get_config()->conn.listener; + + if (listen(fd, listenerconf.backlog) != 0) { + auto error = errno; + LOG(FATAL) << "Failed to listen to UNIX domain socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + LOG(NOTICE) << "Listening on UNIX domain socket " << faddr.host + << (faddr.tls ? ", tls" : ""); + + faddr.fd = fd; + faddr.hostport = StringRef::from_lit("localhost"); + + return 0; +} +} // namespace + +namespace { +int create_tcp_server_socket(UpstreamAddr &faddr, + std::vector<InheritedAddr> &iaddrs) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int fd = -1; + int rv; + + auto &listenerconf = get_config()->conn.listener; + + auto service = util::utos(faddr.port); + addrinfo hints{}; + hints.ai_family = faddr.family; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; +#ifdef AI_ADDRCONFIG + hints.ai_flags |= AI_ADDRCONFIG; +#endif // AI_ADDRCONFIG + + auto node = + faddr.host == StringRef::from_lit("*") ? nullptr : faddr.host.c_str(); + + addrinfo *res, *rp; + rv = getaddrinfo(node, service.c_str(), &hints, &res); +#ifdef AI_ADDRCONFIG + if (rv != 0) { + // Retry without AI_ADDRCONFIG + hints.ai_flags &= ~AI_ADDRCONFIG; + rv = getaddrinfo(node, service.c_str(), &hints, &res); + } +#endif // AI_ADDRCONFIG + if (rv != 0) { + LOG(FATAL) << "Unable to get IPv" << (faddr.family == AF_INET ? "4" : "6") + << " address for " << faddr.host << ", port " << faddr.port + << ": " << gai_strerror(rv); + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + std::array<char, NI_MAXHOST> host; + + for (rp = res; rp; rp = rp->ai_next) { + + rv = getnameinfo(rp->ai_addr, rp->ai_addrlen, host.data(), host.size(), + nullptr, 0, NI_NUMERICHOST); + + if (rv != 0) { + LOG(WARN) << "getnameinfo() failed: " << gai_strerror(rv); + continue; + } + + auto found = std::find_if(std::begin(iaddrs), std::end(iaddrs), + [&host, &faddr](const InheritedAddr &ia) { + return !ia.used && !ia.host_unix && + ia.host == host.data() && + ia.port == faddr.port; + }); + + if (found != std::end(iaddrs)) { + (*found).used = true; + fd = (*found).fd; + break; + } + +#ifdef SOCK_NONBLOCK + fd = + socket(rp->ai_family, rp->ai_socktype | SOCK_NONBLOCK, rp->ai_protocol); + if (fd == -1) { + auto error = errno; + LOG(WARN) << "socket() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + continue; + } +#else // !SOCK_NONBLOCK + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + auto error = errno; + LOG(WARN) << "socket() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + continue; + } + util::make_socket_nonblocking(fd); +#endif // !SOCK_NONBLOCK + int val = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set SO_REUSEADDR option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + +#ifdef IPV6_V6ONLY + if (faddr.family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set IPV6_V6ONLY option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + } +#endif // IPV6_V6ONLY + +#ifdef TCP_DEFER_ACCEPT + val = 3; + if (setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set TCP_DEFER_ACCEPT option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } +#endif // TCP_DEFER_ACCEPT + + // When we are executing new binary, and the old binary did not + // bind privileged port (< 1024) for some reason, binding to those + // ports will fail with permission denied error. + if (bind(fd, rp->ai_addr, rp->ai_addrlen) == -1) { + auto error = errno; + LOG(WARN) << "bind() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + + if (listenerconf.fastopen > 0) { + val = listenerconf.fastopen; + if (setsockopt(fd, SOL_TCP, TCP_FASTOPEN, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set TCP_FASTOPEN option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } + + if (listen(fd, listenerconf.backlog) == -1) { + auto error = errno; + LOG(WARN) << "listen() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + + break; + } + + if (!rp) { + LOG(FATAL) << "Listening " << (faddr.family == AF_INET ? "IPv4" : "IPv6") + << " socket failed"; + + return -1; + } + + faddr.fd = fd; + faddr.hostport = util::make_http_hostport(mod_config()->balloc, + StringRef{host.data()}, faddr.port); + + LOG(NOTICE) << "Listening on " << faddr.hostport + << (faddr.tls ? ", tls" : ""); + + return 0; +} +} // namespace + +namespace { +// Returns array of InheritedAddr constructed from |config|. This +// function is intended to be used when reloading configuration, and +// |config| is usually a current configuration. +std::vector<InheritedAddr> +get_inherited_addr_from_config(BlockAllocator &balloc, Config *config) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int rv; + + auto &listenerconf = config->conn.listener; + + std::vector<InheritedAddr> iaddrs(listenerconf.addrs.size()); + + size_t idx = 0; + for (auto &addr : listenerconf.addrs) { + auto &iaddr = iaddrs[idx++]; + + if (addr.host_unix) { + iaddr.host = addr.host; + iaddr.host_unix = true; + iaddr.fd = addr.fd; + + continue; + } + + iaddr.port = addr.port; + iaddr.fd = addr.fd; + + // We have to getsockname/getnameinfo for fd, since we may have + // '*' appear in addr.host, which makes comparison against "real" + // address fail. + + sockaddr_union su; + socklen_t salen = sizeof(su); + + // We already added entry to iaddrs. Even if we got errors, we + // don't remove it. This is required because we have to close the + // socket if it is not reused. The empty host name usually does + // not match anything. + + if (getsockname(addr.fd, &su.sa, &salen) != 0) { + auto error = errno; + LOG(WARN) << "getsockname() syscall failed (fd=" << addr.fd + << "): " << xsi_strerror(error, errbuf.data(), errbuf.size()); + continue; + } + + std::array<char, NI_MAXHOST> host; + rv = getnameinfo(&su.sa, salen, host.data(), host.size(), nullptr, 0, + NI_NUMERICHOST); + if (rv != 0) { + LOG(WARN) << "getnameinfo() failed (fd=" << addr.fd + << "): " << gai_strerror(rv); + continue; + } + + iaddr.host = make_string_ref(balloc, StringRef{host.data()}); + } + + return iaddrs; +} +} // namespace + +namespace { +// Returns array of InheritedAddr constructed from environment +// variables. This function handles the old environment variable +// names used in 1.7.0 or earlier. +std::vector<InheritedAddr> get_inherited_addr_from_env(Config *config) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int rv; + std::vector<InheritedAddr> iaddrs; + + { + // Upgrade from 1.7.0 or earlier + auto portenv = getenv(ENV_PORT.c_str()); + if (portenv) { + size_t i = 1; + for (const auto &env_name : {ENV_LISTENER4_FD, ENV_LISTENER6_FD}) { + auto fdenv = getenv(env_name.c_str()); + if (fdenv) { + auto name = ENV_ACCEPT_PREFIX.str(); + name += util::utos(i); + std::string value = "tcp,"; + value += fdenv; + setenv(name.c_str(), value.c_str(), 0); + ++i; + } + } + } else { + // The return value of getenv may be allocated statically. + if (getenv(ENV_UNIX_PATH.c_str()) && getenv(ENV_UNIX_FD.c_str())) { + auto name = ENV_ACCEPT_PREFIX.str(); + name += '1'; + std::string value = "unix,"; + value += getenv(ENV_UNIX_FD.c_str()); + value += ','; + value += getenv(ENV_UNIX_PATH.c_str()); + setenv(name.c_str(), value.c_str(), 0); + } + } + } + + for (size_t i = 1;; ++i) { + auto name = ENV_ACCEPT_PREFIX.str(); + name += util::utos(i); + auto env = getenv(name.c_str()); + if (!env) { + break; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Read env " << name << "=" << env; + } + + auto end_type = strchr(env, ','); + if (!end_type) { + continue; + } + + auto type = StringRef(env, end_type); + auto value = end_type + 1; + + if (type == StringRef::from_lit("unix")) { + auto endfd = strchr(value, ','); + if (!endfd) { + continue; + } + auto fd = util::parse_uint(reinterpret_cast<const uint8_t *>(value), + endfd - value); + if (fd == -1) { + LOG(WARN) << "Could not parse file descriptor from " + << std::string(value, endfd - value); + continue; + } + + auto path = endfd + 1; + if (strlen(path) == 0) { + LOG(WARN) << "Empty UNIX domain socket path (fd=" << fd << ")"; + close(fd); + continue; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Inherit UNIX domain socket fd=" << fd + << ", path=" << path; + } + + InheritedAddr addr{}; + addr.host = make_string_ref(config->balloc, StringRef{path}); + addr.host_unix = true; + addr.fd = static_cast<int>(fd); + iaddrs.push_back(std::move(addr)); + } + + if (type == StringRef::from_lit("tcp")) { + auto fd = util::parse_uint(value); + if (fd == -1) { + LOG(WARN) << "Could not parse file descriptor from " << value; + continue; + } + + sockaddr_union su; + socklen_t salen = sizeof(su); + + if (getsockname(fd, &su.sa, &salen) != 0) { + auto error = errno; + LOG(WARN) << "getsockname() syscall failed (fd=" << fd + << "): " << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + + uint16_t port; + + switch (su.storage.ss_family) { + case AF_INET: + port = ntohs(su.in.sin_port); + break; + case AF_INET6: + port = ntohs(su.in6.sin6_port); + break; + default: + close(fd); + continue; + } + + std::array<char, NI_MAXHOST> host; + rv = getnameinfo(&su.sa, salen, host.data(), host.size(), nullptr, 0, + NI_NUMERICHOST); + if (rv != 0) { + LOG(WARN) << "getnameinfo() failed (fd=" << fd + << "): " << gai_strerror(rv); + close(fd); + continue; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Inherit TCP socket fd=" << fd + << ", address=" << host.data() << ", port=" << port; + } + + InheritedAddr addr{}; + addr.host = make_string_ref(config->balloc, StringRef{host.data()}); + addr.port = static_cast<uint16_t>(port); + addr.fd = static_cast<int>(fd); + iaddrs.push_back(std::move(addr)); + continue; + } + } + + return iaddrs; +} +} // namespace + +namespace { +// Closes all sockets which are not reused. +void close_unused_inherited_addr(const std::vector<InheritedAddr> &iaddrs) { + for (auto &ia : iaddrs) { + if (ia.used) { + continue; + } + + close(ia.fd); + } +} +} // namespace + +namespace { +// Returns the PID of the original main process from environment +// variable ENV_ORIG_PID. +pid_t get_orig_pid_from_env() { + auto s = getenv(ENV_ORIG_PID.c_str()); + if (s == nullptr) { + return -1; + } + return util::parse_uint(s); +} +} // namespace + +#ifdef ENABLE_HTTP3 +namespace { +std::vector<QUICLingeringWorkerProcess> + inherited_quic_lingering_worker_processes; +} // namespace + +namespace { +std::vector<QUICLingeringWorkerProcess> +get_inherited_quic_lingering_worker_process_from_env() { + std::vector<QUICLingeringWorkerProcess> iwps; + + for (size_t i = 1;; ++i) { + auto name = ENV_QUIC_WORKER_PROCESS_PREFIX.str(); + name += util::utos(i); + auto env = getenv(name.c_str()); + if (!env) { + break; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Read env " << name << "=" << env; + } + + auto envend = env + strlen(env); + + auto end_fd = std::find(env, envend, ','); + if (end_fd == envend) { + continue; + } + + auto fd = + util::parse_uint(reinterpret_cast<const uint8_t *>(env), end_fd - env); + if (fd == -1) { + LOG(WARN) << "Could not parse file descriptor from " + << StringRef{env, static_cast<size_t>(end_fd - env)}; + continue; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Inherit worker process QUIC IPC socket fd=" << fd; + } + + util::make_socket_closeonexec(fd); + + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes; + + auto p = end_fd + 1; + for (;;) { + auto end = std::find(p, envend, ','); + + auto hex_cid_prefix = StringRef{p, end}; + if (hex_cid_prefix.size() != SHRPX_QUIC_CID_PREFIXLEN * 2 || + !util::is_hex_string(hex_cid_prefix)) { + LOG(WARN) << "Found invalid CID prefix=" << hex_cid_prefix; + break; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Inherit worker process CID prefix=" << hex_cid_prefix; + } + + cid_prefixes.emplace_back(); + + util::decode_hex(std::begin(cid_prefixes.back()), hex_cid_prefix); + + if (end == envend) { + break; + } + + p = end + 1; + } + + iwps.emplace_back(std::move(cid_prefixes), fd); + } + + return iwps; +} +} // namespace +#endif // ENABLE_HTTP3 + +namespace { +int create_acceptor_socket(Config *config, std::vector<InheritedAddr> &iaddrs) { + std::array<char, STRERROR_BUFSIZE> errbuf; + auto &listenerconf = config->conn.listener; + + for (auto &addr : listenerconf.addrs) { + if (addr.host_unix) { + if (create_unix_domain_server_socket(addr, iaddrs) != 0) { + return -1; + } + + if (config->uid != 0) { + // fd is not associated to inode, so we cannot use fchown(2) + // here. https://lkml.org/lkml/2004/11/1/84 + if (chown(addr.host.c_str(), config->uid, config->gid) == -1) { + auto error = errno; + LOG(WARN) << "Changing owner of UNIX domain socket " << addr.host + << " failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } + continue; + } + + if (create_tcp_server_socket(addr, iaddrs) != 0) { + return -1; + } + } + + return 0; +} +} // namespace + +namespace { +int call_daemon() { +#ifdef __sgi + return _daemonize(0, 0, 0, 0); +#else // !__sgi +# ifdef HAVE_LIBSYSTEMD + if (sd_booted() && (getenv("NOTIFY_SOCKET") != nullptr)) { + LOG(NOTICE) << "Daemonising disabled under systemd"; + chdir("/"); + return 0; + } +# endif // HAVE_LIBSYSTEMD + return util::daemonize(0, 0); +#endif // !__sgi +} +} // namespace + +namespace { +// Opens IPC socket used to communicate with worker proess. The +// communication is unidirectional; that is main process sends +// messages to the worker process. On success, ipc_fd[0] is for +// reading, and ipc_fd[1] for writing, just like pipe(2). +int create_ipc_socket(std::array<int, 2> &ipc_fd) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int rv; + + rv = pipe(ipc_fd.data()); + if (rv == -1) { + auto error = errno; + LOG(WARN) << "Failed to create pipe to communicate worker process: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + for (int i = 0; i < 2; ++i) { + auto fd = ipc_fd[i]; + util::make_socket_nonblocking(fd); + util::make_socket_closeonexec(fd); + } + + return 0; +} +} // namespace + +namespace { +int create_worker_process_ready_ipc_socket(std::array<int, 2> &ipc_fd) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int rv; + + rv = socketpair(AF_UNIX, SOCK_DGRAM, 0, ipc_fd.data()); + if (rv == -1) { + auto error = errno; + LOG(WARN) << "Failed to create socket pair to communicate worker process " + "readiness: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + for (auto fd : ipc_fd) { + util::make_socket_closeonexec(fd); + } + + util::make_socket_nonblocking(ipc_fd[0]); + + return 0; +} +} // namespace + +#ifdef ENABLE_HTTP3 +namespace { +int create_quic_ipc_socket(std::array<int, 2> &quic_ipc_fd) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int rv; + + rv = socketpair(AF_UNIX, SOCK_DGRAM, 0, quic_ipc_fd.data()); + if (rv == -1) { + auto error = errno; + LOG(WARN) << "Failed to create socket pair to communicate worker process: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + for (auto fd : quic_ipc_fd) { + util::make_socket_nonblocking(fd); + } + + return 0; +} +} // namespace + +namespace { +int generate_cid_prefix( + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> &cid_prefixes, + const Config *config) { + auto &apiconf = config->api; + auto &quicconf = config->quic; + + size_t num_cid_prefix; + if (config->single_thread) { + num_cid_prefix = 1; + } else { + num_cid_prefix = config->num_worker; + + // API endpoint occupies the one dedicated worker thread. + // Although such worker never gets QUIC traffic, we create CID + // prefix for it to make code a bit simpler. + if (apiconf.enabled) { + ++num_cid_prefix; + } + } + + cid_prefixes.resize(num_cid_prefix); + + for (auto &cid_prefix : cid_prefixes) { + if (create_cid_prefix(cid_prefix.data(), quicconf.server_id.data()) != 0) { + return -1; + } + } + + return 0; +} +} // namespace + +namespace { +std::vector<QUICLingeringWorkerProcess> +collect_quic_lingering_worker_processes() { + std::vector<QUICLingeringWorkerProcess> quic_lwps{ + std::begin(inherited_quic_lingering_worker_processes), + std::end(inherited_quic_lingering_worker_processes)}; + + for (auto &wp : worker_processes) { + quic_lwps.emplace_back(wp->cid_prefixes, wp->quic_ipc_fd); + } + + return quic_lwps; +} +} // namespace +#endif // ENABLE_HTTP3 + +namespace { +ev_signal reopen_log_signalev; +ev_signal exec_binary_signalev; +ev_signal graceful_shutdown_signalev; +ev_signal reload_signalev; +} // namespace + +namespace { +void start_signal_watchers(struct ev_loop *loop) { + ev_signal_init(&reopen_log_signalev, signal_cb, REOPEN_LOG_SIGNAL); + ev_signal_start(loop, &reopen_log_signalev); + + ev_signal_init(&exec_binary_signalev, signal_cb, EXEC_BINARY_SIGNAL); + ev_signal_start(loop, &exec_binary_signalev); + + ev_signal_init(&graceful_shutdown_signalev, signal_cb, + GRACEFUL_SHUTDOWN_SIGNAL); + ev_signal_start(loop, &graceful_shutdown_signalev); + + ev_signal_init(&reload_signalev, signal_cb, RELOAD_SIGNAL); + ev_signal_start(loop, &reload_signalev); +} +} // namespace + +namespace { +void shutdown_signal_watchers(struct ev_loop *loop) { + ev_signal_stop(loop, &reload_signalev); + ev_signal_stop(loop, &graceful_shutdown_signalev); + ev_signal_stop(loop, &exec_binary_signalev); + ev_signal_stop(loop, &reopen_log_signalev); +} +} // namespace + +namespace { +// A pair of connected socket with which a worker process tells main +// process that it is ready for service. A worker process writes its +// PID to worker_process_ready_ipc_fd[1] and main process reads it +// from worker_process_ready_ipc_fd[0]. +std::array<int, 2> worker_process_ready_ipc_fd; +} // namespace + +namespace { +ev_io worker_process_ready_ipcev; +} // namespace + +namespace { +// PID received via NGHTTPX_ORIG_PID environment variable. +pid_t orig_pid = -1; +} // namespace + +namespace { +void worker_process_ready_ipc_readcb(struct ev_loop *loop, ev_io *w, + int revents) { + std::array<uint8_t, 8> buf; + ssize_t nread; + + while ((nread = read(w->fd, buf.data(), buf.size())) == -1 && errno == EINTR) + ; + + if (nread == -1) { + std::array<char, STRERROR_BUFSIZE> errbuf; + auto error = errno; + + LOG(ERROR) << "Failed to read data from worker process ready IPC channel: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + return; + } + + if (nread == 0) { + return; + } + + if (nread != sizeof(pid_t)) { + LOG(ERROR) << "Read " << nread + << " bytes from worker process ready IPC channel"; + + return; + } + + pid_t pid; + + memcpy(&pid, buf.data(), sizeof(pid)); + + LOG(NOTICE) << "Worker process pid=" << pid << " is ready"; + + for (auto &wp : worker_processes) { + // Send graceful shutdown signal to all worker processes prior to + // pid. + if (wp->worker_pid == pid) { + break; + } + + LOG(INFO) << "Sending graceful shutdown event to worker process pid=" + << wp->worker_pid; + + ipc_send(wp.get(), SHRPX_IPC_GRACEFUL_SHUTDOWN); + worker_process_set_termination_deadline(wp.get(), loop); + } + + if (orig_pid != -1) { + LOG(NOTICE) << "Send QUIT signal to the original main process to tell " + "that we are ready to serve requests."; + kill(orig_pid, SIGQUIT); + + orig_pid = -1; + } +} +} // namespace + +namespace { +void start_worker_process_ready_ipc_watcher(struct ev_loop *loop) { + ev_io_init(&worker_process_ready_ipcev, worker_process_ready_ipc_readcb, + worker_process_ready_ipc_fd[0], EV_READ); + ev_io_start(loop, &worker_process_ready_ipcev); +} +} // namespace + +namespace { +void shutdown_worker_process_ready_ipc_watcher(struct ev_loop *loop) { + ev_io_stop(loop, &worker_process_ready_ipcev); +} +} // namespace + +namespace { +// Creates worker process, and returns PID of worker process. On +// success, file descriptor for IPC (send only) is assigned to +// |main_ipc_fd|. In child process, we will close file descriptors +// which are inherited from previous configuration/process, but not +// used in the current configuration. +pid_t fork_worker_process( + int &main_ipc_fd +#ifdef ENABLE_HTTP3 + , + int &wp_quic_ipc_fd +#endif // ENABLE_HTTP3 + , + const std::vector<InheritedAddr> &iaddrs +#ifdef ENABLE_HTTP3 + , + const std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> + &cid_prefixes, + const std::vector<QUICLingeringWorkerProcess> &quic_lwps +#endif // ENABLE_HTTP3 +) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int rv; + sigset_t oldset; + + std::array<int, 2> ipc_fd; + + rv = create_ipc_socket(ipc_fd); + if (rv != 0) { + return -1; + } + +#ifdef ENABLE_HTTP3 + std::array<int, 2> quic_ipc_fd; + + rv = create_quic_ipc_socket(quic_ipc_fd); + if (rv != 0) { + return -1; + } +#endif // ENABLE_HTTP3 + + rv = shrpx_signal_block_all(&oldset); + if (rv != 0) { + auto error = errno; + LOG(ERROR) << "Blocking all signals failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + close(ipc_fd[0]); + close(ipc_fd[1]); + + return -1; + } + + auto config = get_config(); + + pid_t pid = 0; + + if (!config->single_process) { + pid = fork(); + } + + if (pid == 0) { + // We are in new process now, update pid for logger. + log_config()->pid = getpid(); + + ev_loop_fork(EV_DEFAULT); + + for (auto &addr : config->conn.listener.addrs) { + util::make_socket_closeonexec(addr.fd); + } + +#ifdef ENABLE_HTTP3 + util::make_socket_closeonexec(quic_ipc_fd[0]); + + for (auto &lwp : quic_lwps) { + util::make_socket_closeonexec(lwp.quic_ipc_fd); + } + + for (auto &wp : worker_processes) { + util::make_socket_closeonexec(wp->quic_ipc_fd); + // Do not close quic_ipc_fd. + wp->quic_ipc_fd = -1; + } +#endif // ENABLE_HTTP3 + + if (!config->single_process) { + close(worker_process_ready_ipc_fd[0]); + shutdown_worker_process_ready_ipc_watcher(EV_DEFAULT); + + shutdown_signal_watchers(EV_DEFAULT); + } + + // Remove all WorkerProcesses to stop any registered watcher on + // default loop. + worker_process_remove_all(EV_DEFAULT); + + close_unused_inherited_addr(iaddrs); + + shrpx_signal_set_worker_proc_ign_handler(); + + rv = shrpx_signal_unblock_all(); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Unblocking all signals failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + if (config->single_process) { + exit(EXIT_FAILURE); + } else { + nghttp2_Exit(EXIT_FAILURE); + } + } + + if (!config->single_process) { + close(ipc_fd[1]); +#ifdef ENABLE_HTTP3 + close(quic_ipc_fd[1]); +#endif // ENABLE_HTTP3 + } + + WorkerProcessConfig wpconf{ + .ipc_fd = ipc_fd[0], + .ready_ipc_fd = worker_process_ready_ipc_fd[1], +#ifdef ENABLE_HTTP3 + .cid_prefixes = cid_prefixes, + .quic_ipc_fd = quic_ipc_fd[0], + .quic_lingering_worker_processes = quic_lwps, +#endif // ENABLE_HTTP3 + }; + rv = worker_process_event_loop(&wpconf); + if (rv != 0) { + LOG(FATAL) << "Worker process returned error"; + + if (config->single_process) { + exit(EXIT_FAILURE); + } else { + nghttp2_Exit(EXIT_FAILURE); + } + } + + LOG(NOTICE) << "Worker process shutting down momentarily"; + + // call exit(...) instead of nghttp2_Exit to get leak sanitizer report + if (config->single_process) { + exit(EXIT_SUCCESS); + } else { + nghttp2_Exit(EXIT_SUCCESS); + } + } + + // parent process + if (pid == -1) { + auto error = errno; + LOG(ERROR) << "Could not spawn worker process: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + + rv = shrpx_signal_set(&oldset); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Restoring signal mask failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + exit(EXIT_FAILURE); + } + + if (pid == -1) { + close(ipc_fd[0]); + close(ipc_fd[1]); +#ifdef ENABLE_HTTP3 + close(quic_ipc_fd[0]); + close(quic_ipc_fd[1]); +#endif // ENABLE_HTTP3 + + return -1; + } + + close(ipc_fd[0]); +#ifdef ENABLE_HTTP3 + close(quic_ipc_fd[0]); +#endif // ENABLE_HTTP3 + + main_ipc_fd = ipc_fd[1]; +#ifdef ENABLE_HTTP3 + wp_quic_ipc_fd = quic_ipc_fd[1]; +#endif // ENABLE_HTTP3 + + LOG(NOTICE) << "Worker process [" << pid << "] spawned"; + + return pid; +} +} // namespace + +namespace { +int event_loop() { + std::array<char, STRERROR_BUFSIZE> errbuf; + + shrpx_signal_set_main_proc_ign_handler(); + + auto config = mod_config(); + + if (config->daemon) { + if (call_daemon() == -1) { + auto error = errno; + LOG(FATAL) << "Failed to daemonize: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + // We get new PID after successful daemon(). + mod_config()->pid = getpid(); + + // daemon redirects stderr file descriptor to /dev/null, so we + // need this. + redirect_stderr_to_errorlog(config->logging); + } + + // update systemd PID tracking + shrpx_sd_notifyf(0, "MAINPID=%d\n", config->pid); + + { + auto iaddrs = get_inherited_addr_from_env(config); + + if (create_acceptor_socket(config, iaddrs) != 0) { + return -1; + } + + close_unused_inherited_addr(iaddrs); + } + + orig_pid = get_orig_pid_from_env(); + +#ifdef ENABLE_HTTP3 + inherited_quic_lingering_worker_processes = + get_inherited_quic_lingering_worker_process_from_env(); +#endif // ENABLE_HTTP3 + + auto loop = ev_default_loop(config->ev_loop_flags); + + int ipc_fd = 0; +#ifdef ENABLE_HTTP3 + int quic_ipc_fd = 0; + + auto quic_lwps = collect_quic_lingering_worker_processes(); + + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes; + + if (generate_cid_prefix(cid_prefixes, config) != 0) { + return -1; + } +#endif // ENABLE_HTTP3 + + if (!config->single_process) { + start_signal_watchers(loop); + } + + create_worker_process_ready_ipc_socket(worker_process_ready_ipc_fd); + start_worker_process_ready_ipc_watcher(loop); + + auto pid = fork_worker_process(ipc_fd +#ifdef ENABLE_HTTP3 + , + quic_ipc_fd +#endif // ENABLE_HTTP3 + , + {} +#ifdef ENABLE_HTTP3 + , + cid_prefixes, quic_lwps +#endif // ENABLE_HTTP3 + ); + + if (pid == -1) { + return -1; + } + + ev_timer_init(&worker_process_grace_period_timer, + worker_process_grace_period_timercb, 0., 0.); + + worker_process_add(std::make_unique<WorkerProcess>(loop, pid, ipc_fd +#ifdef ENABLE_HTTP3 + , + quic_ipc_fd, cid_prefixes +#endif // ENABLE_HTTP3 + )); + + // Write PID file when we are ready to accept connection from peer. + // This makes easier to write restart script for nghttpx. Because + // when we know that PID file is recreated, it means we can send + // QUIT signal to the old process to make it shutdown gracefully. + if (!config->pid_file.empty()) { + save_pid(); + } + + shrpx_sd_notifyf(0, "READY=1"); + + ev_run(loop, 0); + + ev_timer_stop(loop, &worker_process_grace_period_timer); + + shutdown_worker_process_ready_ipc_watcher(loop); + + // config is now stale if reload has happened. + if (!get_config()->single_process) { + shutdown_signal_watchers(loop); + } + + return 0; +} +} // namespace + +namespace { +// Returns true if regular file or symbolic link |path| exists. +bool conf_exists(const char *path) { + struct stat buf; + int rv = stat(path, &buf); + return rv == 0 && (buf.st_mode & (S_IFREG | S_IFLNK)); +} +} // namespace + +namespace { +constexpr auto DEFAULT_ALPN_LIST = + StringRef::from_lit("h2,h2-16,h2-14,http/1.1"); +} // namespace + +namespace { +constexpr auto DEFAULT_TLS_MIN_PROTO_VERSION = StringRef::from_lit("TLSv1.2"); +#ifdef TLS1_3_VERSION +constexpr auto DEFAULT_TLS_MAX_PROTO_VERSION = StringRef::from_lit("TLSv1.3"); +#else // !TLS1_3_VERSION +constexpr auto DEFAULT_TLS_MAX_PROTO_VERSION = StringRef::from_lit("TLSv1.2"); +#endif // !TLS1_3_VERSION +} // namespace + +namespace { +constexpr auto DEFAULT_ACCESSLOG_FORMAT = + StringRef::from_lit(R"($remote_addr - - [$time_local] )" + R"("$request" $status $body_bytes_sent )" + R"("$http_referer" "$http_user_agent")"); +} // namespace + +namespace { +void fill_default_config(Config *config) { + config->num_worker = 1; + config->conf_path = StringRef::from_lit("/etc/nghttpx/nghttpx.conf"); + config->pid = getpid(); + +#ifdef NOTHREADS + config->single_thread = true; +#endif // NOTHREADS + + if (ev_supported_backends() & ~ev_recommended_backends() & EVBACKEND_KQUEUE) { + config->ev_loop_flags = ev_recommended_backends() | EVBACKEND_KQUEUE; + } + + auto &tlsconf = config->tls; + { + auto &ticketconf = tlsconf.ticket; + { + auto &memcachedconf = ticketconf.memcached; + memcachedconf.max_retry = 3; + memcachedconf.max_fail = 2; + memcachedconf.interval = 10_min; + memcachedconf.family = AF_UNSPEC; + } + + auto &session_cacheconf = tlsconf.session_cache; + { + auto &memcachedconf = session_cacheconf.memcached; + memcachedconf.family = AF_UNSPEC; + } + + ticketconf.cipher = EVP_aes_128_cbc(); + } + + { + auto &ocspconf = tlsconf.ocsp; + // ocsp update interval = 14400 secs = 4 hours, borrowed from h2o + ocspconf.update_interval = 4_h; + ocspconf.fetch_ocsp_response_file = + StringRef::from_lit(PKGDATADIR "/fetch-ocsp-response"); + } + + { + auto &dyn_recconf = tlsconf.dyn_rec; + dyn_recconf.warmup_threshold = 1_m; + dyn_recconf.idle_timeout = 1_s; + } + + tlsconf.session_timeout = std::chrono::hours(12); + tlsconf.ciphers = StringRef::from_lit(nghttp2::tls::DEFAULT_CIPHER_LIST); + tlsconf.tls13_ciphers = + StringRef::from_lit(nghttp2::tls::DEFAULT_TLS13_CIPHER_LIST); + tlsconf.client.ciphers = + StringRef::from_lit(nghttp2::tls::DEFAULT_CIPHER_LIST); + tlsconf.client.tls13_ciphers = + StringRef::from_lit(nghttp2::tls::DEFAULT_TLS13_CIPHER_LIST); + tlsconf.min_proto_version = + tls::proto_version_from_string(DEFAULT_TLS_MIN_PROTO_VERSION); + tlsconf.max_proto_version = + tls::proto_version_from_string(DEFAULT_TLS_MAX_PROTO_VERSION); + tlsconf.max_early_data = 16_k; + tlsconf.ecdh_curves = StringRef::from_lit("X25519:P-256:P-384:P-521"); + + auto &httpconf = config->http; + httpconf.server_name = StringRef::from_lit("nghttpx"); + httpconf.no_host_rewrite = true; + httpconf.request_header_field_buffer = 64_k; + httpconf.max_request_header_fields = 100; + httpconf.response_header_field_buffer = 64_k; + httpconf.max_response_header_fields = 500; + httpconf.redirect_https_port = StringRef::from_lit("443"); + httpconf.max_requests = std::numeric_limits<size_t>::max(); + httpconf.xfp.add = true; + httpconf.xfp.strip_incoming = true; + httpconf.early_data.strip_incoming = true; + + auto &http2conf = config->http2; + { + auto &upstreamconf = http2conf.upstream; + + { + auto &timeoutconf = upstreamconf.timeout; + timeoutconf.settings = 10_s; + } + + // window size for HTTP/2 upstream connection per stream. 2**16-1 + // = 64KiB-1, which is HTTP/2 default. + upstreamconf.window_size = 64_k - 1; + // HTTP/2 has connection-level flow control. The default window + // size for HTTP/2 is 64KiB - 1. + upstreamconf.connection_window_size = 64_k - 1; + upstreamconf.max_concurrent_streams = 100; + + upstreamconf.encoder_dynamic_table_size = 4_k; + upstreamconf.decoder_dynamic_table_size = 4_k; + + nghttp2_option_new(&upstreamconf.option); + nghttp2_option_set_no_auto_window_update(upstreamconf.option, 1); + nghttp2_option_set_no_recv_client_magic(upstreamconf.option, 1); + nghttp2_option_set_max_deflate_dynamic_table_size( + upstreamconf.option, upstreamconf.encoder_dynamic_table_size); + nghttp2_option_set_server_fallback_rfc7540_priorities(upstreamconf.option, + 1); + nghttp2_option_set_builtin_recv_extension_type(upstreamconf.option, + NGHTTP2_PRIORITY_UPDATE); + + // For API endpoint, we enable automatic window update. This is + // because we are a sink. + nghttp2_option_new(&upstreamconf.alt_mode_option); + nghttp2_option_set_no_recv_client_magic(upstreamconf.alt_mode_option, 1); + nghttp2_option_set_max_deflate_dynamic_table_size( + upstreamconf.alt_mode_option, upstreamconf.encoder_dynamic_table_size); + } + + http2conf.timeout.stream_write = 1_min; + + { + auto &downstreamconf = http2conf.downstream; + + { + auto &timeoutconf = downstreamconf.timeout; + timeoutconf.settings = 10_s; + } + + downstreamconf.window_size = 64_k - 1; + downstreamconf.connection_window_size = (1u << 31) - 1; + downstreamconf.max_concurrent_streams = 100; + + downstreamconf.encoder_dynamic_table_size = 4_k; + downstreamconf.decoder_dynamic_table_size = 4_k; + + nghttp2_option_new(&downstreamconf.option); + nghttp2_option_set_no_auto_window_update(downstreamconf.option, 1); + nghttp2_option_set_peer_max_concurrent_streams(downstreamconf.option, 100); + nghttp2_option_set_max_deflate_dynamic_table_size( + downstreamconf.option, downstreamconf.encoder_dynamic_table_size); + } + +#ifdef ENABLE_HTTP3 + auto &quicconf = config->quic; + { + auto &upstreamconf = quicconf.upstream; + + { + auto &timeoutconf = upstreamconf.timeout; + timeoutconf.idle = 30_s; + } + + auto &bpfconf = quicconf.bpf; + bpfconf.prog_file = StringRef::from_lit(PKGLIBDIR "/reuseport_kern.o"); + + upstreamconf.congestion_controller = NGTCP2_CC_ALGO_CUBIC; + + upstreamconf.initial_rtt = + static_cast<ev_tstamp>(NGTCP2_DEFAULT_INITIAL_RTT) / NGTCP2_SECONDS; + } + + if (RAND_bytes(quicconf.server_id.data(), quicconf.server_id.size()) != 1) { + assert(0); + abort(); + } + + auto &http3conf = config->http3; + { + auto &upstreamconf = http3conf.upstream; + + upstreamconf.max_concurrent_streams = 100; + upstreamconf.window_size = 256_k; + upstreamconf.connection_window_size = 1_m; + upstreamconf.max_window_size = 6_m; + upstreamconf.max_connection_window_size = 8_m; + } +#endif // ENABLE_HTTP3 + + auto &loggingconf = config->logging; + { + auto &accessconf = loggingconf.access; + accessconf.format = + parse_log_format(config->balloc, DEFAULT_ACCESSLOG_FORMAT); + + auto &errorconf = loggingconf.error; + errorconf.file = StringRef::from_lit("/dev/stderr"); + } + + loggingconf.syslog_facility = LOG_DAEMON; + loggingconf.severity = NOTICE; + + auto &connconf = config->conn; + { + auto &listenerconf = connconf.listener; + { + // Default accept() backlog + listenerconf.backlog = 65536; + listenerconf.timeout.sleep = 30_s; + } + } + + { + auto &upstreamconf = connconf.upstream; + { + auto &timeoutconf = upstreamconf.timeout; + // Read timeout for HTTP2 upstream connection + timeoutconf.http2_read = 3_min; + + // Read timeout for HTTP3 upstream connection + timeoutconf.http3_read = 3_min; + + // Read timeout for non-HTTP2 upstream connection + timeoutconf.read = 1_min; + + // Write timeout for HTTP2/non-HTTP2 upstream connection + timeoutconf.write = 30_s; + + // Keep alive timeout for HTTP/1 upstream connection + timeoutconf.idle_read = 1_min; + } + } + + { + connconf.downstream = std::make_shared<DownstreamConfig>(); + auto &downstreamconf = *connconf.downstream; + { + auto &timeoutconf = downstreamconf.timeout; + // Read/Write timeouts for downstream connection + timeoutconf.read = 1_min; + timeoutconf.write = 30_s; + // Timeout for pooled (idle) connections + timeoutconf.idle_read = 2_s; + timeoutconf.connect = 30_s; + timeoutconf.max_backoff = 120_s; + } + + downstreamconf.connections_per_host = 8; + downstreamconf.request_buffer_size = 16_k; + downstreamconf.response_buffer_size = 128_k; + downstreamconf.family = AF_UNSPEC; + } + + auto &apiconf = config->api; + apiconf.max_request_body = 32_m; + + auto &dnsconf = config->dns; + { + auto &timeoutconf = dnsconf.timeout; + timeoutconf.cache = 10_s; + timeoutconf.lookup = 5_s; + } + dnsconf.max_try = 2; +} + +} // namespace + +namespace { +void print_version(std::ostream &out) { + out << "nghttpx nghttp2/" NGHTTP2_VERSION +#ifdef ENABLE_HTTP3 + " ngtcp2/" NGTCP2_VERSION " nghttp3/" NGHTTP3_VERSION +#endif // ENABLE_HTTP3 + << std::endl; +} +} // namespace + +namespace { +void print_usage(std::ostream &out) { + out << R"(Usage: nghttpx [OPTIONS]... [<PRIVATE_KEY> <CERT>] +A reverse proxy for HTTP/3, HTTP/2, and HTTP/1.)" + << std::endl; +} +} // namespace + +namespace { +void print_help(std::ostream &out) { + auto config = get_config(); + + print_usage(out); + out << R"( + <PRIVATE_KEY> + Set path to server's private key. Required unless + "no-tls" parameter is used in --frontend option. + <CERT> Set path to server's certificate. Required unless + "no-tls" parameter is used in --frontend option. To + make OCSP stapling work, this must be an absolute path. + +Options: + The options are categorized into several groups. + +Connections: + -b, --backend=(<HOST>,<PORT>|unix:<PATH>)[;[<PATTERN>[:...]][[;<PARAM>]...] + + Set backend host and port. The multiple backend + addresses are accepted by repeating this option. UNIX + domain socket can be specified by prefixing path name + with "unix:" (e.g., unix:/var/run/backend.sock). + + Optionally, if <PATTERN>s are given, the backend address + is only used if request matches the pattern. The + pattern matching is closely designed to ServeMux in + net/http package of Go programming language. <PATTERN> + consists of path, host + path or just host. The path + must start with "/". If it ends with "/", it matches + all request path in its subtree. To deal with the + request to the directory without trailing slash, the + path which ends with "/" also matches the request path + which only lacks trailing '/' (e.g., path "/foo/" + matches request path "/foo"). If it does not end with + "/", it performs exact match against the request path. + If host is given, it performs a match against the + request host. For a request received on the frontend + listener with "sni-fwd" parameter enabled, SNI host is + used instead of a request host. If host alone is given, + "/" is appended to it, so that it matches all request + paths under the host (e.g., specifying "nghttp2.org" + equals to "nghttp2.org/"). CONNECT method is treated + specially. It does not have path, and we don't allow + empty path. To workaround this, we assume that CONNECT + method has "/" as path. + + Patterns with host take precedence over patterns with + just path. Then, longer patterns take precedence over + shorter ones. + + Host can include "*" in the left most position to + indicate wildcard match (only suffix match is done). + The "*" must match at least one character. For example, + host pattern "*.nghttp2.org" matches against + "www.nghttp2.org" and "git.ngttp2.org", but does not + match against "nghttp2.org". The exact hosts match + takes precedence over the wildcard hosts match. + + If path part ends with "*", it is treated as wildcard + path. The wildcard path behaves differently from the + normal path. For normal path, match is made around the + boundary of path component separator,"/". On the other + hand, the wildcard path does not take into account the + path component separator. All paths which include the + wildcard path without last "*" as prefix, and are + strictly longer than wildcard path without last "*" are + matched. "*" must match at least one character. For + example, the pattern "/foo*" matches "/foo/" and + "/foobar". But it does not match "/foo", or "/fo". + + If <PATTERN> is omitted or empty string, "/" is used as + pattern, which matches all request paths (catch-all + pattern). The catch-all backend must be given. + + When doing a match, nghttpx made some normalization to + pattern, request host and path. For host part, they are + converted to lower case. For path part, percent-encoded + unreserved characters defined in RFC 3986 are decoded, + and any dot-segments (".." and ".") are resolved and + removed. + + For example, -b'127.0.0.1,8080;nghttp2.org/httpbin/' + matches the request host "nghttp2.org" and the request + path "/httpbin/get", but does not match the request host + "nghttp2.org" and the request path "/index.html". + + The multiple <PATTERN>s can be specified, delimiting + them by ":". Specifying + -b'127.0.0.1,8080;nghttp2.org:www.nghttp2.org' has the + same effect to specify -b'127.0.0.1,8080;nghttp2.org' + and -b'127.0.0.1,8080;www.nghttp2.org'. + + The backend addresses sharing same <PATTERN> are grouped + together forming load balancing group. + + Several parameters <PARAM> are accepted after <PATTERN>. + The parameters are delimited by ";". The available + parameters are: "proto=<PROTO>", "tls", + "sni=<SNI_HOST>", "fall=<N>", "rise=<N>", + "affinity=<METHOD>", "dns", "redirect-if-not-tls", + "upgrade-scheme", "mruby=<PATH>", + "read-timeout=<DURATION>", "write-timeout=<DURATION>", + "group=<GROUP>", "group-weight=<N>", "weight=<N>", and + "dnf". The parameter consists of keyword, and + optionally followed by "=" and value. For example, the + parameter "proto=h2" consists of the keyword "proto" and + value "h2". The parameter "tls" consists of the keyword + "tls" without value. Each parameter is described as + follows. + + The backend application protocol can be specified using + optional "proto" parameter, and in the form of + "proto=<PROTO>". <PROTO> should be one of the following + list without quotes: "h2", "http/1.1". The default + value of <PROTO> is "http/1.1". Note that usually "h2" + refers to HTTP/2 over TLS. But in this option, it may + mean HTTP/2 over cleartext TCP unless "tls" keyword is + used (see below). + + TLS can be enabled by specifying optional "tls" + parameter. TLS is not enabled by default. + + With "sni=<SNI_HOST>" parameter, it can override the TLS + SNI field value with given <SNI_HOST>. This will + default to the backend <HOST> name + + The feature to detect whether backend is online or + offline can be enabled using optional "fall" and "rise" + parameters. Using "fall=<N>" parameter, if nghttpx + cannot connect to a this backend <N> times in a row, + this backend is assumed to be offline, and it is + excluded from load balancing. If <N> is 0, this backend + never be excluded from load balancing whatever times + nghttpx cannot connect to it, and this is the default. + There is also "rise=<N>" parameter. After backend was + excluded from load balancing group, nghttpx periodically + attempts to make a connection to the failed backend, and + if the connection is made successfully <N> times in a + row, the backend is assumed to be online, and it is now + eligible for load balancing target. If <N> is 0, a + backend is permanently offline, once it goes in that + state, and this is the default behaviour. + + The session affinity is enabled using + "affinity=<METHOD>" parameter. If "ip" is given in + <METHOD>, client IP based session affinity is enabled. + If "cookie" is given in <METHOD>, cookie based session + affinity is enabled. If "none" is given in <METHOD>, + session affinity is disabled, and this is the default. + The session affinity is enabled per <PATTERN>. If at + least one backend has "affinity" parameter, and its + <METHOD> is not "none", session affinity is enabled for + all backend servers sharing the same <PATTERN>. It is + advised to set "affinity" parameter to all backend + explicitly if session affinity is desired. The session + affinity may break if one of the backend gets + unreachable, or backend settings are reloaded or + replaced by API. + + If "affinity=cookie" is used, the additional + configuration is required. + "affinity-cookie-name=<NAME>" must be used to specify a + name of cookie to use. Optionally, + "affinity-cookie-path=<PATH>" can be used to specify a + path which cookie is applied. The optional + "affinity-cookie-secure=<SECURE>" controls the Secure + attribute of a cookie. The default value is "auto", and + the Secure attribute is determined by a request scheme. + If a request scheme is "https", then Secure attribute is + set. Otherwise, it is not set. If <SECURE> is "yes", + the Secure attribute is always set. If <SECURE> is + "no", the Secure attribute is always omitted. + "affinity-cookie-stickiness=<STICKINESS>" controls + stickiness of this affinity. If <STICKINESS> is + "loose", removing or adding a backend server might break + the affinity and the request might be forwarded to a + different backend server. If <STICKINESS> is "strict", + removing the designated backend server breaks affinity, + but adding new backend server does not cause breakage. + If the designated backend server becomes unavailable, + new backend server is chosen as if the request does not + have an affinity cookie. <STICKINESS> defaults to + "loose". + + By default, name resolution of backend host name is done + at start up, or reloading configuration. If "dns" + parameter is given, name resolution takes place + dynamically. This is useful if backend address changes + frequently. If "dns" is given, name resolution of + backend host name at start up, or reloading + configuration is skipped. + + If "redirect-if-not-tls" parameter is used, the matched + backend requires that frontend connection is TLS + encrypted. If it isn't, nghttpx responds to the request + with 308 status code, and https URI the client should + use instead is included in Location header field. The + port number in redirect URI is 443 by default, and can + be changed using --redirect-https-port option. If at + least one backend has "redirect-if-not-tls" parameter, + this feature is enabled for all backend servers sharing + the same <PATTERN>. It is advised to set + "redirect-if-no-tls" parameter to all backends + explicitly if this feature is desired. + + If "upgrade-scheme" parameter is used along with "tls" + parameter, HTTP/2 :scheme pseudo header field is changed + to "https" from "http" when forwarding a request to this + particular backend. This is a workaround for a backend + server which requires "https" :scheme pseudo header + field on TLS encrypted connection. + + "mruby=<PATH>" parameter specifies a path to mruby + script file which is invoked when this pattern is + matched. All backends which share the same pattern must + have the same mruby path. + + "read-timeout=<DURATION>" and "write-timeout=<DURATION>" + parameters specify the read and write timeout of the + backend connection when this pattern is matched. All + backends which share the same pattern must have the same + timeouts. If these timeouts are entirely omitted for a + pattern, --backend-read-timeout and + --backend-write-timeout are used. + + "group=<GROUP>" parameter specifies the name of group + this backend address belongs to. By default, it belongs + to the unnamed default group. The name of group is + unique per pattern. "group-weight=<N>" parameter + specifies the weight of the group. The higher weight + gets more frequently selected by the load balancing + algorithm. <N> must be [1, 256] inclusive. The weight + 8 has 4 times more weight than 2. <N> must be the same + for all addresses which share the same <GROUP>. If + "group-weight" is omitted in an address, but the other + address which belongs to the same group specifies + "group-weight", its weight is used. If no + "group-weight" is specified for all addresses, the + weight of a group becomes 1. "group" and "group-weight" + are ignored if session affinity is enabled. + + "weight=<N>" parameter specifies the weight of the + backend address inside a group which this address + belongs to. The higher weight gets more frequently + selected by the load balancing algorithm. <N> must be + [1, 256] inclusive. The weight 8 has 4 times more + weight than weight 2. If this parameter is omitted, + weight becomes 1. "weight" is ignored if session + affinity is enabled. + + If "dnf" parameter is specified, an incoming request is + not forwarded to a backend and just consumed along with + the request body (actually a backend server never be + contacted). It is expected that the HTTP response is + generated by mruby script (see "mruby=<PATH>" parameter + above). "dnf" is an abbreviation of "do not forward". + + Since ";" and ":" are used as delimiter, <PATTERN> must + not contain these characters. In order to include ":" + in <PATTERN>, one has to specify "%3A" (which is + percent-encoded from of ":") instead. Since ";" has + special meaning in shell, the option value must be + quoted. + + Default: )" + << DEFAULT_DOWNSTREAM_HOST << "," << DEFAULT_DOWNSTREAM_PORT << R"( + -f, --frontend=(<HOST>,<PORT>|unix:<PATH>)[[;<PARAM>]...] + Set frontend host and port. If <HOST> is '*', it + assumes all addresses including both IPv4 and IPv6. + UNIX domain socket can be specified by prefixing path + name with "unix:" (e.g., unix:/var/run/nghttpx.sock). + This option can be used multiple times to listen to + multiple addresses. + + This option can take 0 or more parameters, which are + described below. Note that "api" and "healthmon" + parameters are mutually exclusive. + + Optionally, TLS can be disabled by specifying "no-tls" + parameter. TLS is enabled by default. + + If "sni-fwd" parameter is used, when performing a match + to select a backend server, SNI host name received from + the client is used instead of the request host. See + --backend option about the pattern match. + + To make this frontend as API endpoint, specify "api" + parameter. This is disabled by default. It is + important to limit the access to the API frontend. + Otherwise, someone may change the backend server, and + break your services, or expose confidential information + to the outside the world. + + To make this frontend as health monitor endpoint, + specify "healthmon" parameter. This is disabled by + default. Any requests which come through this address + are replied with 200 HTTP status, without no body. + + To accept PROXY protocol version 1 and 2 on frontend + connection, specify "proxyproto" parameter. This is + disabled by default. + + To receive HTTP/3 (QUIC) traffic, specify "quic" + parameter. It makes nghttpx listen on UDP port rather + than TCP port. UNIX domain socket, "api", and + "healthmon" parameters cannot be used with "quic" + parameter. + + Default: *,3000 + --backlog=<N> + Set listen backlog size. + Default: )" + << config->conn.listener.backlog << R"( + --backend-address-family=(auto|IPv4|IPv6) + Specify address family of backend connections. If + "auto" is given, both IPv4 and IPv6 are considered. If + "IPv4" is given, only IPv4 address is considered. If + "IPv6" is given, only IPv6 address is considered. + Default: auto + --backend-http-proxy-uri=<URI> + Specify proxy URI in the form + http://[<USER>:<PASS>@]<PROXY>:<PORT>. If a proxy + requires authentication, specify <USER> and <PASS>. + Note that they must be properly percent-encoded. This + proxy is used when the backend connection is HTTP/2. + First, make a CONNECT request to the proxy and it + connects to the backend on behalf of nghttpx. This + forms tunnel. After that, nghttpx performs SSL/TLS + handshake with the downstream through the tunnel. The + timeouts when connecting and making CONNECT request can + be specified by --backend-read-timeout and + --backend-write-timeout options. + +Performance: + -n, --workers=<N> + Set the number of worker threads. + Default: )" + << config->num_worker << R"( + --single-thread + Run everything in one thread inside the worker process. + This feature is provided for better debugging + experience, or for the platforms which lack thread + support. If threading is disabled, this option is + always enabled. + --read-rate=<SIZE> + Set maximum average read rate on frontend connection. + Setting 0 to this option means read rate is unlimited. + Default: )" + << config->conn.upstream.ratelimit.read.rate << R"( + --read-burst=<SIZE> + Set maximum read burst size on frontend connection. + Setting 0 to this option means read burst size is + unlimited. + Default: )" + << config->conn.upstream.ratelimit.read.burst << R"( + --write-rate=<SIZE> + Set maximum average write rate on frontend connection. + Setting 0 to this option means write rate is unlimited. + Default: )" + << config->conn.upstream.ratelimit.write.rate << R"( + --write-burst=<SIZE> + Set maximum write burst size on frontend connection. + Setting 0 to this option means write burst size is + unlimited. + Default: )" + << config->conn.upstream.ratelimit.write.burst << R"( + --worker-read-rate=<SIZE> + Set maximum average read rate on frontend connection per + worker. Setting 0 to this option means read rate is + unlimited. Not implemented yet. + Default: 0 + --worker-read-burst=<SIZE> + Set maximum read burst size on frontend connection per + worker. Setting 0 to this option means read burst size + is unlimited. Not implemented yet. + Default: 0 + --worker-write-rate=<SIZE> + Set maximum average write rate on frontend connection + per worker. Setting 0 to this option means write rate + is unlimited. Not implemented yet. + Default: 0 + --worker-write-burst=<SIZE> + Set maximum write burst size on frontend connection per + worker. Setting 0 to this option means write burst size + is unlimited. Not implemented yet. + Default: 0 + --worker-frontend-connections=<N> + Set maximum number of simultaneous connections frontend + accepts. Setting 0 means unlimited. + Default: )" + << config->conn.upstream.worker_connections << R"( + --backend-connections-per-host=<N> + Set maximum number of backend concurrent connections + (and/or streams in case of HTTP/2) per origin host. + This option is meaningful when --http2-proxy option is + used. The origin host is determined by authority + portion of request URI (or :authority header field for + HTTP/2). To limit the number of connections per + frontend for default mode, use + --backend-connections-per-frontend. + Default: )" + << config->conn.downstream->connections_per_host << R"( + --backend-connections-per-frontend=<N> + Set maximum number of backend concurrent connections + (and/or streams in case of HTTP/2) per frontend. This + option is only used for default mode. 0 means + unlimited. To limit the number of connections per host + with --http2-proxy option, use + --backend-connections-per-host. + Default: )" + << config->conn.downstream->connections_per_frontend << R"( + --rlimit-nofile=<N> + Set maximum number of open files (RLIMIT_NOFILE) to <N>. + If 0 is given, nghttpx does not set the limit. + Default: )" + << config->rlimit_nofile << R"( + --rlimit-memlock=<N> + Set maximum number of bytes of memory that may be locked + into RAM. If 0 is given, nghttpx does not set the + limit. + Default: )" + << config->rlimit_memlock << R"( + --backend-request-buffer=<SIZE> + Set buffer size used to store backend request. + Default: )" + << util::utos_unit(config->conn.downstream->request_buffer_size) << R"( + --backend-response-buffer=<SIZE> + Set buffer size used to store backend response. + Default: )" + << util::utos_unit(config->conn.downstream->response_buffer_size) << R"( + --fastopen=<N> + Enables "TCP Fast Open" for the listening socket and + limits the maximum length for the queue of connections + that have not yet completed the three-way handshake. If + value is 0 then fast open is disabled. + Default: )" + << config->conn.listener.fastopen << R"( + --no-kqueue Don't use kqueue. This option is only applicable for + the platforms which have kqueue. For other platforms, + this option will be simply ignored. + +Timeout: + --frontend-http2-read-timeout=<DURATION> + Specify read timeout for HTTP/2 frontend connection. + Default: )" + << util::duration_str(config->conn.upstream.timeout.http2_read) << R"( + --frontend-http3-read-timeout=<DURATION> + Specify read timeout for HTTP/3 frontend connection. + Default: )" + << util::duration_str(config->conn.upstream.timeout.http3_read) << R"( + --frontend-read-timeout=<DURATION> + Specify read timeout for HTTP/1.1 frontend connection. + Default: )" + << util::duration_str(config->conn.upstream.timeout.read) << R"( + --frontend-write-timeout=<DURATION> + Specify write timeout for all frontend connections. + Default: )" + << util::duration_str(config->conn.upstream.timeout.write) << R"( + --frontend-keep-alive-timeout=<DURATION> + Specify keep-alive timeout for frontend HTTP/1 + connection. + Default: )" + << util::duration_str(config->conn.upstream.timeout.idle_read) << R"( + --stream-read-timeout=<DURATION> + Specify read timeout for HTTP/2 streams. 0 means no + timeout. + Default: )" + << util::duration_str(config->http2.timeout.stream_read) << R"( + --stream-write-timeout=<DURATION> + Specify write timeout for HTTP/2 streams. 0 means no + timeout. + Default: )" + << util::duration_str(config->http2.timeout.stream_write) << R"( + --backend-read-timeout=<DURATION> + Specify read timeout for backend connection. + Default: )" + << util::duration_str(config->conn.downstream->timeout.read) << R"( + --backend-write-timeout=<DURATION> + Specify write timeout for backend connection. + Default: )" + << util::duration_str(config->conn.downstream->timeout.write) << R"( + --backend-connect-timeout=<DURATION> + Specify timeout before establishing TCP connection to + backend. + Default: )" + << util::duration_str(config->conn.downstream->timeout.connect) << R"( + --backend-keep-alive-timeout=<DURATION> + Specify keep-alive timeout for backend HTTP/1 + connection. + Default: )" + << util::duration_str(config->conn.downstream->timeout.idle_read) << R"( + --listener-disable-timeout=<DURATION> + After accepting connection failed, connection listener + is disabled for a given amount of time. Specifying 0 + disables this feature. + Default: )" + << util::duration_str(config->conn.listener.timeout.sleep) << R"( + --frontend-http2-setting-timeout=<DURATION> + Specify timeout before SETTINGS ACK is received from + client. + Default: )" + << util::duration_str(config->http2.upstream.timeout.settings) << R"( + --backend-http2-settings-timeout=<DURATION> + Specify timeout before SETTINGS ACK is received from + backend server. + Default: )" + << util::duration_str(config->http2.downstream.timeout.settings) << R"( + --backend-max-backoff=<DURATION> + Specify maximum backoff interval. This is used when + doing health check against offline backend (see "fail" + parameter in --backend option). It is also used to + limit the maximum interval to temporarily disable + backend when nghttpx failed to connect to it. These + intervals are calculated using exponential backoff, and + consecutive failed attempts increase the interval. This + option caps its maximum value. + Default: )" + << util::duration_str(config->conn.downstream->timeout.max_backoff) << R"( + +SSL/TLS: + --ciphers=<SUITE> + Set allowed cipher list for frontend connection. The + format of the string is described in OpenSSL ciphers(1). + This option sets cipher suites for TLSv1.2 or earlier. + Use --tls13-ciphers for TLSv1.3. + Default: )" + << config->tls.ciphers << R"( + --tls13-ciphers=<SUITE> + Set allowed cipher list for frontend connection. The + format of the string is described in OpenSSL ciphers(1). + This option sets cipher suites for TLSv1.3. Use + --ciphers for TLSv1.2 or earlier. + Default: )" + << config->tls.tls13_ciphers << R"( + --client-ciphers=<SUITE> + Set allowed cipher list for backend connection. The + format of the string is described in OpenSSL ciphers(1). + This option sets cipher suites for TLSv1.2 or earlier. + Use --tls13-client-ciphers for TLSv1.3. + Default: )" + << config->tls.client.ciphers << R"( + --tls13-client-ciphers=<SUITE> + Set allowed cipher list for backend connection. The + format of the string is described in OpenSSL ciphers(1). + This option sets cipher suites for TLSv1.3. Use + --tls13-client-ciphers for TLSv1.2 or earlier. + Default: )" + << config->tls.client.tls13_ciphers << R"( + --ecdh-curves=<LIST> + Set supported curve list for frontend connections. + <LIST> is a colon separated list of curve NID or names + in the preference order. The supported curves depend on + the linked OpenSSL library. This function requires + OpenSSL >= 1.0.2. + Default: )" + << config->tls.ecdh_curves << R"( + -k, --insecure + Don't verify backend server's certificate if TLS is + enabled for backend connections. + --cacert=<PATH> + Set path to trusted CA certificate file. It is used in + backend TLS connections to verify peer's certificate. + It is also used to verify OCSP response from the script + set by --fetch-ocsp-response-file. The file must be in + PEM format. It can contain multiple certificates. If + the linked OpenSSL is configured to load system wide + certificates, they are loaded at startup regardless of + this option. + --private-key-passwd-file=<PATH> + Path to file that contains password for the server's + private key. If none is given and the private key is + password protected it'll be requested interactively. + --subcert=<KEYPATH>:<CERTPATH>[[;<PARAM>]...] + Specify additional certificate and private key file. + nghttpx will choose certificates based on the hostname + indicated by client using TLS SNI extension. If nghttpx + is built with OpenSSL >= 1.0.2, the shared elliptic + curves (e.g., P-256) between client and server are also + taken into consideration. This allows nghttpx to send + ECDSA certificate to modern clients, while sending RSA + based certificate to older clients. This option can be + used multiple times. To make OCSP stapling work, + <CERTPATH> must be absolute path. + + Additional parameter can be specified in <PARAM>. The + available <PARAM> is "sct-dir=<DIR>". + + "sct-dir=<DIR>" specifies the path to directory which + contains *.sct files for TLS + signed_certificate_timestamp extension (RFC 6962). This + feature requires OpenSSL >= 1.0.2. See also + --tls-sct-dir option. + --dh-param-file=<PATH> + Path to file that contains DH parameters in PEM format. + Without this option, DHE cipher suites are not + available. + --alpn-list=<LIST> + Comma delimited list of ALPN protocol identifier sorted + in the order of preference. That means most desirable + protocol comes first. The parameter must be delimited + by a single comma only and any white spaces are treated + as a part of protocol string. + Default: )" + << DEFAULT_ALPN_LIST + << R"( + --verify-client + Require and verify client certificate. + --verify-client-cacert=<PATH> + Path to file that contains CA certificates to verify + client certificate. The file must be in PEM format. It + can contain multiple certificates. + --verify-client-tolerate-expired + Accept expired client certificate. Operator should + handle the expired client certificate by some means + (e.g., mruby script). Otherwise, this option might + cause a security risk. + --client-private-key-file=<PATH> + Path to file that contains client private key used in + backend client authentication. + --client-cert-file=<PATH> + Path to file that contains client certificate used in + backend client authentication. + --tls-min-proto-version=<VER> + Specify minimum SSL/TLS protocol. The name matching is + done in case-insensitive manner. The versions between + --tls-min-proto-version and --tls-max-proto-version are + enabled. If the protocol list advertised by client does + not overlap this range, you will receive the error + message "unknown protocol". If a protocol version lower + than TLSv1.2 is specified, make sure that the compatible + ciphers are included in --ciphers option. The default + cipher list only includes ciphers compatible with + TLSv1.2 or above. The available versions are: + )" +#ifdef TLS1_3_VERSION + "TLSv1.3, " +#endif // TLS1_3_VERSION + "TLSv1.2, TLSv1.1, and TLSv1.0" + R"( + Default: )" + << DEFAULT_TLS_MIN_PROTO_VERSION + << R"( + --tls-max-proto-version=<VER> + Specify maximum SSL/TLS protocol. The name matching is + done in case-insensitive manner. The versions between + --tls-min-proto-version and --tls-max-proto-version are + enabled. If the protocol list advertised by client does + not overlap this range, you will receive the error + message "unknown protocol". The available versions are: + )" +#ifdef TLS1_3_VERSION + "TLSv1.3, " +#endif // TLS1_3_VERSION + "TLSv1.2, TLSv1.1, and TLSv1.0" + R"( + Default: )" + << DEFAULT_TLS_MAX_PROTO_VERSION << R"( + --tls-ticket-key-file=<PATH> + Path to file that contains random data to construct TLS + session ticket parameters. If aes-128-cbc is given in + --tls-ticket-key-cipher, the file must contain exactly + 48 bytes. If aes-256-cbc is given in + --tls-ticket-key-cipher, the file must contain exactly + 80 bytes. This options can be used repeatedly to + specify multiple ticket parameters. If several files + are given, only the first key is used to encrypt TLS + session tickets. Other keys are accepted but server + will issue new session ticket with first key. This + allows session key rotation. Please note that key + rotation does not occur automatically. User should + rearrange files or change options values and restart + nghttpx gracefully. If opening or reading given file + fails, all loaded keys are discarded and it is treated + as if none of this option is given. If this option is + not given or an error occurred while opening or reading + a file, key is generated every 1 hour internally and + they are valid for 12 hours. This is recommended if + ticket key sharing between nghttpx instances is not + required. + --tls-ticket-key-memcached=<HOST>,<PORT>[;tls] + Specify address of memcached server to get TLS ticket + keys for session resumption. This enables shared TLS + ticket key between multiple nghttpx instances. nghttpx + does not set TLS ticket key to memcached. The external + ticket key generator is required. nghttpx just gets TLS + ticket keys from memcached, and use them, possibly + replacing current set of keys. It is up to extern TLS + ticket key generator to rotate keys frequently. See + "TLS SESSION TICKET RESUMPTION" section in manual page + to know the data format in memcached entry. Optionally, + memcached connection can be encrypted with TLS by + specifying "tls" parameter. + --tls-ticket-key-memcached-address-family=(auto|IPv4|IPv6) + Specify address family of memcached connections to get + TLS ticket keys. If "auto" is given, both IPv4 and IPv6 + are considered. If "IPv4" is given, only IPv4 address + is considered. If "IPv6" is given, only IPv6 address is + considered. + Default: auto + --tls-ticket-key-memcached-interval=<DURATION> + Set interval to get TLS ticket keys from memcached. + Default: )" + << util::duration_str(config->tls.ticket.memcached.interval) << R"( + --tls-ticket-key-memcached-max-retry=<N> + Set maximum number of consecutive retries before + abandoning TLS ticket key retrieval. If this number is + reached, the attempt is considered as failure, and + "failure" count is incremented by 1, which contributed + to the value controlled + --tls-ticket-key-memcached-max-fail option. + Default: )" + << config->tls.ticket.memcached.max_retry << R"( + --tls-ticket-key-memcached-max-fail=<N> + Set maximum number of consecutive failure before + disabling TLS ticket until next scheduled key retrieval. + Default: )" + << config->tls.ticket.memcached.max_fail << R"( + --tls-ticket-key-cipher=<CIPHER> + Specify cipher to encrypt TLS session ticket. Specify + either aes-128-cbc or aes-256-cbc. By default, + aes-128-cbc is used. + --tls-ticket-key-memcached-cert-file=<PATH> + Path to client certificate for memcached connections to + get TLS ticket keys. + --tls-ticket-key-memcached-private-key-file=<PATH> + Path to client private key for memcached connections to + get TLS ticket keys. + --fetch-ocsp-response-file=<PATH> + Path to fetch-ocsp-response script file. It should be + absolute path. + Default: )" + << config->tls.ocsp.fetch_ocsp_response_file << R"( + --ocsp-update-interval=<DURATION> + Set interval to update OCSP response cache. + Default: )" + << util::duration_str(config->tls.ocsp.update_interval) << R"( + --ocsp-startup + Start accepting connections after initial attempts to + get OCSP responses finish. It does not matter some of + the attempts fail. This feature is useful if OCSP + responses must be available before accepting + connections. + --no-verify-ocsp + nghttpx does not verify OCSP response. + --no-ocsp Disable OCSP stapling. + --tls-session-cache-memcached=<HOST>,<PORT>[;tls] + Specify address of memcached server to store session + cache. This enables shared session cache between + multiple nghttpx instances. Optionally, memcached + connection can be encrypted with TLS by specifying "tls" + parameter. + --tls-session-cache-memcached-address-family=(auto|IPv4|IPv6) + Specify address family of memcached connections to store + session cache. If "auto" is given, both IPv4 and IPv6 + are considered. If "IPv4" is given, only IPv4 address + is considered. If "IPv6" is given, only IPv6 address is + considered. + Default: auto + --tls-session-cache-memcached-cert-file=<PATH> + Path to client certificate for memcached connections to + store session cache. + --tls-session-cache-memcached-private-key-file=<PATH> + Path to client private key for memcached connections to + store session cache. + --tls-dyn-rec-warmup-threshold=<SIZE> + Specify the threshold size for TLS dynamic record size + behaviour. During a TLS session, after the threshold + number of bytes have been written, the TLS record size + will be increased to the maximum allowed (16K). The max + record size will continue to be used on the active TLS + session. After --tls-dyn-rec-idle-timeout has elapsed, + the record size is reduced to 1300 bytes. Specify 0 to + always use the maximum record size, regardless of idle + period. This behaviour applies to all TLS based + frontends, and TLS HTTP/2 backends. + Default: )" + << util::utos_unit(config->tls.dyn_rec.warmup_threshold) << R"( + --tls-dyn-rec-idle-timeout=<DURATION> + Specify TLS dynamic record size behaviour timeout. See + --tls-dyn-rec-warmup-threshold for more information. + This behaviour applies to all TLS based frontends, and + TLS HTTP/2 backends. + Default: )" + << util::duration_str(config->tls.dyn_rec.idle_timeout) << R"( + --no-http2-cipher-block-list + Allow block listed cipher suite on frontend HTTP/2 + connection. See + https://tools.ietf.org/html/rfc7540#appendix-A for the + complete HTTP/2 cipher suites block list. + --client-no-http2-cipher-block-list + Allow block listed cipher suite on backend HTTP/2 + connection. See + https://tools.ietf.org/html/rfc7540#appendix-A for the + complete HTTP/2 cipher suites block list. + --tls-sct-dir=<DIR> + Specifies the directory where *.sct files exist. All + *.sct files in <DIR> are read, and sent as + extension_data of TLS signed_certificate_timestamp (RFC + 6962) to client. These *.sct files are for the + certificate specified in positional command-line + argument <CERT>, or certificate option in configuration + file. For additional certificates, use --subcert + option. This option requires OpenSSL >= 1.0.2. + --psk-secrets=<PATH> + Read list of PSK identity and secrets from <PATH>. This + is used for frontend connection. The each line of input + file is formatted as <identity>:<hex-secret>, where + <identity> is PSK identity, and <hex-secret> is secret + in hex. An empty line, and line which starts with '#' + are skipped. The default enabled cipher list might not + contain any PSK cipher suite. In that case, desired PSK + cipher suites must be enabled using --ciphers option. + The desired PSK cipher suite may be block listed by + HTTP/2. To use those cipher suites with HTTP/2, + consider to use --no-http2-cipher-block-list option. + But be aware its implications. + --client-psk-secrets=<PATH> + Read PSK identity and secrets from <PATH>. This is used + for backend connection. The each line of input file is + formatted as <identity>:<hex-secret>, where <identity> + is PSK identity, and <hex-secret> is secret in hex. An + empty line, and line which starts with '#' are skipped. + The first identity and secret pair encountered is used. + The default enabled cipher list might not contain any + PSK cipher suite. In that case, desired PSK cipher + suites must be enabled using --client-ciphers option. + The desired PSK cipher suite may be block listed by + HTTP/2. To use those cipher suites with HTTP/2, + consider to use --client-no-http2-cipher-block-list + option. But be aware its implications. + --tls-no-postpone-early-data + By default, except for QUIC connections, nghttpx + postpones forwarding HTTP requests sent in early data, + including those sent in partially in it, until TLS + handshake finishes. If all backend server recognizes + "Early-Data" header field, using this option makes + nghttpx not postpone forwarding request and get full + potential of 0-RTT data. + --tls-max-early-data=<SIZE> + Sets the maximum amount of 0-RTT data that server + accepts. + Default: )" + << util::utos_unit(config->tls.max_early_data) << R"( + --tls-ktls Enable ktls. For server, ktls is enable if + --tls-session-cache-memcached is not configured. + +HTTP/2: + -c, --frontend-http2-max-concurrent-streams=<N> + Set the maximum number of the concurrent streams in one + frontend HTTP/2 session. + Default: )" + << config->http2.upstream.max_concurrent_streams << R"( + --backend-http2-max-concurrent-streams=<N> + Set the maximum number of the concurrent streams in one + backend HTTP/2 session. This sets maximum number of + concurrent opened pushed streams. The maximum number of + concurrent requests are set by a remote server. + Default: )" + << config->http2.downstream.max_concurrent_streams << R"( + --frontend-http2-window-size=<SIZE> + Sets the per-stream initial window size of HTTP/2 + frontend connection. + Default: )" + << config->http2.upstream.window_size << R"( + --frontend-http2-connection-window-size=<SIZE> + Sets the per-connection window size of HTTP/2 frontend + connection. + Default: )" + << config->http2.upstream.connection_window_size << R"( + --backend-http2-window-size=<SIZE> + Sets the initial window size of HTTP/2 backend + connection. + Default: )" + << config->http2.downstream.window_size << R"( + --backend-http2-connection-window-size=<SIZE> + Sets the per-connection window size of HTTP/2 backend + connection. + Default: )" + << config->http2.downstream.connection_window_size << R"( + --http2-no-cookie-crumbling + Don't crumble cookie header field. + --padding=<N> + Add at most <N> bytes to a HTTP/2 frame payload as + padding. Specify 0 to disable padding. This option is + meant for debugging purpose and not intended to enhance + protocol security. + --no-server-push + Disable HTTP/2 server push. Server push is supported by + default mode and HTTP/2 frontend via Link header field. + It is also supported if both frontend and backend are + HTTP/2 in default mode. In this case, server push from + backend session is relayed to frontend, and server push + via Link header field is also supported. + --frontend-http2-optimize-write-buffer-size + (Experimental) Enable write buffer size optimization in + frontend HTTP/2 TLS connection. This optimization aims + to reduce write buffer size so that it only contains + bytes which can send immediately. This makes server + more responsive to prioritized HTTP/2 stream because the + buffering of lower priority stream is reduced. This + option is only effective on recent Linux platform. + --frontend-http2-optimize-window-size + (Experimental) Automatically tune connection level + window size of frontend HTTP/2 TLS connection. If this + feature is enabled, connection window size starts with + the default window size, 65535 bytes. nghttpx + automatically adjusts connection window size based on + TCP receiving window size. The maximum window size is + capped by the value specified by + --frontend-http2-connection-window-size. Since the + stream is subject to stream level window size, it should + be adjusted using --frontend-http2-window-size option as + well. This option is only effective on recent Linux + platform. + --frontend-http2-encoder-dynamic-table-size=<SIZE> + Specify the maximum dynamic table size of HPACK encoder + in the frontend HTTP/2 connection. The decoder (client) + specifies the maximum dynamic table size it accepts. + Then the negotiated dynamic table size is the minimum of + this option value and the value which client specified. + Default: )" + << util::utos_unit(config->http2.upstream.encoder_dynamic_table_size) + << R"( + --frontend-http2-decoder-dynamic-table-size=<SIZE> + Specify the maximum dynamic table size of HPACK decoder + in the frontend HTTP/2 connection. + Default: )" + << util::utos_unit(config->http2.upstream.decoder_dynamic_table_size) + << R"( + --backend-http2-encoder-dynamic-table-size=<SIZE> + Specify the maximum dynamic table size of HPACK encoder + in the backend HTTP/2 connection. The decoder (backend) + specifies the maximum dynamic table size it accepts. + Then the negotiated dynamic table size is the minimum of + this option value and the value which backend specified. + Default: )" + << util::utos_unit(config->http2.downstream.encoder_dynamic_table_size) + << R"( + --backend-http2-decoder-dynamic-table-size=<SIZE> + Specify the maximum dynamic table size of HPACK decoder + in the backend HTTP/2 connection. + Default: )" + << util::utos_unit(config->http2.downstream.decoder_dynamic_table_size) + << R"( + +Mode: + (default mode) + Accept HTTP/2, and HTTP/1.1 over SSL/TLS. "no-tls" + parameter is used in --frontend option, accept HTTP/2 + and HTTP/1.1 over cleartext TCP. The incoming HTTP/1.1 + connection can be upgraded to HTTP/2 through HTTP + Upgrade. + -s, --http2-proxy + Like default mode, but enable forward proxy. This is so + called HTTP/2 proxy mode. + +Logging: + -L, --log-level=<LEVEL> + Set the severity level of log output. <LEVEL> must be + one of INFO, NOTICE, WARN, ERROR and FATAL. + Default: NOTICE + --accesslog-file=<PATH> + Set path to write access log. To reopen file, send USR1 + signal to nghttpx. + --accesslog-syslog + Send access log to syslog. If this option is used, + --accesslog-file option is ignored. + --accesslog-format=<FORMAT> + Specify format string for access log. The default + format is combined format. The following variables are + available: + + * $remote_addr: client IP address. + * $time_local: local time in Common Log format. + * $time_iso8601: local time in ISO 8601 format. + * $request: HTTP request line. + * $status: HTTP response status code. + * $body_bytes_sent: the number of bytes sent to client + as response body. + * $http_<VAR>: value of HTTP request header <VAR> where + '_' in <VAR> is replaced with '-'. + * $remote_port: client port. + * $server_port: server port. + * $request_time: request processing time in seconds with + milliseconds resolution. + * $pid: PID of the running process. + * $alpn: ALPN identifier of the protocol which generates + the response. For HTTP/1, ALPN is always http/1.1, + regardless of minor version. + * $tls_cipher: cipher used for SSL/TLS connection. + * $tls_client_fingerprint_sha256: SHA-256 fingerprint of + client certificate. + * $tls_client_fingerprint_sha1: SHA-1 fingerprint of + client certificate. + * $tls_client_subject_name: subject name in client + certificate. + * $tls_client_issuer_name: issuer name in client + certificate. + * $tls_client_serial: serial number in client + certificate. + * $tls_protocol: protocol for SSL/TLS connection. + * $tls_session_id: session ID for SSL/TLS connection. + * $tls_session_reused: "r" if SSL/TLS session was + reused. Otherwise, "." + * $tls_sni: SNI server name for SSL/TLS connection. + * $backend_host: backend host used to fulfill the + request. "-" if backend host is not available. + * $backend_port: backend port used to fulfill the + request. "-" if backend host is not available. + * $method: HTTP method + * $path: Request path including query. For CONNECT + request, authority is recorded. + * $path_without_query: $path up to the first '?' + character. For CONNECT request, authority is + recorded. + * $protocol_version: HTTP version (e.g., HTTP/1.1, + HTTP/2) + + The variable can be enclosed by "{" and "}" for + disambiguation (e.g., ${remote_addr}). + + Default: )" + << DEFAULT_ACCESSLOG_FORMAT << R"( + --accesslog-write-early + Write access log when response header fields are + received from backend rather than when request + transaction finishes. + --errorlog-file=<PATH> + Set path to write error log. To reopen file, send USR1 + signal to nghttpx. stderr will be redirected to the + error log file unless --errorlog-syslog is used. + Default: )" + << config->logging.error.file << R"( + --errorlog-syslog + Send error log to syslog. If this option is used, + --errorlog-file option is ignored. + --syslog-facility=<FACILITY> + Set syslog facility to <FACILITY>. + Default: )" + << str_syslog_facility(config->logging.syslog_facility) << R"( + +HTTP: + --add-x-forwarded-for + Append X-Forwarded-For header field to the downstream + request. + --strip-incoming-x-forwarded-for + Strip X-Forwarded-For header field from inbound client + requests. + --no-add-x-forwarded-proto + Don't append additional X-Forwarded-Proto header field + to the backend request. If inbound client sets + X-Forwarded-Proto, and + --no-strip-incoming-x-forwarded-proto option is used, + they are passed to the backend. + --no-strip-incoming-x-forwarded-proto + Don't strip X-Forwarded-Proto header field from inbound + client requests. + --add-forwarded=<LIST> + Append RFC 7239 Forwarded header field with parameters + specified in comma delimited list <LIST>. The supported + parameters are "by", "for", "host", and "proto". By + default, the value of "by" and "for" parameters are + obfuscated string. See --forwarded-by and + --forwarded-for options respectively. Note that nghttpx + does not translate non-standard X-Forwarded-* header + fields into Forwarded header field, and vice versa. + --strip-incoming-forwarded + Strip Forwarded header field from inbound client + requests. + --forwarded-by=(obfuscated|ip|<VALUE>) + Specify the parameter value sent out with "by" parameter + of Forwarded header field. If "obfuscated" is given, + the string is randomly generated at startup. If "ip" is + given, the interface address of the connection, + including port number, is sent with "by" parameter. In + case of UNIX domain socket, "localhost" is used instead + of address and port. User can also specify the static + obfuscated string. The limitation is that it must start + with "_", and only consists of character set + [A-Za-z0-9._-], as described in RFC 7239. + Default: obfuscated + --forwarded-for=(obfuscated|ip) + Specify the parameter value sent out with "for" + parameter of Forwarded header field. If "obfuscated" is + given, the string is randomly generated for each client + connection. If "ip" is given, the remote client address + of the connection, without port number, is sent with + "for" parameter. In case of UNIX domain socket, + "localhost" is used instead of address. + Default: obfuscated + --no-via Don't append to Via header field. If Via header field + is received, it is left unaltered. + --no-strip-incoming-early-data + Don't strip Early-Data header field from inbound client + requests. + --no-location-rewrite + Don't rewrite location header field in default mode. + When --http2-proxy is used, location header field will + not be altered regardless of this option. + --host-rewrite + Rewrite host and :authority header fields in default + mode. When --http2-proxy is used, these headers will + not be altered regardless of this option. + --altsvc=<PROTOID,PORT[,HOST,[ORIGIN[,PARAMS]]]> + Specify protocol ID, port, host and origin of + alternative service. <HOST>, <ORIGIN> and <PARAMS> are + optional. Empty <HOST> and <ORIGIN> are allowed and + they are treated as nothing is specified. They are + advertised in alt-svc header field only in HTTP/1.1 + frontend. This option can be used multiple times to + specify multiple alternative services. + Example: --altsvc="h2,443,,,ma=3600; persist=1" + --http2-altsvc=<PROTOID,PORT[,HOST,[ORIGIN[,PARAMS]]]> + Just like --altsvc option, but this altsvc is only sent + in HTTP/2 frontend. + --add-request-header=<HEADER> + Specify additional header field to add to request header + set. The field name must be lowercase. This option + just appends header field and won't replace anything + already set. This option can be used several times to + specify multiple header fields. + Example: --add-request-header="foo: bar" + --add-response-header=<HEADER> + Specify additional header field to add to response + header set. The field name must be lowercase. This + option just appends header field and won't replace + anything already set. This option can be used several + times to specify multiple header fields. + Example: --add-response-header="foo: bar" + --request-header-field-buffer=<SIZE> + Set maximum buffer size for incoming HTTP request header + field list. This is the sum of header name and value in + bytes. If trailer fields exist, they are counted + towards this number. + Default: )" + << util::utos_unit(config->http.request_header_field_buffer) << R"( + --max-request-header-fields=<N> + Set maximum number of incoming HTTP request header + fields. If trailer fields exist, they are counted + towards this number. + Default: )" + << config->http.max_request_header_fields << R"( + --response-header-field-buffer=<SIZE> + Set maximum buffer size for incoming HTTP response + header field list. This is the sum of header name and + value in bytes. If trailer fields exist, they are + counted towards this number. + Default: )" + << util::utos_unit(config->http.response_header_field_buffer) << R"( + --max-response-header-fields=<N> + Set maximum number of incoming HTTP response header + fields. If trailer fields exist, they are counted + towards this number. + Default: )" + << config->http.max_response_header_fields << R"( + --error-page=(<CODE>|*)=<PATH> + Set file path to custom error page served when nghttpx + originally generates HTTP error status code <CODE>. + <CODE> must be greater than or equal to 400, and at most + 599. If "*" is used instead of <CODE>, it matches all + HTTP status code. If error status code comes from + backend server, the custom error pages are not used. + --server-name=<NAME> + Change server response header field value to <NAME>. + Default: )" + << config->http.server_name << R"( + --no-server-rewrite + Don't rewrite server header field in default mode. When + --http2-proxy is used, these headers will not be altered + regardless of this option. + --redirect-https-port=<PORT> + Specify the port number which appears in Location header + field when redirect to HTTPS URI is made due to + "redirect-if-not-tls" parameter in --backend option. + Default: )" + << config->http.redirect_https_port << R"( + --require-http-scheme + Always require http or https scheme in HTTP request. It + also requires that https scheme must be used for an + encrypted connection. Otherwise, http scheme must be + used. This option is recommended for a server + deployment which directly faces clients and the services + it provides only require http or https scheme. + +API: + --api-max-request-body=<SIZE> + Set the maximum size of request body for API request. + Default: )" + << util::utos_unit(config->api.max_request_body) << R"( + +DNS: + --dns-cache-timeout=<DURATION> + Set duration that cached DNS results remain valid. Note + that nghttpx caches the unsuccessful results as well. + Default: )" + << util::duration_str(config->dns.timeout.cache) << R"( + --dns-lookup-timeout=<DURATION> + Set timeout that DNS server is given to respond to the + initial DNS query. For the 2nd and later queries, + server is given time based on this timeout, and it is + scaled linearly. + Default: )" + << util::duration_str(config->dns.timeout.lookup) << R"( + --dns-max-try=<N> + Set the number of DNS query before nghttpx gives up name + lookup. + Default: )" + << config->dns.max_try << R"( + --frontend-max-requests=<N> + The number of requests that single frontend connection + can process. For HTTP/2, this is the number of streams + in one HTTP/2 connection. For HTTP/1, this is the + number of keep alive requests. This is hint to nghttpx, + and it may allow additional few requests. The default + value is unlimited. + +Debug: + --frontend-http2-dump-request-header=<PATH> + Dumps request headers received by HTTP/2 frontend to the + file denoted in <PATH>. The output is done in HTTP/1 + header field format and each header block is followed by + an empty line. This option is not thread safe and MUST + NOT be used with option -n<N>, where <N> >= 2. + --frontend-http2-dump-response-header=<PATH> + Dumps response headers sent from HTTP/2 frontend to the + file denoted in <PATH>. The output is done in HTTP/1 + header field format and each header block is followed by + an empty line. This option is not thread safe and MUST + NOT be used with option -n<N>, where <N> >= 2. + -o, --frontend-frame-debug + Print HTTP/2 frames in frontend to stderr. This option + is not thread safe and MUST NOT be used with option + -n=N, where N >= 2. + +Process: + -D, --daemon + Run in a background. If -D is used, the current working + directory is changed to '/'. + --pid-file=<PATH> + Set path to save PID of this program. + --user=<USER> + Run this program as <USER>. This option is intended to + be used to drop root privileges. + --single-process + Run this program in a single process mode for debugging + purpose. Without this option, nghttpx creates at least + 2 processes: main and worker processes. If this option + is used, main and worker are unified into a single + process. nghttpx still spawns additional process if + neverbleed is used. In the single process mode, the + signal handling feature is disabled. + --max-worker-processes=<N> + The maximum number of worker processes. nghttpx spawns + new worker process when it reloads its configuration. + The previous worker process enters graceful termination + period and will terminate when it finishes handling the + existing connections. However, if reloading + configurations happen very frequently, the worker + processes might be piled up if they take a bit long time + to finish the existing connections. With this option, + if the number of worker processes exceeds the given + value, the oldest worker process is terminated + immediately. Specifying 0 means no limit and it is the + default behaviour. + --worker-process-grace-shutdown-period=<DURATION> + Maximum period for a worker process to terminate + gracefully. When a worker process enters in graceful + shutdown period (e.g., when nghttpx reloads its + configuration) and it does not finish handling the + existing connections in the given period of time, it is + immediately terminated. Specifying 0 means no limit and + it is the default behaviour. + +Scripting: + --mruby-file=<PATH> + Set mruby script file + --ignore-per-pattern-mruby-error + Ignore mruby compile error for per-pattern mruby script + file. If error occurred, it is treated as if no mruby + file were specified for the pattern. +)"; + +#ifdef ENABLE_HTTP3 + out << R"( +HTTP/3 and QUIC: + --frontend-quic-idle-timeout=<DURATION> + Specify an idle timeout for QUIC connection. + Default: )" + << util::duration_str(config->quic.upstream.timeout.idle) << R"( + --frontend-quic-debug-log + Output QUIC debug log to /dev/stderr. + --quic-bpf-program-file=<PATH> + Specify a path to eBPF program file reuseport_kern.o to + direct an incoming QUIC UDP datagram to a correct + socket. + Default: )" + << config->quic.bpf.prog_file << R"( + --frontend-quic-early-data + Enable early data on frontend QUIC connections. nghttpx + sends "Early-Data" header field to a backend server if a + request is received in early data and handshake has not + finished. All backend servers should deal with possibly + replayed requests. + --frontend-quic-qlog-dir=<DIR> + Specify a directory where a qlog file is written for + frontend QUIC connections. A qlog file is created per + each QUIC connection. The file name is ISO8601 basic + format, followed by "-", server Source Connection ID and + ".sqlog". + --frontend-quic-require-token + Require an address validation token for a frontend QUIC + connection. Server sends a token in Retry packet or + NEW_TOKEN frame in the previous connection. + --frontend-quic-congestion-controller=<CC> + Specify a congestion controller algorithm for a frontend + QUIC connection. <CC> should be either "cubic" or + "bbr". + Default: )" + << (config->quic.upstream.congestion_controller == NGTCP2_CC_ALGO_CUBIC + ? "cubic" + : "bbr") + << R"( + --frontend-quic-secret-file=<PATH> + Path to file that contains secure random data to be used + as QUIC keying materials. It is used to derive keys for + encrypting tokens and Connection IDs. It is not used to + encrypt QUIC packets. Each line of this file must + contain exactly 136 bytes hex-encoded string (when + decoded the byte string is 68 bytes long). The first 2 + bits of decoded byte string are used to identify the + keying material. An empty line or a line which starts + '#' is ignored. The file can contain more than one + keying materials. Because the identifier is 2 bits, at + most 4 keying materials are read and the remaining data + is discarded. The first keying material in the file is + primarily used for encryption and decryption for new + connection. The other ones are used to decrypt data for + the existing connections. Specifying multiple keying + materials enables key rotation. Please note that key + rotation does not occur automatically. User should + update files or change options values and restart + nghttpx gracefully. If opening or reading given file + fails, all loaded keying materials are discarded and it + is treated as if none of this option is given. If this + option is not given or an error occurred while opening + or reading a file, a keying material is generated + internally on startup and reload. + --quic-server-id=<HEXSTRING> + Specify server ID encoded in Connection ID to identify + this particular server instance. Connection ID is + encrypted and this part is not visible in public. It + must be 4 bytes long and must be encoded in hex string + (which is 8 bytes long). If this option is omitted, a + random server ID is generated on startup and + configuration reload. + --frontend-quic-initial-rtt=<DURATION> + Specify the initial RTT of the frontend QUIC connection. + Default: )" + << util::duration_str(config->quic.upstream.initial_rtt) << R"( + --no-quic-bpf + Disable eBPF. + --frontend-http3-window-size=<SIZE> + Sets the per-stream initial window size of HTTP/3 + frontend connection. + Default: )" + << util::utos_unit(config->http3.upstream.window_size) << R"( + --frontend-http3-connection-window-size=<SIZE> + Sets the per-connection window size of HTTP/3 frontend + connection. + Default: )" + << util::utos_unit(config->http3.upstream.connection_window_size) << R"( + --frontend-http3-max-window-size=<SIZE> + Sets the maximum per-stream window size of HTTP/3 + frontend connection. The window size is adjusted based + on the receiving rate of stream data. The initial value + is the value specified by --frontend-http3-window-size + and the window size grows up to <SIZE> bytes. + Default: )" + << util::utos_unit(config->http3.upstream.max_window_size) << R"( + --frontend-http3-max-connection-window-size=<SIZE> + Sets the maximum per-connection window size of HTTP/3 + frontend connection. The window size is adjusted based + on the receiving rate of stream data. The initial value + is the value specified by + --frontend-http3-connection-window-size and the window + size grows up to <SIZE> bytes. + Default: )" + << util::utos_unit(config->http3.upstream.max_connection_window_size) + << R"( + --frontend-http3-max-concurrent-streams=<N> + Set the maximum number of the concurrent streams in one + frontend HTTP/3 connection. + Default: )" + << config->http3.upstream.max_concurrent_streams << R"( +)"; +#endif // ENABLE_HTTP3 + + out << R"( +Misc: + --conf=<PATH> + Load configuration from <PATH>. Please note that + nghttpx always tries to read the default configuration + file if --conf is not given. + Default: )" + << config->conf_path << R"( + --include=<PATH> + Load additional configurations from <PATH>. File <PATH> + is read when configuration parser encountered this + option. This option can be used multiple times, or even + recursively. + -v, --version + Print version and exit. + -h, --help Print this help and exit. + +-- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms + (hours, minutes, seconds and milliseconds, respectively). If a unit + is omitted, a second is used as unit.)" + << std::endl; +} +} // namespace + +namespace { +int process_options(Config *config, + std::vector<std::pair<StringRef, StringRef>> &cmdcfgs) { + std::array<char, STRERROR_BUFSIZE> errbuf; + std::map<StringRef, size_t> pattern_addr_indexer; + if (conf_exists(config->conf_path.c_str())) { + LOG(NOTICE) << "Loading configuration from " << config->conf_path; + std::set<StringRef> include_set; + if (load_config(config, config->conf_path.c_str(), include_set, + pattern_addr_indexer) == -1) { + LOG(FATAL) << "Failed to load configuration from " << config->conf_path; + return -1; + } + assert(include_set.empty()); + } + + // Reopen log files using configurations in file + reopen_log_files(config->logging); + + { + std::set<StringRef> include_set; + + for (auto &p : cmdcfgs) { + if (parse_config(config, p.first, p.second, include_set, + pattern_addr_indexer) == -1) { + LOG(FATAL) << "Failed to parse command-line argument."; + return -1; + } + } + + assert(include_set.empty()); + } + + Log::set_severity_level(config->logging.severity); + + auto &loggingconf = config->logging; + + if (loggingconf.access.syslog || loggingconf.error.syslog) { + openlog("nghttpx", LOG_NDELAY | LOG_NOWAIT | LOG_PID, + loggingconf.syslog_facility); + } + + if (reopen_log_files(config->logging) != 0) { + LOG(FATAL) << "Failed to open log file"; + return -1; + } + + redirect_stderr_to_errorlog(loggingconf); + + if (config->uid != 0) { + if (log_config()->accesslog_fd != -1 && + fchown(log_config()->accesslog_fd, config->uid, config->gid) == -1) { + auto error = errno; + LOG(WARN) << "Changing owner of access log file failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + if (log_config()->errorlog_fd != -1 && + fchown(log_config()->errorlog_fd, config->uid, config->gid) == -1) { + auto error = errno; + LOG(WARN) << "Changing owner of error log file failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } + + if (config->single_thread) { + LOG(WARN) << "single-thread: Set workers to 1"; + config->num_worker = 1; + } + + auto &http2conf = config->http2; + { + auto &dumpconf = http2conf.upstream.debug.dump; + + if (!dumpconf.request_header_file.empty()) { + auto path = dumpconf.request_header_file.c_str(); + auto f = open_file_for_write(path); + + if (f == nullptr) { + LOG(FATAL) << "Failed to open http2 upstream request header file: " + << path; + return -1; + } + + dumpconf.request_header = f; + + if (config->uid != 0) { + if (chown(path, config->uid, config->gid) == -1) { + auto error = errno; + LOG(WARN) << "Changing owner of http2 upstream request header file " + << path << " failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } + } + + if (!dumpconf.response_header_file.empty()) { + auto path = dumpconf.response_header_file.c_str(); + auto f = open_file_for_write(path); + + if (f == nullptr) { + LOG(FATAL) << "Failed to open http2 upstream response header file: " + << path; + return -1; + } + + dumpconf.response_header = f; + + if (config->uid != 0) { + if (chown(path, config->uid, config->gid) == -1) { + auto error = errno; + LOG(WARN) << "Changing owner of http2 upstream response header file" + << " " << path << " failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } + } + } + + auto &tlsconf = config->tls; + + if (tlsconf.alpn_list.empty()) { + tlsconf.alpn_list = util::split_str(DEFAULT_ALPN_LIST, ','); + } + + if (!tlsconf.tls_proto_list.empty()) { + tlsconf.tls_proto_mask = tls::create_tls_proto_mask(tlsconf.tls_proto_list); + } + + // TODO We depends on the ordering of protocol version macro in + // OpenSSL. + if (tlsconf.min_proto_version > tlsconf.max_proto_version) { + LOG(ERROR) << "tls-max-proto-version must be equal to or larger than " + "tls-min-proto-version"; + return -1; + } + + if (tls::set_alpn_prefs(tlsconf.alpn_prefs, tlsconf.alpn_list) != 0) { + return -1; + } + + tlsconf.bio_method = create_bio_method(); + + auto &listenerconf = config->conn.listener; + auto &upstreamconf = config->conn.upstream; + + if (listenerconf.addrs.empty()) { + UpstreamAddr addr{}; + addr.host = StringRef::from_lit("*"); + addr.port = 3000; + addr.tls = true; + addr.family = AF_INET; + addr.index = 0; + listenerconf.addrs.push_back(addr); + addr.family = AF_INET6; + addr.index = 1; + listenerconf.addrs.push_back(std::move(addr)); + } + + if (upstreamconf.worker_connections == 0) { + upstreamconf.worker_connections = std::numeric_limits<size_t>::max(); + } + + if (tls::upstream_tls_enabled(config->conn) && + (tlsconf.private_key_file.empty() || tlsconf.cert_file.empty())) { + LOG(FATAL) << "TLS private key and certificate files are required. " + "Specify them in command-line, or in configuration file " + "using private-key-file and certificate-file options."; + return -1; + } + + if (tls::upstream_tls_enabled(config->conn) && !tlsconf.ocsp.disabled) { + struct stat buf; + if (stat(tlsconf.ocsp.fetch_ocsp_response_file.c_str(), &buf) != 0) { + tlsconf.ocsp.disabled = true; + LOG(WARN) << "--fetch-ocsp-response-file: " + << tlsconf.ocsp.fetch_ocsp_response_file + << " not found. OCSP stapling has been disabled."; + } + } + + if (configure_downstream_group(config, config->http2_proxy, false, tlsconf) != + 0) { + return -1; + } + + std::array<char, util::max_hostport> hostport_buf; + + auto &proxy = config->downstream_http_proxy; + if (!proxy.host.empty()) { + auto hostport = util::make_hostport(std::begin(hostport_buf), + StringRef{proxy.host}, proxy.port); + if (resolve_hostname(&proxy.addr, proxy.host.c_str(), proxy.port, + AF_UNSPEC) == -1) { + LOG(FATAL) << "Resolving backend HTTP proxy address failed: " << hostport; + return -1; + } + LOG(NOTICE) << "Backend HTTP proxy address: " << hostport << " -> " + << util::to_numeric_addr(&proxy.addr); + } + + { + auto &memcachedconf = tlsconf.session_cache.memcached; + if (!memcachedconf.host.empty()) { + auto hostport = util::make_hostport(std::begin(hostport_buf), + StringRef{memcachedconf.host}, + memcachedconf.port); + if (resolve_hostname(&memcachedconf.addr, memcachedconf.host.c_str(), + memcachedconf.port, memcachedconf.family) == -1) { + LOG(FATAL) + << "Resolving memcached address for TLS session cache failed: " + << hostport; + return -1; + } + LOG(NOTICE) << "Memcached address for TLS session cache: " << hostport + << " -> " << util::to_numeric_addr(&memcachedconf.addr); + if (memcachedconf.tls) { + LOG(NOTICE) << "Connection to memcached for TLS session cache will be " + "encrypted by TLS"; + } + } + } + + { + auto &memcachedconf = tlsconf.ticket.memcached; + if (!memcachedconf.host.empty()) { + auto hostport = util::make_hostport(std::begin(hostport_buf), + StringRef{memcachedconf.host}, + memcachedconf.port); + if (resolve_hostname(&memcachedconf.addr, memcachedconf.host.c_str(), + memcachedconf.port, memcachedconf.family) == -1) { + LOG(FATAL) << "Resolving memcached address for TLS ticket key failed: " + << hostport; + return -1; + } + LOG(NOTICE) << "Memcached address for TLS ticket key: " << hostport + << " -> " << util::to_numeric_addr(&memcachedconf.addr); + if (memcachedconf.tls) { + LOG(NOTICE) << "Connection to memcached for TLS ticket key will be " + "encrypted by TLS"; + } + } + } + + if (config->rlimit_nofile) { + struct rlimit lim = {static_cast<rlim_t>(config->rlimit_nofile), + static_cast<rlim_t>(config->rlimit_nofile)}; + if (setrlimit(RLIMIT_NOFILE, &lim) != 0) { + auto error = errno; + LOG(WARN) << "Setting rlimit-nofile failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } + +#ifdef RLIMIT_MEMLOCK + if (config->rlimit_memlock) { + struct rlimit lim = {static_cast<rlim_t>(config->rlimit_memlock), + static_cast<rlim_t>(config->rlimit_memlock)}; + if (setrlimit(RLIMIT_MEMLOCK, &lim) != 0) { + auto error = errno; + LOG(WARN) << "Setting rlimit-memlock failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + } + } +#endif // RLIMIT_MEMLOCK + + auto &fwdconf = config->http.forwarded; + + if (fwdconf.by_node_type == ForwardedNode::OBFUSCATED && + fwdconf.by_obfuscated.empty()) { + // 2 for '_' and terminal NULL + auto iov = make_byte_ref(config->balloc, SHRPX_OBFUSCATED_NODE_LENGTH + 2); + auto p = iov.base; + *p++ = '_'; + auto gen = util::make_mt19937(); + p = util::random_alpha_digit(p, p + SHRPX_OBFUSCATED_NODE_LENGTH, gen); + *p = '\0'; + fwdconf.by_obfuscated = StringRef{iov.base, p}; + } + + if (config->http2.upstream.debug.frame_debug) { + // To make it sync to logging + set_output(stderr); + if (isatty(fileno(stdout))) { + set_color_output(true); + } + reset_timer(); + } + + config->http2.upstream.callbacks = create_http2_upstream_callbacks(); + config->http2.downstream.callbacks = create_http2_downstream_callbacks(); + + if (!config->http.altsvcs.empty()) { + config->http.altsvc_header_value = + http::create_altsvc_header_value(config->balloc, config->http.altsvcs); + } + + if (!config->http.http2_altsvcs.empty()) { + config->http.http2_altsvc_header_value = http::create_altsvc_header_value( + config->balloc, config->http.http2_altsvcs); + } + + return 0; +} +} // namespace + +namespace { +// Closes file descriptor which are opened for listeners in config, +// and are not inherited from |iaddrs|. +void close_not_inherited_fd(Config *config, + const std::vector<InheritedAddr> &iaddrs) { + auto &listenerconf = config->conn.listener; + + for (auto &addr : listenerconf.addrs) { + auto inherited = std::find_if( + std::begin(iaddrs), std::end(iaddrs), + [&addr](const InheritedAddr &iaddr) { return addr.fd == iaddr.fd; }); + + if (inherited != std::end(iaddrs)) { + continue; + } + + close(addr.fd); + } +} +} // namespace + +namespace { +void reload_config() { + int rv; + + LOG(NOTICE) << "Reloading configuration"; + + auto cur_config = mod_config(); + auto new_config = std::make_unique<Config>(); + + fill_default_config(new_config.get()); + + new_config->conf_path = + make_string_ref(new_config->balloc, cur_config->conf_path); + // daemon option is ignored here. + new_config->daemon = cur_config->daemon; + // loop is reused, and ev_loop_flags gets ignored + new_config->ev_loop_flags = cur_config->ev_loop_flags; + new_config->config_revision = cur_config->config_revision + 1; + + rv = process_options(new_config.get(), suconfig.cmdcfgs); + if (rv != 0) { + LOG(ERROR) << "Failed to process new configuration"; + return; + } + + auto iaddrs = get_inherited_addr_from_config(new_config->balloc, cur_config); + + if (create_acceptor_socket(new_config.get(), iaddrs) != 0) { + close_not_inherited_fd(new_config.get(), iaddrs); + return; + } + + // According to libev documentation, flags are ignored since we have + // already created first default loop. + auto loop = ev_default_loop(new_config->ev_loop_flags); + + int ipc_fd = 0; +#ifdef ENABLE_HTTP3 + int quic_ipc_fd = 0; + + auto quic_lwps = collect_quic_lingering_worker_processes(); + + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes; + + if (generate_cid_prefix(cid_prefixes, new_config.get()) != 0) { + close_not_inherited_fd(new_config.get(), iaddrs); + return; + } +#endif // ENABLE_HTTP3 + + // fork_worker_process and forked child process assumes new + // configuration can be obtained from get_config(). + + auto old_config = replace_config(std::move(new_config)); + + auto pid = fork_worker_process(ipc_fd +#ifdef ENABLE_HTTP3 + , + quic_ipc_fd +#endif // ENABLE_HTTP3 + + , + iaddrs +#ifdef ENABLE_HTTP3 + , + cid_prefixes, quic_lwps +#endif // ENABLE_HTTP3 + ); + + if (pid == -1) { + LOG(ERROR) << "Failed to process new configuration"; + + new_config = replace_config(std::move(old_config)); + close_not_inherited_fd(new_config.get(), iaddrs); + + return; + } + + close_unused_inherited_addr(iaddrs); + + worker_process_add(std::make_unique<WorkerProcess>(loop, pid, ipc_fd +#ifdef ENABLE_HTTP3 + , + quic_ipc_fd, cid_prefixes +#endif // ENABLE_HTTP3 + )); + + worker_process_adjust_limit(); + + if (!get_config()->pid_file.empty()) { + save_pid(); + } +} +} // namespace + +int main(int argc, char **argv) { + int rv; + std::array<char, STRERROR_BUFSIZE> errbuf; + +#ifdef HAVE_LIBBPF + libbpf_set_strict_mode(LIBBPF_STRICT_ALL); +#endif // HAVE_LIBBPF + + Log::set_severity_level(NOTICE); + create_config(); + fill_default_config(mod_config()); + + // make copy of stderr + store_original_fds(); + + // First open log files with default configuration, so that we can + // log errors/warnings while reading configuration files. + reopen_log_files(get_config()->logging); + + suconfig.original_argv = argv; + + // We have to copy argv, since getopt_long may change its content. + suconfig.argc = argc; + suconfig.argv = new char *[argc]; + + for (int i = 0; i < argc; ++i) { + suconfig.argv[i] = strdup(argv[i]); + if (suconfig.argv[i] == nullptr) { + auto error = errno; + LOG(FATAL) << "failed to copy argv: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + exit(EXIT_FAILURE); + } + } + + suconfig.cwd = getcwd(nullptr, 0); + if (suconfig.cwd == nullptr) { + auto error = errno; + LOG(FATAL) << "failed to get current working directory: errno=" << error; + exit(EXIT_FAILURE); + } + + auto &cmdcfgs = suconfig.cmdcfgs; + + while (1) { + static int flag = 0; + static constexpr option long_options[] = { + {SHRPX_OPT_DAEMON.c_str(), no_argument, nullptr, 'D'}, + {SHRPX_OPT_LOG_LEVEL.c_str(), required_argument, nullptr, 'L'}, + {SHRPX_OPT_BACKEND.c_str(), required_argument, nullptr, 'b'}, + {SHRPX_OPT_HTTP2_MAX_CONCURRENT_STREAMS.c_str(), required_argument, + nullptr, 'c'}, + {SHRPX_OPT_FRONTEND.c_str(), required_argument, nullptr, 'f'}, + {"help", no_argument, nullptr, 'h'}, + {SHRPX_OPT_INSECURE.c_str(), no_argument, nullptr, 'k'}, + {SHRPX_OPT_WORKERS.c_str(), required_argument, nullptr, 'n'}, + {SHRPX_OPT_CLIENT_PROXY.c_str(), no_argument, nullptr, 'p'}, + {SHRPX_OPT_HTTP2_PROXY.c_str(), no_argument, nullptr, 's'}, + {"version", no_argument, nullptr, 'v'}, + {SHRPX_OPT_FRONTEND_FRAME_DEBUG.c_str(), no_argument, nullptr, 'o'}, + {SHRPX_OPT_ADD_X_FORWARDED_FOR.c_str(), no_argument, &flag, 1}, + {SHRPX_OPT_FRONTEND_HTTP2_READ_TIMEOUT.c_str(), required_argument, + &flag, 2}, + {SHRPX_OPT_FRONTEND_READ_TIMEOUT.c_str(), required_argument, &flag, 3}, + {SHRPX_OPT_FRONTEND_WRITE_TIMEOUT.c_str(), required_argument, &flag, 4}, + {SHRPX_OPT_BACKEND_READ_TIMEOUT.c_str(), required_argument, &flag, 5}, + {SHRPX_OPT_BACKEND_WRITE_TIMEOUT.c_str(), required_argument, &flag, 6}, + {SHRPX_OPT_ACCESSLOG_FILE.c_str(), required_argument, &flag, 7}, + {SHRPX_OPT_BACKEND_KEEP_ALIVE_TIMEOUT.c_str(), required_argument, &flag, + 8}, + {SHRPX_OPT_FRONTEND_HTTP2_WINDOW_BITS.c_str(), required_argument, &flag, + 9}, + {SHRPX_OPT_PID_FILE.c_str(), required_argument, &flag, 10}, + {SHRPX_OPT_USER.c_str(), required_argument, &flag, 11}, + {"conf", required_argument, &flag, 12}, + {SHRPX_OPT_SYSLOG_FACILITY.c_str(), required_argument, &flag, 14}, + {SHRPX_OPT_BACKLOG.c_str(), required_argument, &flag, 15}, + {SHRPX_OPT_CIPHERS.c_str(), required_argument, &flag, 16}, + {SHRPX_OPT_CLIENT.c_str(), no_argument, &flag, 17}, + {SHRPX_OPT_BACKEND_HTTP2_WINDOW_BITS.c_str(), required_argument, &flag, + 18}, + {SHRPX_OPT_CACERT.c_str(), required_argument, &flag, 19}, + {SHRPX_OPT_BACKEND_IPV4.c_str(), no_argument, &flag, 20}, + {SHRPX_OPT_BACKEND_IPV6.c_str(), no_argument, &flag, 21}, + {SHRPX_OPT_PRIVATE_KEY_PASSWD_FILE.c_str(), required_argument, &flag, + 22}, + {SHRPX_OPT_NO_VIA.c_str(), no_argument, &flag, 23}, + {SHRPX_OPT_SUBCERT.c_str(), required_argument, &flag, 24}, + {SHRPX_OPT_HTTP2_BRIDGE.c_str(), no_argument, &flag, 25}, + {SHRPX_OPT_BACKEND_HTTP_PROXY_URI.c_str(), required_argument, &flag, + 26}, + {SHRPX_OPT_BACKEND_NO_TLS.c_str(), no_argument, &flag, 27}, + {SHRPX_OPT_OCSP_STARTUP.c_str(), no_argument, &flag, 28}, + {SHRPX_OPT_FRONTEND_NO_TLS.c_str(), no_argument, &flag, 29}, + {SHRPX_OPT_NO_VERIFY_OCSP.c_str(), no_argument, &flag, 30}, + {SHRPX_OPT_BACKEND_TLS_SNI_FIELD.c_str(), required_argument, &flag, 31}, + {SHRPX_OPT_DH_PARAM_FILE.c_str(), required_argument, &flag, 33}, + {SHRPX_OPT_READ_RATE.c_str(), required_argument, &flag, 34}, + {SHRPX_OPT_READ_BURST.c_str(), required_argument, &flag, 35}, + {SHRPX_OPT_WRITE_RATE.c_str(), required_argument, &flag, 36}, + {SHRPX_OPT_WRITE_BURST.c_str(), required_argument, &flag, 37}, + {SHRPX_OPT_NPN_LIST.c_str(), required_argument, &flag, 38}, + {SHRPX_OPT_VERIFY_CLIENT.c_str(), no_argument, &flag, 39}, + {SHRPX_OPT_VERIFY_CLIENT_CACERT.c_str(), required_argument, &flag, 40}, + {SHRPX_OPT_CLIENT_PRIVATE_KEY_FILE.c_str(), required_argument, &flag, + 41}, + {SHRPX_OPT_CLIENT_CERT_FILE.c_str(), required_argument, &flag, 42}, + {SHRPX_OPT_FRONTEND_HTTP2_DUMP_REQUEST_HEADER.c_str(), + required_argument, &flag, 43}, + {SHRPX_OPT_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER.c_str(), + required_argument, &flag, 44}, + {SHRPX_OPT_HTTP2_NO_COOKIE_CRUMBLING.c_str(), no_argument, &flag, 45}, + {SHRPX_OPT_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS.c_str(), + required_argument, &flag, 46}, + {SHRPX_OPT_BACKEND_HTTP2_CONNECTION_WINDOW_BITS.c_str(), + required_argument, &flag, 47}, + {SHRPX_OPT_TLS_PROTO_LIST.c_str(), required_argument, &flag, 48}, + {SHRPX_OPT_PADDING.c_str(), required_argument, &flag, 49}, + {SHRPX_OPT_WORKER_READ_RATE.c_str(), required_argument, &flag, 50}, + {SHRPX_OPT_WORKER_READ_BURST.c_str(), required_argument, &flag, 51}, + {SHRPX_OPT_WORKER_WRITE_RATE.c_str(), required_argument, &flag, 52}, + {SHRPX_OPT_WORKER_WRITE_BURST.c_str(), required_argument, &flag, 53}, + {SHRPX_OPT_ALTSVC.c_str(), required_argument, &flag, 54}, + {SHRPX_OPT_ADD_RESPONSE_HEADER.c_str(), required_argument, &flag, 55}, + {SHRPX_OPT_WORKER_FRONTEND_CONNECTIONS.c_str(), required_argument, + &flag, 56}, + {SHRPX_OPT_ACCESSLOG_SYSLOG.c_str(), no_argument, &flag, 57}, + {SHRPX_OPT_ERRORLOG_FILE.c_str(), required_argument, &flag, 58}, + {SHRPX_OPT_ERRORLOG_SYSLOG.c_str(), no_argument, &flag, 59}, + {SHRPX_OPT_STREAM_READ_TIMEOUT.c_str(), required_argument, &flag, 60}, + {SHRPX_OPT_STREAM_WRITE_TIMEOUT.c_str(), required_argument, &flag, 61}, + {SHRPX_OPT_NO_LOCATION_REWRITE.c_str(), no_argument, &flag, 62}, + {SHRPX_OPT_BACKEND_HTTP1_CONNECTIONS_PER_HOST.c_str(), + required_argument, &flag, 63}, + {SHRPX_OPT_LISTENER_DISABLE_TIMEOUT.c_str(), required_argument, &flag, + 64}, + {SHRPX_OPT_STRIP_INCOMING_X_FORWARDED_FOR.c_str(), no_argument, &flag, + 65}, + {SHRPX_OPT_ACCESSLOG_FORMAT.c_str(), required_argument, &flag, 66}, + {SHRPX_OPT_BACKEND_HTTP1_CONNECTIONS_PER_FRONTEND.c_str(), + required_argument, &flag, 67}, + {SHRPX_OPT_TLS_TICKET_KEY_FILE.c_str(), required_argument, &flag, 68}, + {SHRPX_OPT_RLIMIT_NOFILE.c_str(), required_argument, &flag, 69}, + {SHRPX_OPT_BACKEND_RESPONSE_BUFFER.c_str(), required_argument, &flag, + 71}, + {SHRPX_OPT_BACKEND_REQUEST_BUFFER.c_str(), required_argument, &flag, + 72}, + {SHRPX_OPT_NO_HOST_REWRITE.c_str(), no_argument, &flag, 73}, + {SHRPX_OPT_NO_SERVER_PUSH.c_str(), no_argument, &flag, 74}, + {SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER.c_str(), + required_argument, &flag, 76}, + {SHRPX_OPT_FETCH_OCSP_RESPONSE_FILE.c_str(), required_argument, &flag, + 77}, + {SHRPX_OPT_OCSP_UPDATE_INTERVAL.c_str(), required_argument, &flag, 78}, + {SHRPX_OPT_NO_OCSP.c_str(), no_argument, &flag, 79}, + {SHRPX_OPT_HEADER_FIELD_BUFFER.c_str(), required_argument, &flag, 80}, + {SHRPX_OPT_MAX_HEADER_FIELDS.c_str(), required_argument, &flag, 81}, + {SHRPX_OPT_ADD_REQUEST_HEADER.c_str(), required_argument, &flag, 82}, + {SHRPX_OPT_INCLUDE.c_str(), required_argument, &flag, 83}, + {SHRPX_OPT_TLS_TICKET_KEY_CIPHER.c_str(), required_argument, &flag, 84}, + {SHRPX_OPT_HOST_REWRITE.c_str(), no_argument, &flag, 85}, + {SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED.c_str(), required_argument, + &flag, 86}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED.c_str(), required_argument, &flag, + 87}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_INTERVAL.c_str(), required_argument, + &flag, 88}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_RETRY.c_str(), + required_argument, &flag, 89}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL.c_str(), required_argument, + &flag, 90}, + {SHRPX_OPT_MRUBY_FILE.c_str(), required_argument, &flag, 91}, + {SHRPX_OPT_ACCEPT_PROXY_PROTOCOL.c_str(), no_argument, &flag, 93}, + {SHRPX_OPT_FASTOPEN.c_str(), required_argument, &flag, 94}, + {SHRPX_OPT_TLS_DYN_REC_WARMUP_THRESHOLD.c_str(), required_argument, + &flag, 95}, + {SHRPX_OPT_TLS_DYN_REC_IDLE_TIMEOUT.c_str(), required_argument, &flag, + 96}, + {SHRPX_OPT_ADD_FORWARDED.c_str(), required_argument, &flag, 97}, + {SHRPX_OPT_STRIP_INCOMING_FORWARDED.c_str(), no_argument, &flag, 98}, + {SHRPX_OPT_FORWARDED_BY.c_str(), required_argument, &flag, 99}, + {SHRPX_OPT_FORWARDED_FOR.c_str(), required_argument, &flag, 100}, + {SHRPX_OPT_RESPONSE_HEADER_FIELD_BUFFER.c_str(), required_argument, + &flag, 101}, + {SHRPX_OPT_MAX_RESPONSE_HEADER_FIELDS.c_str(), required_argument, &flag, + 102}, + {SHRPX_OPT_NO_HTTP2_CIPHER_BLACK_LIST.c_str(), no_argument, &flag, 103}, + {SHRPX_OPT_REQUEST_HEADER_FIELD_BUFFER.c_str(), required_argument, + &flag, 104}, + {SHRPX_OPT_MAX_REQUEST_HEADER_FIELDS.c_str(), required_argument, &flag, + 105}, + {SHRPX_OPT_BACKEND_HTTP1_TLS.c_str(), no_argument, &flag, 106}, + {SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_TLS.c_str(), no_argument, &flag, + 108}, + {SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_CERT_FILE.c_str(), + required_argument, &flag, 109}, + {SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_PRIVATE_KEY_FILE.c_str(), + required_argument, &flag, 110}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_TLS.c_str(), no_argument, &flag, + 111}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_CERT_FILE.c_str(), + required_argument, &flag, 112}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_PRIVATE_KEY_FILE.c_str(), + required_argument, &flag, 113}, + {SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_ADDRESS_FAMILY.c_str(), + required_argument, &flag, 114}, + {SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY.c_str(), + required_argument, &flag, 115}, + {SHRPX_OPT_BACKEND_ADDRESS_FAMILY.c_str(), required_argument, &flag, + 116}, + {SHRPX_OPT_FRONTEND_HTTP2_MAX_CONCURRENT_STREAMS.c_str(), + required_argument, &flag, 117}, + {SHRPX_OPT_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS.c_str(), + required_argument, &flag, 118}, + {SHRPX_OPT_BACKEND_CONNECTIONS_PER_FRONTEND.c_str(), required_argument, + &flag, 119}, + {SHRPX_OPT_BACKEND_TLS.c_str(), no_argument, &flag, 120}, + {SHRPX_OPT_BACKEND_CONNECTIONS_PER_HOST.c_str(), required_argument, + &flag, 121}, + {SHRPX_OPT_ERROR_PAGE.c_str(), required_argument, &flag, 122}, + {SHRPX_OPT_NO_KQUEUE.c_str(), no_argument, &flag, 123}, + {SHRPX_OPT_FRONTEND_HTTP2_SETTINGS_TIMEOUT.c_str(), required_argument, + &flag, 124}, + {SHRPX_OPT_BACKEND_HTTP2_SETTINGS_TIMEOUT.c_str(), required_argument, + &flag, 125}, + {SHRPX_OPT_API_MAX_REQUEST_BODY.c_str(), required_argument, &flag, 126}, + {SHRPX_OPT_BACKEND_MAX_BACKOFF.c_str(), required_argument, &flag, 127}, + {SHRPX_OPT_SERVER_NAME.c_str(), required_argument, &flag, 128}, + {SHRPX_OPT_NO_SERVER_REWRITE.c_str(), no_argument, &flag, 129}, + {SHRPX_OPT_FRONTEND_HTTP2_OPTIMIZE_WRITE_BUFFER_SIZE.c_str(), + no_argument, &flag, 130}, + {SHRPX_OPT_FRONTEND_HTTP2_OPTIMIZE_WINDOW_SIZE.c_str(), no_argument, + &flag, 131}, + {SHRPX_OPT_FRONTEND_HTTP2_WINDOW_SIZE.c_str(), required_argument, &flag, + 132}, + {SHRPX_OPT_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE.c_str(), + required_argument, &flag, 133}, + {SHRPX_OPT_BACKEND_HTTP2_WINDOW_SIZE.c_str(), required_argument, &flag, + 134}, + {SHRPX_OPT_BACKEND_HTTP2_CONNECTION_WINDOW_SIZE.c_str(), + required_argument, &flag, 135}, + {SHRPX_OPT_FRONTEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE.c_str(), + required_argument, &flag, 136}, + {SHRPX_OPT_FRONTEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE.c_str(), + required_argument, &flag, 137}, + {SHRPX_OPT_BACKEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE.c_str(), + required_argument, &flag, 138}, + {SHRPX_OPT_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE.c_str(), + required_argument, &flag, 139}, + {SHRPX_OPT_ECDH_CURVES.c_str(), required_argument, &flag, 140}, + {SHRPX_OPT_TLS_SCT_DIR.c_str(), required_argument, &flag, 141}, + {SHRPX_OPT_BACKEND_CONNECT_TIMEOUT.c_str(), required_argument, &flag, + 142}, + {SHRPX_OPT_DNS_CACHE_TIMEOUT.c_str(), required_argument, &flag, 143}, + {SHRPX_OPT_DNS_LOOKUP_TIMEOUT.c_str(), required_argument, &flag, 144}, + {SHRPX_OPT_DNS_MAX_TRY.c_str(), required_argument, &flag, 145}, + {SHRPX_OPT_FRONTEND_KEEP_ALIVE_TIMEOUT.c_str(), required_argument, + &flag, 146}, + {SHRPX_OPT_PSK_SECRETS.c_str(), required_argument, &flag, 147}, + {SHRPX_OPT_CLIENT_PSK_SECRETS.c_str(), required_argument, &flag, 148}, + {SHRPX_OPT_CLIENT_NO_HTTP2_CIPHER_BLACK_LIST.c_str(), no_argument, + &flag, 149}, + {SHRPX_OPT_CLIENT_CIPHERS.c_str(), required_argument, &flag, 150}, + {SHRPX_OPT_ACCESSLOG_WRITE_EARLY.c_str(), no_argument, &flag, 151}, + {SHRPX_OPT_TLS_MIN_PROTO_VERSION.c_str(), required_argument, &flag, + 152}, + {SHRPX_OPT_TLS_MAX_PROTO_VERSION.c_str(), required_argument, &flag, + 153}, + {SHRPX_OPT_REDIRECT_HTTPS_PORT.c_str(), required_argument, &flag, 154}, + {SHRPX_OPT_FRONTEND_MAX_REQUESTS.c_str(), required_argument, &flag, + 155}, + {SHRPX_OPT_SINGLE_THREAD.c_str(), no_argument, &flag, 156}, + {SHRPX_OPT_NO_ADD_X_FORWARDED_PROTO.c_str(), no_argument, &flag, 157}, + {SHRPX_OPT_NO_STRIP_INCOMING_X_FORWARDED_PROTO.c_str(), no_argument, + &flag, 158}, + {SHRPX_OPT_SINGLE_PROCESS.c_str(), no_argument, &flag, 159}, + {SHRPX_OPT_VERIFY_CLIENT_TOLERATE_EXPIRED.c_str(), no_argument, &flag, + 160}, + {SHRPX_OPT_IGNORE_PER_PATTERN_MRUBY_ERROR.c_str(), no_argument, &flag, + 161}, + {SHRPX_OPT_TLS_NO_POSTPONE_EARLY_DATA.c_str(), no_argument, &flag, 162}, + {SHRPX_OPT_TLS_MAX_EARLY_DATA.c_str(), required_argument, &flag, 163}, + {SHRPX_OPT_TLS13_CIPHERS.c_str(), required_argument, &flag, 164}, + {SHRPX_OPT_TLS13_CLIENT_CIPHERS.c_str(), required_argument, &flag, 165}, + {SHRPX_OPT_NO_STRIP_INCOMING_EARLY_DATA.c_str(), no_argument, &flag, + 166}, + {SHRPX_OPT_NO_HTTP2_CIPHER_BLOCK_LIST.c_str(), no_argument, &flag, 167}, + {SHRPX_OPT_CLIENT_NO_HTTP2_CIPHER_BLOCK_LIST.c_str(), no_argument, + &flag, 168}, + {SHRPX_OPT_QUIC_BPF_PROGRAM_FILE.c_str(), required_argument, &flag, + 169}, + {SHRPX_OPT_NO_QUIC_BPF.c_str(), no_argument, &flag, 170}, + {SHRPX_OPT_HTTP2_ALTSVC.c_str(), required_argument, &flag, 171}, + {SHRPX_OPT_FRONTEND_HTTP3_READ_TIMEOUT.c_str(), required_argument, + &flag, 172}, + {SHRPX_OPT_FRONTEND_QUIC_IDLE_TIMEOUT.c_str(), required_argument, &flag, + 173}, + {SHRPX_OPT_FRONTEND_QUIC_DEBUG_LOG.c_str(), no_argument, &flag, 174}, + {SHRPX_OPT_FRONTEND_HTTP3_WINDOW_SIZE.c_str(), required_argument, &flag, + 175}, + {SHRPX_OPT_FRONTEND_HTTP3_CONNECTION_WINDOW_SIZE.c_str(), + required_argument, &flag, 176}, + {SHRPX_OPT_FRONTEND_HTTP3_MAX_WINDOW_SIZE.c_str(), required_argument, + &flag, 177}, + {SHRPX_OPT_FRONTEND_HTTP3_MAX_CONNECTION_WINDOW_SIZE.c_str(), + required_argument, &flag, 178}, + {SHRPX_OPT_FRONTEND_HTTP3_MAX_CONCURRENT_STREAMS.c_str(), + required_argument, &flag, 179}, + {SHRPX_OPT_FRONTEND_QUIC_EARLY_DATA.c_str(), no_argument, &flag, 180}, + {SHRPX_OPT_FRONTEND_QUIC_QLOG_DIR.c_str(), required_argument, &flag, + 181}, + {SHRPX_OPT_FRONTEND_QUIC_REQUIRE_TOKEN.c_str(), no_argument, &flag, + 182}, + {SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER.c_str(), + required_argument, &flag, 183}, + {SHRPX_OPT_QUIC_SERVER_ID.c_str(), required_argument, &flag, 185}, + {SHRPX_OPT_FRONTEND_QUIC_SECRET_FILE.c_str(), required_argument, &flag, + 186}, + {SHRPX_OPT_RLIMIT_MEMLOCK.c_str(), required_argument, &flag, 187}, + {SHRPX_OPT_MAX_WORKER_PROCESSES.c_str(), required_argument, &flag, 188}, + {SHRPX_OPT_WORKER_PROCESS_GRACE_SHUTDOWN_PERIOD.c_str(), + required_argument, &flag, 189}, + {SHRPX_OPT_FRONTEND_QUIC_INITIAL_RTT.c_str(), required_argument, &flag, + 190}, + {SHRPX_OPT_REQUIRE_HTTP_SCHEME.c_str(), no_argument, &flag, 191}, + {SHRPX_OPT_TLS_KTLS.c_str(), no_argument, &flag, 192}, + {SHRPX_OPT_ALPN_LIST.c_str(), required_argument, &flag, 193}, + {nullptr, 0, nullptr, 0}}; + + int option_index = 0; + int c = getopt_long(argc, argv, "DL:b:c:f:hkn:opsv", long_options, + &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'D': + cmdcfgs.emplace_back(SHRPX_OPT_DAEMON, StringRef::from_lit("yes")); + break; + case 'L': + cmdcfgs.emplace_back(SHRPX_OPT_LOG_LEVEL, StringRef{optarg}); + break; + case 'b': + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND, StringRef{optarg}); + break; + case 'c': + cmdcfgs.emplace_back(SHRPX_OPT_HTTP2_MAX_CONCURRENT_STREAMS, + StringRef{optarg}); + break; + case 'f': + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND, StringRef{optarg}); + break; + case 'h': + print_help(std::cout); + exit(EXIT_SUCCESS); + case 'k': + cmdcfgs.emplace_back(SHRPX_OPT_INSECURE, StringRef::from_lit("yes")); + break; + case 'n': + cmdcfgs.emplace_back(SHRPX_OPT_WORKERS, StringRef{optarg}); + break; + case 'o': + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_FRAME_DEBUG, + StringRef::from_lit("yes")); + break; + case 'p': + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_PROXY, StringRef::from_lit("yes")); + break; + case 's': + cmdcfgs.emplace_back(SHRPX_OPT_HTTP2_PROXY, StringRef::from_lit("yes")); + break; + case 'v': + print_version(std::cout); + exit(EXIT_SUCCESS); + case '?': + util::show_candidates(argv[optind - 1], long_options); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // --add-x-forwarded-for + cmdcfgs.emplace_back(SHRPX_OPT_ADD_X_FORWARDED_FOR, + StringRef::from_lit("yes")); + break; + case 2: + // --frontend-http2-read-timeout + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_READ_TIMEOUT, + StringRef{optarg}); + break; + case 3: + // --frontend-read-timeout + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_READ_TIMEOUT, + StringRef{optarg}); + break; + case 4: + // --frontend-write-timeout + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_WRITE_TIMEOUT, + StringRef{optarg}); + break; + case 5: + // --backend-read-timeout + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_READ_TIMEOUT, StringRef{optarg}); + break; + case 6: + // --backend-write-timeout + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_WRITE_TIMEOUT, + StringRef{optarg}); + break; + case 7: + cmdcfgs.emplace_back(SHRPX_OPT_ACCESSLOG_FILE, StringRef{optarg}); + break; + case 8: + // --backend-keep-alive-timeout + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_KEEP_ALIVE_TIMEOUT, + StringRef{optarg}); + break; + case 9: + // --frontend-http2-window-bits + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_WINDOW_BITS, + StringRef{optarg}); + break; + case 10: + cmdcfgs.emplace_back(SHRPX_OPT_PID_FILE, StringRef{optarg}); + break; + case 11: + cmdcfgs.emplace_back(SHRPX_OPT_USER, StringRef{optarg}); + break; + case 12: + // --conf + mod_config()->conf_path = + make_string_ref(mod_config()->balloc, StringRef{optarg}); + break; + case 14: + // --syslog-facility + cmdcfgs.emplace_back(SHRPX_OPT_SYSLOG_FACILITY, StringRef{optarg}); + break; + case 15: + // --backlog + cmdcfgs.emplace_back(SHRPX_OPT_BACKLOG, StringRef{optarg}); + break; + case 16: + // --ciphers + cmdcfgs.emplace_back(SHRPX_OPT_CIPHERS, StringRef{optarg}); + break; + case 17: + // --client + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT, StringRef::from_lit("yes")); + break; + case 18: + // --backend-http2-window-bits + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_WINDOW_BITS, + StringRef{optarg}); + break; + case 19: + // --cacert + cmdcfgs.emplace_back(SHRPX_OPT_CACERT, StringRef{optarg}); + break; + case 20: + // --backend-ipv4 + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_IPV4, + StringRef::from_lit("yes")); + break; + case 21: + // --backend-ipv6 + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_IPV6, + StringRef::from_lit("yes")); + break; + case 22: + // --private-key-passwd-file + cmdcfgs.emplace_back(SHRPX_OPT_PRIVATE_KEY_PASSWD_FILE, + StringRef{optarg}); + break; + case 23: + // --no-via + cmdcfgs.emplace_back(SHRPX_OPT_NO_VIA, StringRef::from_lit("yes")); + break; + case 24: + // --subcert + cmdcfgs.emplace_back(SHRPX_OPT_SUBCERT, StringRef{optarg}); + break; + case 25: + // --http2-bridge + cmdcfgs.emplace_back(SHRPX_OPT_HTTP2_BRIDGE, + StringRef::from_lit("yes")); + break; + case 26: + // --backend-http-proxy-uri + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP_PROXY_URI, + StringRef{optarg}); + break; + case 27: + // --backend-no-tls + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_NO_TLS, + StringRef::from_lit("yes")); + break; + case 28: + // --ocsp-startup + cmdcfgs.emplace_back(SHRPX_OPT_OCSP_STARTUP, + StringRef::from_lit("yes")); + break; + case 29: + // --frontend-no-tls + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_NO_TLS, + StringRef::from_lit("yes")); + break; + case 30: + // --no-verify-ocsp + cmdcfgs.emplace_back(SHRPX_OPT_NO_VERIFY_OCSP, + StringRef::from_lit("yes")); + break; + case 31: + // --backend-tls-sni-field + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_TLS_SNI_FIELD, + StringRef{optarg}); + break; + case 33: + // --dh-param-file + cmdcfgs.emplace_back(SHRPX_OPT_DH_PARAM_FILE, StringRef{optarg}); + break; + case 34: + // --read-rate + cmdcfgs.emplace_back(SHRPX_OPT_READ_RATE, StringRef{optarg}); + break; + case 35: + // --read-burst + cmdcfgs.emplace_back(SHRPX_OPT_READ_BURST, StringRef{optarg}); + break; + case 36: + // --write-rate + cmdcfgs.emplace_back(SHRPX_OPT_WRITE_RATE, StringRef{optarg}); + break; + case 37: + // --write-burst + cmdcfgs.emplace_back(SHRPX_OPT_WRITE_BURST, StringRef{optarg}); + break; + case 38: + // --npn-list + cmdcfgs.emplace_back(SHRPX_OPT_NPN_LIST, StringRef{optarg}); + break; + case 39: + // --verify-client + cmdcfgs.emplace_back(SHRPX_OPT_VERIFY_CLIENT, + StringRef::from_lit("yes")); + break; + case 40: + // --verify-client-cacert + cmdcfgs.emplace_back(SHRPX_OPT_VERIFY_CLIENT_CACERT, StringRef{optarg}); + break; + case 41: + // --client-private-key-file + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_PRIVATE_KEY_FILE, + StringRef{optarg}); + break; + case 42: + // --client-cert-file + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_CERT_FILE, StringRef{optarg}); + break; + case 43: + // --frontend-http2-dump-request-header + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_DUMP_REQUEST_HEADER, + StringRef{optarg}); + break; + case 44: + // --frontend-http2-dump-response-header + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER, + StringRef{optarg}); + break; + case 45: + // --http2-no-cookie-crumbling + cmdcfgs.emplace_back(SHRPX_OPT_HTTP2_NO_COOKIE_CRUMBLING, + StringRef::from_lit("yes")); + break; + case 46: + // --frontend-http2-connection-window-bits + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS, + StringRef{optarg}); + break; + case 47: + // --backend-http2-connection-window-bits + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_CONNECTION_WINDOW_BITS, + StringRef{optarg}); + break; + case 48: + // --tls-proto-list + cmdcfgs.emplace_back(SHRPX_OPT_TLS_PROTO_LIST, StringRef{optarg}); + break; + case 49: + // --padding + cmdcfgs.emplace_back(SHRPX_OPT_PADDING, StringRef{optarg}); + break; + case 50: + // --worker-read-rate + cmdcfgs.emplace_back(SHRPX_OPT_WORKER_READ_RATE, StringRef{optarg}); + break; + case 51: + // --worker-read-burst + cmdcfgs.emplace_back(SHRPX_OPT_WORKER_READ_BURST, StringRef{optarg}); + break; + case 52: + // --worker-write-rate + cmdcfgs.emplace_back(SHRPX_OPT_WORKER_WRITE_RATE, StringRef{optarg}); + break; + case 53: + // --worker-write-burst + cmdcfgs.emplace_back(SHRPX_OPT_WORKER_WRITE_BURST, StringRef{optarg}); + break; + case 54: + // --altsvc + cmdcfgs.emplace_back(SHRPX_OPT_ALTSVC, StringRef{optarg}); + break; + case 55: + // --add-response-header + cmdcfgs.emplace_back(SHRPX_OPT_ADD_RESPONSE_HEADER, StringRef{optarg}); + break; + case 56: + // --worker-frontend-connections + cmdcfgs.emplace_back(SHRPX_OPT_WORKER_FRONTEND_CONNECTIONS, + StringRef{optarg}); + break; + case 57: + // --accesslog-syslog + cmdcfgs.emplace_back(SHRPX_OPT_ACCESSLOG_SYSLOG, + StringRef::from_lit("yes")); + break; + case 58: + // --errorlog-file + cmdcfgs.emplace_back(SHRPX_OPT_ERRORLOG_FILE, StringRef{optarg}); + break; + case 59: + // --errorlog-syslog + cmdcfgs.emplace_back(SHRPX_OPT_ERRORLOG_SYSLOG, + StringRef::from_lit("yes")); + break; + case 60: + // --stream-read-timeout + cmdcfgs.emplace_back(SHRPX_OPT_STREAM_READ_TIMEOUT, StringRef{optarg}); + break; + case 61: + // --stream-write-timeout + cmdcfgs.emplace_back(SHRPX_OPT_STREAM_WRITE_TIMEOUT, StringRef{optarg}); + break; + case 62: + // --no-location-rewrite + cmdcfgs.emplace_back(SHRPX_OPT_NO_LOCATION_REWRITE, + StringRef::from_lit("yes")); + break; + case 63: + // --backend-http1-connections-per-host + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP1_CONNECTIONS_PER_HOST, + StringRef{optarg}); + break; + case 64: + // --listener-disable-timeout + cmdcfgs.emplace_back(SHRPX_OPT_LISTENER_DISABLE_TIMEOUT, + StringRef{optarg}); + break; + case 65: + // --strip-incoming-x-forwarded-for + cmdcfgs.emplace_back(SHRPX_OPT_STRIP_INCOMING_X_FORWARDED_FOR, + StringRef::from_lit("yes")); + break; + case 66: + // --accesslog-format + cmdcfgs.emplace_back(SHRPX_OPT_ACCESSLOG_FORMAT, StringRef{optarg}); + break; + case 67: + // --backend-http1-connections-per-frontend + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP1_CONNECTIONS_PER_FRONTEND, + StringRef{optarg}); + break; + case 68: + // --tls-ticket-key-file + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_FILE, StringRef{optarg}); + break; + case 69: + // --rlimit-nofile + cmdcfgs.emplace_back(SHRPX_OPT_RLIMIT_NOFILE, StringRef{optarg}); + break; + case 71: + // --backend-response-buffer + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_RESPONSE_BUFFER, + StringRef{optarg}); + break; + case 72: + // --backend-request-buffer + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_REQUEST_BUFFER, + StringRef{optarg}); + break; + case 73: + // --no-host-rewrite + cmdcfgs.emplace_back(SHRPX_OPT_NO_HOST_REWRITE, + StringRef::from_lit("yes")); + break; + case 74: + // --no-server-push + cmdcfgs.emplace_back(SHRPX_OPT_NO_SERVER_PUSH, + StringRef::from_lit("yes")); + break; + case 76: + // --backend-http2-connections-per-worker + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER, + StringRef{optarg}); + break; + case 77: + // --fetch-ocsp-response-file + cmdcfgs.emplace_back(SHRPX_OPT_FETCH_OCSP_RESPONSE_FILE, + StringRef{optarg}); + break; + case 78: + // --ocsp-update-interval + cmdcfgs.emplace_back(SHRPX_OPT_OCSP_UPDATE_INTERVAL, StringRef{optarg}); + break; + case 79: + // --no-ocsp + cmdcfgs.emplace_back(SHRPX_OPT_NO_OCSP, StringRef::from_lit("yes")); + break; + case 80: + // --header-field-buffer + cmdcfgs.emplace_back(SHRPX_OPT_HEADER_FIELD_BUFFER, StringRef{optarg}); + break; + case 81: + // --max-header-fields + cmdcfgs.emplace_back(SHRPX_OPT_MAX_HEADER_FIELDS, StringRef{optarg}); + break; + case 82: + // --add-request-header + cmdcfgs.emplace_back(SHRPX_OPT_ADD_REQUEST_HEADER, StringRef{optarg}); + break; + case 83: + // --include + cmdcfgs.emplace_back(SHRPX_OPT_INCLUDE, StringRef{optarg}); + break; + case 84: + // --tls-ticket-key-cipher + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_CIPHER, + StringRef{optarg}); + break; + case 85: + // --host-rewrite + cmdcfgs.emplace_back(SHRPX_OPT_HOST_REWRITE, + StringRef::from_lit("yes")); + break; + case 86: + // --tls-session-cache-memcached + cmdcfgs.emplace_back(SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED, + StringRef{optarg}); + break; + case 87: + // --tls-ticket-key-memcached + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED, + StringRef{optarg}); + break; + case 88: + // --tls-ticket-key-memcached-interval + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_INTERVAL, + StringRef{optarg}); + break; + case 89: + // --tls-ticket-key-memcached-max-retry + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_RETRY, + StringRef{optarg}); + break; + case 90: + // --tls-ticket-key-memcached-max-fail + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL, + StringRef{optarg}); + break; + case 91: + // --mruby-file + cmdcfgs.emplace_back(SHRPX_OPT_MRUBY_FILE, StringRef{optarg}); + break; + case 93: + // --accept-proxy-protocol + cmdcfgs.emplace_back(SHRPX_OPT_ACCEPT_PROXY_PROTOCOL, + StringRef::from_lit("yes")); + break; + case 94: + // --fastopen + cmdcfgs.emplace_back(SHRPX_OPT_FASTOPEN, StringRef{optarg}); + break; + case 95: + // --tls-dyn-rec-warmup-threshold + cmdcfgs.emplace_back(SHRPX_OPT_TLS_DYN_REC_WARMUP_THRESHOLD, + StringRef{optarg}); + break; + case 96: + // --tls-dyn-rec-idle-timeout + cmdcfgs.emplace_back(SHRPX_OPT_TLS_DYN_REC_IDLE_TIMEOUT, + StringRef{optarg}); + break; + case 97: + // --add-forwarded + cmdcfgs.emplace_back(SHRPX_OPT_ADD_FORWARDED, StringRef{optarg}); + break; + case 98: + // --strip-incoming-forwarded + cmdcfgs.emplace_back(SHRPX_OPT_STRIP_INCOMING_FORWARDED, + StringRef::from_lit("yes")); + break; + case 99: + // --forwarded-by + cmdcfgs.emplace_back(SHRPX_OPT_FORWARDED_BY, StringRef{optarg}); + break; + case 100: + // --forwarded-for + cmdcfgs.emplace_back(SHRPX_OPT_FORWARDED_FOR, StringRef{optarg}); + break; + case 101: + // --response-header-field-buffer + cmdcfgs.emplace_back(SHRPX_OPT_RESPONSE_HEADER_FIELD_BUFFER, + StringRef{optarg}); + break; + case 102: + // --max-response-header-fields + cmdcfgs.emplace_back(SHRPX_OPT_MAX_RESPONSE_HEADER_FIELDS, + StringRef{optarg}); + break; + case 103: + // --no-http2-cipher-black-list + cmdcfgs.emplace_back(SHRPX_OPT_NO_HTTP2_CIPHER_BLACK_LIST, + StringRef::from_lit("yes")); + break; + case 104: + // --request-header-field-buffer + cmdcfgs.emplace_back(SHRPX_OPT_REQUEST_HEADER_FIELD_BUFFER, + StringRef{optarg}); + break; + case 105: + // --max-request-header-fields + cmdcfgs.emplace_back(SHRPX_OPT_MAX_REQUEST_HEADER_FIELDS, + StringRef{optarg}); + break; + case 106: + // --backend-http1-tls + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP1_TLS, + StringRef::from_lit("yes")); + break; + case 108: + // --tls-session-cache-memcached-tls + cmdcfgs.emplace_back(SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_TLS, + StringRef::from_lit("yes")); + break; + case 109: + // --tls-session-cache-memcached-cert-file + cmdcfgs.emplace_back(SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_CERT_FILE, + StringRef{optarg}); + break; + case 110: + // --tls-session-cache-memcached-private-key-file + cmdcfgs.emplace_back( + SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_PRIVATE_KEY_FILE, + StringRef{optarg}); + break; + case 111: + // --tls-ticket-key-memcached-tls + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_TLS, + StringRef::from_lit("yes")); + break; + case 112: + // --tls-ticket-key-memcached-cert-file + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_CERT_FILE, + StringRef{optarg}); + break; + case 113: + // --tls-ticket-key-memcached-private-key-file + cmdcfgs.emplace_back( + SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_PRIVATE_KEY_FILE, + StringRef{optarg}); + break; + case 114: + // --tls-ticket-key-memcached-address-family + cmdcfgs.emplace_back(SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_ADDRESS_FAMILY, + StringRef{optarg}); + break; + case 115: + // --tls-session-cache-memcached-address-family + cmdcfgs.emplace_back( + SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY, + StringRef{optarg}); + break; + case 116: + // --backend-address-family + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_ADDRESS_FAMILY, + StringRef{optarg}); + break; + case 117: + // --frontend-http2-max-concurrent-streams + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_MAX_CONCURRENT_STREAMS, + StringRef{optarg}); + break; + case 118: + // --backend-http2-max-concurrent-streams + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS, + StringRef{optarg}); + break; + case 119: + // --backend-connections-per-frontend + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_CONNECTIONS_PER_FRONTEND, + StringRef{optarg}); + break; + case 120: + // --backend-tls + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_TLS, StringRef::from_lit("yes")); + break; + case 121: + // --backend-connections-per-host + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_CONNECTIONS_PER_HOST, + StringRef{optarg}); + break; + case 122: + // --error-page + cmdcfgs.emplace_back(SHRPX_OPT_ERROR_PAGE, StringRef{optarg}); + break; + case 123: + // --no-kqueue + cmdcfgs.emplace_back(SHRPX_OPT_NO_KQUEUE, StringRef::from_lit("yes")); + break; + case 124: + // --frontend-http2-settings-timeout + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_SETTINGS_TIMEOUT, + StringRef{optarg}); + break; + case 125: + // --backend-http2-settings-timeout + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_SETTINGS_TIMEOUT, + StringRef{optarg}); + break; + case 126: + // --api-max-request-body + cmdcfgs.emplace_back(SHRPX_OPT_API_MAX_REQUEST_BODY, StringRef{optarg}); + break; + case 127: + // --backend-max-backoff + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_MAX_BACKOFF, StringRef{optarg}); + break; + case 128: + // --server-name + cmdcfgs.emplace_back(SHRPX_OPT_SERVER_NAME, StringRef{optarg}); + break; + case 129: + // --no-server-rewrite + cmdcfgs.emplace_back(SHRPX_OPT_NO_SERVER_REWRITE, + StringRef::from_lit("yes")); + break; + case 130: + // --frontend-http2-optimize-write-buffer-size + cmdcfgs.emplace_back( + SHRPX_OPT_FRONTEND_HTTP2_OPTIMIZE_WRITE_BUFFER_SIZE, + StringRef::from_lit("yes")); + break; + case 131: + // --frontend-http2-optimize-window-size + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_OPTIMIZE_WINDOW_SIZE, + StringRef::from_lit("yes")); + break; + case 132: + // --frontend-http2-window-size + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_WINDOW_SIZE, + StringRef{optarg}); + break; + case 133: + // --frontend-http2-connection-window-size + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE, + StringRef{optarg}); + break; + case 134: + // --backend-http2-window-size + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_WINDOW_SIZE, + StringRef{optarg}); + break; + case 135: + // --backend-http2-connection-window-size + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_CONNECTION_WINDOW_SIZE, + StringRef{optarg}); + break; + case 136: + // --frontend-http2-encoder-dynamic-table-size + cmdcfgs.emplace_back( + SHRPX_OPT_FRONTEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE, + StringRef{optarg}); + break; + case 137: + // --frontend-http2-decoder-dynamic-table-size + cmdcfgs.emplace_back( + SHRPX_OPT_FRONTEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE, + StringRef{optarg}); + break; + case 138: + // --backend-http2-encoder-dynamic-table-size + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE, + StringRef{optarg}); + break; + case 139: + // --backend-http2-decoder-dynamic-table-size + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE, + StringRef{optarg}); + break; + case 140: + // --ecdh-curves + cmdcfgs.emplace_back(SHRPX_OPT_ECDH_CURVES, StringRef{optarg}); + break; + case 141: + // --tls-sct-dir + cmdcfgs.emplace_back(SHRPX_OPT_TLS_SCT_DIR, StringRef{optarg}); + break; + case 142: + // --backend-connect-timeout + cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_CONNECT_TIMEOUT, + StringRef{optarg}); + break; + case 143: + // --dns-cache-timeout + cmdcfgs.emplace_back(SHRPX_OPT_DNS_CACHE_TIMEOUT, StringRef{optarg}); + break; + case 144: + // --dns-lookup-timeou + cmdcfgs.emplace_back(SHRPX_OPT_DNS_LOOKUP_TIMEOUT, StringRef{optarg}); + break; + case 145: + // --dns-max-try + cmdcfgs.emplace_back(SHRPX_OPT_DNS_MAX_TRY, StringRef{optarg}); + break; + case 146: + // --frontend-keep-alive-timeout + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_KEEP_ALIVE_TIMEOUT, + StringRef{optarg}); + break; + case 147: + // --psk-secrets + cmdcfgs.emplace_back(SHRPX_OPT_PSK_SECRETS, StringRef{optarg}); + break; + case 148: + // --client-psk-secrets + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_PSK_SECRETS, StringRef{optarg}); + break; + case 149: + // --client-no-http2-cipher-black-list + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_NO_HTTP2_CIPHER_BLACK_LIST, + StringRef::from_lit("yes")); + break; + case 150: + // --client-ciphers + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_CIPHERS, StringRef{optarg}); + break; + case 151: + // --accesslog-write-early + cmdcfgs.emplace_back(SHRPX_OPT_ACCESSLOG_WRITE_EARLY, + StringRef::from_lit("yes")); + break; + case 152: + // --tls-min-proto-version + cmdcfgs.emplace_back(SHRPX_OPT_TLS_MIN_PROTO_VERSION, + StringRef{optarg}); + break; + case 153: + // --tls-max-proto-version + cmdcfgs.emplace_back(SHRPX_OPT_TLS_MAX_PROTO_VERSION, + StringRef{optarg}); + break; + case 154: + // --redirect-https-port + cmdcfgs.emplace_back(SHRPX_OPT_REDIRECT_HTTPS_PORT, StringRef{optarg}); + break; + case 155: + // --frontend-max-requests + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_MAX_REQUESTS, + StringRef{optarg}); + break; + case 156: + // --single-thread + cmdcfgs.emplace_back(SHRPX_OPT_SINGLE_THREAD, + StringRef::from_lit("yes")); + break; + case 157: + // --no-add-x-forwarded-proto + cmdcfgs.emplace_back(SHRPX_OPT_NO_ADD_X_FORWARDED_PROTO, + StringRef::from_lit("yes")); + break; + case 158: + // --no-strip-incoming-x-forwarded-proto + cmdcfgs.emplace_back(SHRPX_OPT_NO_STRIP_INCOMING_X_FORWARDED_PROTO, + StringRef::from_lit("yes")); + break; + case 159: + // --single-process + cmdcfgs.emplace_back(SHRPX_OPT_SINGLE_PROCESS, + StringRef::from_lit("yes")); + break; + case 160: + // --verify-client-tolerate-expired + cmdcfgs.emplace_back(SHRPX_OPT_VERIFY_CLIENT_TOLERATE_EXPIRED, + StringRef::from_lit("yes")); + break; + case 161: + // --ignore-per-pattern-mruby-error + cmdcfgs.emplace_back(SHRPX_OPT_IGNORE_PER_PATTERN_MRUBY_ERROR, + StringRef::from_lit("yes")); + break; + case 162: + // --tls-no-postpone-early-data + cmdcfgs.emplace_back(SHRPX_OPT_TLS_NO_POSTPONE_EARLY_DATA, + StringRef::from_lit("yes")); + break; + case 163: + // --tls-max-early-data + cmdcfgs.emplace_back(SHRPX_OPT_TLS_MAX_EARLY_DATA, StringRef{optarg}); + break; + case 164: + // --tls13-ciphers + cmdcfgs.emplace_back(SHRPX_OPT_TLS13_CIPHERS, StringRef{optarg}); + break; + case 165: + // --tls13-client-ciphers + cmdcfgs.emplace_back(SHRPX_OPT_TLS13_CLIENT_CIPHERS, StringRef{optarg}); + break; + case 166: + // --no-strip-incoming-early-data + cmdcfgs.emplace_back(SHRPX_OPT_NO_STRIP_INCOMING_EARLY_DATA, + StringRef::from_lit("yes")); + break; + case 167: + // --no-http2-cipher-block-list + cmdcfgs.emplace_back(SHRPX_OPT_NO_HTTP2_CIPHER_BLOCK_LIST, + StringRef::from_lit("yes")); + break; + case 168: + // --client-no-http2-cipher-block-list + cmdcfgs.emplace_back(SHRPX_OPT_CLIENT_NO_HTTP2_CIPHER_BLOCK_LIST, + StringRef::from_lit("yes")); + break; + case 169: + // --quic-bpf-program-file + cmdcfgs.emplace_back(SHRPX_OPT_QUIC_BPF_PROGRAM_FILE, + StringRef{optarg}); + break; + case 170: + // --no-quic-bpf + cmdcfgs.emplace_back(SHRPX_OPT_NO_QUIC_BPF, StringRef::from_lit("yes")); + break; + case 171: + // --http2-altsvc + cmdcfgs.emplace_back(SHRPX_OPT_HTTP2_ALTSVC, StringRef{optarg}); + break; + case 172: + // --frontend-http3-read-timeout + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP3_READ_TIMEOUT, + StringRef{optarg}); + break; + case 173: + // --frontend-quic-idle-timeout + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_IDLE_TIMEOUT, + StringRef{optarg}); + break; + case 174: + // --frontend-quic-debug-log + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_DEBUG_LOG, + StringRef::from_lit("yes")); + break; + case 175: + // --frontend-http3-window-size + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP3_WINDOW_SIZE, + StringRef{optarg}); + break; + case 176: + // --frontend-http3-connection-window-size + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP3_CONNECTION_WINDOW_SIZE, + StringRef{optarg}); + break; + case 177: + // --frontend-http3-max-window-size + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP3_MAX_WINDOW_SIZE, + StringRef{optarg}); + break; + case 178: + // --frontend-http3-max-connection-window-size + cmdcfgs.emplace_back( + SHRPX_OPT_FRONTEND_HTTP3_MAX_CONNECTION_WINDOW_SIZE, + StringRef{optarg}); + break; + case 179: + // --frontend-http3-max-concurrent-streams + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_HTTP3_MAX_CONCURRENT_STREAMS, + StringRef{optarg}); + break; + case 180: + // --frontend-quic-early-data + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_EARLY_DATA, + StringRef::from_lit("yes")); + break; + case 181: + // --frontend-quic-qlog-dir + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_QLOG_DIR, + StringRef{optarg}); + break; + case 182: + // --frontend-quic-require-token + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_REQUIRE_TOKEN, + StringRef::from_lit("yes")); + break; + case 183: + // --frontend-quic-congestion-controller + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER, + StringRef{optarg}); + break; + case 185: + // --quic-server-id + cmdcfgs.emplace_back(SHRPX_OPT_QUIC_SERVER_ID, StringRef{optarg}); + break; + case 186: + // --frontend-quic-secret-file + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_SECRET_FILE, + StringRef{optarg}); + break; + case 187: + // --rlimit-memlock + cmdcfgs.emplace_back(SHRPX_OPT_RLIMIT_MEMLOCK, StringRef{optarg}); + break; + case 188: + // --max-worker-processes + cmdcfgs.emplace_back(SHRPX_OPT_MAX_WORKER_PROCESSES, StringRef{optarg}); + break; + case 189: + // --worker-process-grace-shutdown-period + cmdcfgs.emplace_back(SHRPX_OPT_WORKER_PROCESS_GRACE_SHUTDOWN_PERIOD, + StringRef{optarg}); + break; + case 190: + // --frontend-quic-initial-rtt + cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_INITIAL_RTT, + StringRef{optarg}); + break; + case 191: + // --require-http-scheme + cmdcfgs.emplace_back(SHRPX_OPT_REQUIRE_HTTP_SCHEME, + StringRef::from_lit("yes")); + break; + case 192: + // --tls-ktls + cmdcfgs.emplace_back(SHRPX_OPT_TLS_KTLS, StringRef::from_lit("yes")); + break; + case 193: + // --alpn-list + cmdcfgs.emplace_back(SHRPX_OPT_ALPN_LIST, StringRef{optarg}); + break; + default: + break; + } + break; + default: + break; + } + } + + if (argc - optind >= 2) { + cmdcfgs.emplace_back(SHRPX_OPT_PRIVATE_KEY_FILE, StringRef{argv[optind++]}); + cmdcfgs.emplace_back(SHRPX_OPT_CERTIFICATE_FILE, StringRef{argv[optind++]}); + } + + rv = process_options(mod_config(), cmdcfgs); + if (rv != 0) { + return -1; + } + + if (event_loop() != 0) { + return -1; + } + + LOG(NOTICE) << "Shutdown momentarily"; + + delete_log_config(); + + return 0; +} + +} // namespace shrpx + +int main(int argc, char **argv) { return run_app(shrpx::main, argc, argv); } diff --git a/src/shrpx.h b/src/shrpx.h new file mode 100644 index 0000000..d881ef5 --- /dev/null +++ b/src/shrpx.h @@ -0,0 +1,58 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_H +#define SHRPX_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H + +#include <cassert> + +#ifndef HAVE__EXIT +# define nghttp2_Exit(status) _exit(status) +#else // HAVE__EXIT +# define nghttp2_Exit(status) _Exit(status) +#endif // HAVE__EXIT + +#define DIE() nghttp2_Exit(EXIT_FAILURE) + +#if defined(HAVE_DECL_INITGROUPS) && !HAVE_DECL_INITGROUPS +inline int initgroups(const char *user, gid_t group) { return 0; } +#endif // defined(HAVE_DECL_INITGROUPS) && !HAVE_DECL_INITGROUPS + +#ifndef HAVE_BPF_STATS_TYPE +/* Newer kernel should have this defined in linux/bpf.h */ +enum bpf_stats_type { + BPF_STATS_RUN_TIME = 0, +}; +#endif // !HAVE_BPF_STATS_TYPE + +#endif // SHRPX_H diff --git a/src/shrpx_accept_handler.cc b/src/shrpx_accept_handler.cc new file mode 100644 index 0000000..01b6415 --- /dev/null +++ b/src/shrpx_accept_handler.cc @@ -0,0 +1,111 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_accept_handler.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H + +#include <cerrno> + +#include "shrpx_connection_handler.h" +#include "shrpx_config.h" +#include "shrpx_log.h" +#include "util.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +void acceptcb(struct ev_loop *loop, ev_io *w, int revent) { + auto h = static_cast<AcceptHandler *>(w->data); + h->accept_connection(); +} +} // namespace + +AcceptHandler::AcceptHandler(const UpstreamAddr *faddr, ConnectionHandler *h) + : conn_hnr_(h), faddr_(faddr) { + ev_io_init(&wev_, acceptcb, faddr_->fd, EV_READ); + wev_.data = this; + ev_io_start(conn_hnr_->get_loop(), &wev_); +} + +AcceptHandler::~AcceptHandler() { + ev_io_stop(conn_hnr_->get_loop(), &wev_); + close(faddr_->fd); +} + +void AcceptHandler::accept_connection() { + sockaddr_union sockaddr; + socklen_t addrlen = sizeof(sockaddr); + +#ifdef HAVE_ACCEPT4 + auto cfd = + accept4(faddr_->fd, &sockaddr.sa, &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC); +#else // !HAVE_ACCEPT4 + auto cfd = accept(faddr_->fd, &sockaddr.sa, &addrlen); +#endif // !HAVE_ACCEPT4 + + if (cfd == -1) { + switch (errno) { + case EINTR: + case ENETDOWN: + case EPROTO: + case ENOPROTOOPT: + case EHOSTDOWN: +#ifdef ENONET + case ENONET: +#endif // ENONET + case EHOSTUNREACH: + case EOPNOTSUPP: + case ENETUNREACH: + return; + case EMFILE: + case ENFILE: + LOG(WARN) << "acceptor: running out file descriptor; disable acceptor " + "temporarily"; + conn_hnr_->sleep_acceptor(get_config()->conn.listener.timeout.sleep); + return; + default: + return; + } + } + +#ifndef HAVE_ACCEPT4 + util::make_socket_nonblocking(cfd); + util::make_socket_closeonexec(cfd); +#endif // !HAVE_ACCEPT4 + + conn_hnr_->handle_connection(cfd, &sockaddr.sa, addrlen, faddr_); +} + +void AcceptHandler::enable() { ev_io_start(conn_hnr_->get_loop(), &wev_); } + +void AcceptHandler::disable() { ev_io_stop(conn_hnr_->get_loop(), &wev_); } + +int AcceptHandler::get_fd() const { return faddr_->fd; } + +} // namespace shrpx diff --git a/src/shrpx_accept_handler.h b/src/shrpx_accept_handler.h new file mode 100644 index 0000000..853e9a2 --- /dev/null +++ b/src/shrpx_accept_handler.h @@ -0,0 +1,54 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_ACCEPT_HANDLER_H +#define SHRPX_ACCEPT_HANDLER_H + +#include "shrpx.h" + +#include <ev.h> + +namespace shrpx { + +class ConnectionHandler; +struct UpstreamAddr; + +class AcceptHandler { +public: + AcceptHandler(const UpstreamAddr *faddr, ConnectionHandler *h); + ~AcceptHandler(); + void accept_connection(); + void enable(); + void disable(); + int get_fd() const; + +private: + ev_io wev_; + ConnectionHandler *conn_hnr_; + const UpstreamAddr *faddr_; +}; + +} // namespace shrpx + +#endif // SHRPX_ACCEPT_HANDLER_H diff --git a/src/shrpx_api_downstream_connection.cc b/src/shrpx_api_downstream_connection.cc new file mode 100644 index 0000000..254ab59 --- /dev/null +++ b/src/shrpx_api_downstream_connection.cc @@ -0,0 +1,478 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_api_downstream_connection.h" + +#include <sys/mman.h> +#include <fcntl.h> +#include <unistd.h> +#include <cstdlib> + +#include "shrpx_client_handler.h" +#include "shrpx_upstream.h" +#include "shrpx_downstream.h" +#include "shrpx_worker.h" +#include "shrpx_connection_handler.h" +#include "shrpx_log.h" + +namespace shrpx { + +namespace { +// List of API endpoints +const std::array<APIEndpoint, 2> &apis() { + static const auto apis = new std::array<APIEndpoint, 2>{ + APIEndpoint{ + StringRef::from_lit("/api/v1beta1/backendconfig"), + true, + (1 << API_METHOD_POST) | (1 << API_METHOD_PUT), + &APIDownstreamConnection::handle_backendconfig, + }, + APIEndpoint{ + StringRef::from_lit("/api/v1beta1/configrevision"), + true, + (1 << API_METHOD_GET), + &APIDownstreamConnection::handle_configrevision, + }, + }; + + return *apis; +} +} // namespace + +namespace { +// The method string. This must be same order of APIMethod. +constexpr StringRef API_METHOD_STRING[] = { + StringRef::from_lit("GET"), + StringRef::from_lit("POST"), + StringRef::from_lit("PUT"), +}; +} // namespace + +APIDownstreamConnection::APIDownstreamConnection(Worker *worker) + : worker_(worker), api_(nullptr), fd_(-1), shutdown_read_(false) {} + +APIDownstreamConnection::~APIDownstreamConnection() { + if (fd_ != -1) { + close(fd_); + } +} + +int APIDownstreamConnection::attach_downstream(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Attaching to DOWNSTREAM:" << downstream; + } + + downstream_ = downstream; + + return 0; +} + +void APIDownstreamConnection::detach_downstream(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Detaching from DOWNSTREAM:" << downstream; + } + downstream_ = nullptr; +} + +int APIDownstreamConnection::send_reply(unsigned int http_status, + APIStatusCode api_status, + const StringRef &data) { + shutdown_read_ = true; + + auto upstream = downstream_->get_upstream(); + + auto &resp = downstream_->response(); + + resp.http_status = http_status; + + auto &balloc = downstream_->get_block_allocator(); + + StringRef api_status_str; + + switch (api_status) { + case APIStatusCode::SUCCESS: + api_status_str = StringRef::from_lit("Success"); + break; + case APIStatusCode::FAILURE: + api_status_str = StringRef::from_lit("Failure"); + break; + default: + assert(0); + } + + constexpr auto M1 = StringRef::from_lit("{\"status\":\""); + constexpr auto M2 = StringRef::from_lit("\",\"code\":"); + constexpr auto M3 = StringRef::from_lit("}"); + + // 3 is the number of digits in http_status, assuming it is 3 digits + // number. + auto buflen = M1.size() + M2.size() + M3.size() + data.size() + + api_status_str.size() + 3; + + auto buf = make_byte_ref(balloc, buflen); + auto p = buf.base; + + p = std::copy(std::begin(M1), std::end(M1), p); + p = std::copy(std::begin(api_status_str), std::end(api_status_str), p); + p = std::copy(std::begin(M2), std::end(M2), p); + p = util::utos(p, http_status); + p = std::copy(std::begin(data), std::end(data), p); + p = std::copy(std::begin(M3), std::end(M3), p); + + buf.len = p - buf.base; + + auto content_length = util::make_string_ref_uint(balloc, buf.len); + + resp.fs.add_header_token(StringRef::from_lit("content-length"), + content_length, false, http2::HD_CONTENT_LENGTH); + + switch (http_status) { + case 400: + case 405: + case 413: + resp.fs.add_header_token(StringRef::from_lit("connection"), + StringRef::from_lit("close"), false, + http2::HD_CONNECTION); + break; + } + + if (upstream->send_reply(downstream_, buf.base, buf.len) != 0) { + return -1; + } + + return 0; +} + +namespace { +const APIEndpoint *lookup_api(const StringRef &path) { + switch (path.size()) { + case 26: + switch (path[25]) { + case 'g': + if (util::streq_l("/api/v1beta1/backendconfi", std::begin(path), 25)) { + return &apis()[0]; + } + break; + } + break; + case 27: + switch (path[26]) { + case 'n': + if (util::streq_l("/api/v1beta1/configrevisio", std::begin(path), 26)) { + return &apis()[1]; + } + break; + } + break; + } + return nullptr; +} +} // namespace + +int APIDownstreamConnection::push_request_headers() { + auto &req = downstream_->request(); + + auto path = + StringRef{std::begin(req.path), + std::find(std::begin(req.path), std::end(req.path), '?')}; + + api_ = lookup_api(path); + + if (!api_) { + send_reply(404, APIStatusCode::FAILURE); + + return 0; + } + + switch (req.method) { + case HTTP_GET: + if (!(api_->allowed_methods & (1 << API_METHOD_GET))) { + error_method_not_allowed(); + return 0; + } + break; + case HTTP_POST: + if (!(api_->allowed_methods & (1 << API_METHOD_POST))) { + error_method_not_allowed(); + return 0; + } + break; + case HTTP_PUT: + if (!(api_->allowed_methods & (1 << API_METHOD_PUT))) { + error_method_not_allowed(); + return 0; + } + break; + default: + error_method_not_allowed(); + return 0; + } + + // This works with req.fs.content_length == -1 + if (req.fs.content_length > + static_cast<int64_t>(get_config()->api.max_request_body)) { + send_reply(413, APIStatusCode::FAILURE); + + return 0; + } + + switch (req.method) { + case HTTP_POST: + case HTTP_PUT: { + char tempname[] = "/tmp/nghttpx-api.XXXXXX"; +#ifdef HAVE_MKOSTEMP + fd_ = mkostemp(tempname, O_CLOEXEC); +#else // !HAVE_MKOSTEMP + fd_ = mkstemp(tempname); +#endif // !HAVE_MKOSTEMP + if (fd_ == -1) { + send_reply(500, APIStatusCode::FAILURE); + + return 0; + } +#ifndef HAVE_MKOSTEMP + util::make_socket_closeonexec(fd_); +#endif // HAVE_MKOSTEMP + unlink(tempname); + break; + } + } + + downstream_->set_request_header_sent(true); + auto src = downstream_->get_blocked_request_buf(); + auto dest = downstream_->get_request_buf(); + src->remove(*dest); + + return 0; +} + +int APIDownstreamConnection::error_method_not_allowed() { + auto &resp = downstream_->response(); + + size_t len = 0; + for (uint8_t i = 0; i < API_METHOD_MAX; ++i) { + if (api_->allowed_methods & (1 << i)) { + // The length of method + ", " + len += API_METHOD_STRING[i].size() + 2; + } + } + + assert(len > 0); + + auto &balloc = downstream_->get_block_allocator(); + + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + for (uint8_t i = 0; i < API_METHOD_MAX; ++i) { + if (api_->allowed_methods & (1 << i)) { + auto &s = API_METHOD_STRING[i]; + p = std::copy(std::begin(s), std::end(s), p); + p = std::copy_n(", ", 2, p); + } + } + + p -= 2; + *p = '\0'; + + resp.fs.add_header_token(StringRef::from_lit("allow"), StringRef{iov.base, p}, + false, -1); + return send_reply(405, APIStatusCode::FAILURE); +} + +int APIDownstreamConnection::push_upload_data_chunk(const uint8_t *data, + size_t datalen) { + if (shutdown_read_ || !api_->require_body) { + return 0; + } + + auto &req = downstream_->request(); + auto &apiconf = get_config()->api; + + if (static_cast<size_t>(req.recv_body_length) > apiconf.max_request_body) { + send_reply(413, APIStatusCode::FAILURE); + + return 0; + } + + ssize_t nwrite; + while ((nwrite = write(fd_, data, datalen)) == -1 && errno == EINTR) + ; + if (nwrite == -1) { + auto error = errno; + LOG(ERROR) << "Could not write API request body: errno=" << error; + send_reply(500, APIStatusCode::FAILURE); + + return 0; + } + + // We don't have to call Upstream::resume_read() here, because + // request buffer is effectively unlimited. Actually, we cannot + // call it here since it could recursively call this function again. + + return 0; +} + +int APIDownstreamConnection::end_upload_data() { + if (shutdown_read_) { + return 0; + } + + return api_->handler(*this); +} + +int APIDownstreamConnection::handle_backendconfig() { + auto &req = downstream_->request(); + + if (req.recv_body_length == 0) { + send_reply(200, APIStatusCode::SUCCESS); + + return 0; + } + + auto rp = mmap(nullptr, req.recv_body_length, PROT_READ, MAP_SHARED, fd_, 0); + if (rp == reinterpret_cast<void *>(-1)) { + send_reply(500, APIStatusCode::FAILURE); + return 0; + } + + auto unmapper = defer(munmap, rp, req.recv_body_length); + + Config new_config{}; + new_config.conn.downstream = std::make_shared<DownstreamConfig>(); + const auto &downstreamconf = new_config.conn.downstream; + + auto config = get_config(); + auto &src = config->conn.downstream; + + downstreamconf->timeout = src->timeout; + downstreamconf->connections_per_host = src->connections_per_host; + downstreamconf->connections_per_frontend = src->connections_per_frontend; + downstreamconf->request_buffer_size = src->request_buffer_size; + downstreamconf->response_buffer_size = src->response_buffer_size; + downstreamconf->family = src->family; + + std::set<StringRef> include_set; + std::map<StringRef, size_t> pattern_addr_indexer; + + for (auto first = reinterpret_cast<const uint8_t *>(rp), + last = first + req.recv_body_length; + first != last;) { + auto eol = std::find(first, last, '\n'); + if (eol == last) { + break; + } + + if (first == eol || *first == '#') { + first = ++eol; + continue; + } + + auto eq = std::find(first, eol, '='); + if (eq == eol) { + send_reply(400, APIStatusCode::FAILURE); + return 0; + } + + auto opt = StringRef{first, eq}; + auto optval = StringRef{eq + 1, eol}; + + auto optid = option_lookup_token(opt.c_str(), opt.size()); + + switch (optid) { + case SHRPX_OPTID_BACKEND: + break; + default: + first = ++eol; + continue; + } + + if (parse_config(&new_config, optid, opt, optval, include_set, + pattern_addr_indexer) != 0) { + send_reply(400, APIStatusCode::FAILURE); + return 0; + } + + first = ++eol; + } + + auto &tlsconf = config->tls; + if (configure_downstream_group(&new_config, config->http2_proxy, true, + tlsconf) != 0) { + send_reply(400, APIStatusCode::FAILURE); + return 0; + } + + auto conn_handler = worker_->get_connection_handler(); + + conn_handler->send_replace_downstream(downstreamconf); + + send_reply(200, APIStatusCode::SUCCESS); + + return 0; +} + +int APIDownstreamConnection::handle_configrevision() { + auto config = get_config(); + auto &balloc = downstream_->get_block_allocator(); + + // Construct the following string: + // , + // "data":{ + // "configRevision": N + // } + auto data = concat_string_ref( + balloc, StringRef::from_lit(R"(,"data":{"configRevision":)"), + util::make_string_ref_uint(balloc, config->config_revision), + StringRef::from_lit("}")); + + send_reply(200, APIStatusCode::SUCCESS, data); + + return 0; +} + +void APIDownstreamConnection::pause_read(IOCtrlReason reason) {} + +int APIDownstreamConnection::resume_read(IOCtrlReason reason, size_t consumed) { + return 0; +} + +void APIDownstreamConnection::force_resume_read() {} + +int APIDownstreamConnection::on_read() { return 0; } + +int APIDownstreamConnection::on_write() { return 0; } + +void APIDownstreamConnection::on_upstream_change(Upstream *upstream) {} + +bool APIDownstreamConnection::poolable() const { return false; } + +const std::shared_ptr<DownstreamAddrGroup> & +APIDownstreamConnection::get_downstream_addr_group() const { + static std::shared_ptr<DownstreamAddrGroup> s; + return s; +} + +DownstreamAddr *APIDownstreamConnection::get_addr() const { return nullptr; } + +} // namespace shrpx diff --git a/src/shrpx_api_downstream_connection.h b/src/shrpx_api_downstream_connection.h new file mode 100644 index 0000000..5d4182f --- /dev/null +++ b/src/shrpx_api_downstream_connection.h @@ -0,0 +1,114 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_API_DOWNSTREAM_CONNECTION_H +#define SHRPX_API_DOWNSTREAM_CONNECTION_H + +#include "shrpx_downstream_connection.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +class Worker; + +// If new method is added, don't forget to update API_METHOD_STRING as +// well. +enum APIMethod { + API_METHOD_GET, + API_METHOD_POST, + API_METHOD_PUT, + API_METHOD_MAX, +}; + +// API status code, which is independent from HTTP status code. But +// generally, 2xx code for SUCCESS, and otherwise FAILURE. +enum class APIStatusCode { + SUCCESS, + FAILURE, +}; + +class APIDownstreamConnection; + +struct APIEndpoint { + // Endpoint path. It must start with "/api/". + StringRef path; + // true if we evaluate request body. + bool require_body; + // Allowed methods. This is bitwise OR of one or more of (1 << + // APIMethod value). + uint8_t allowed_methods; + std::function<int(APIDownstreamConnection &)> handler; +}; + +class APIDownstreamConnection : public DownstreamConnection { +public: + APIDownstreamConnection(Worker *worker); + virtual ~APIDownstreamConnection(); + virtual int attach_downstream(Downstream *downstream); + virtual void detach_downstream(Downstream *downstream); + + virtual int push_request_headers(); + virtual int push_upload_data_chunk(const uint8_t *data, size_t datalen); + virtual int end_upload_data(); + + virtual void pause_read(IOCtrlReason reason); + virtual int resume_read(IOCtrlReason reason, size_t consumed); + virtual void force_resume_read(); + + virtual int on_read(); + virtual int on_write(); + + virtual void on_upstream_change(Upstream *upstream); + + // true if this object is poolable. + virtual bool poolable() const; + + virtual const std::shared_ptr<DownstreamAddrGroup> & + get_downstream_addr_group() const; + virtual DownstreamAddr *get_addr() const; + + int send_reply(unsigned int http_status, APIStatusCode api_status, + const StringRef &data = StringRef{}); + int error_method_not_allowed(); + + // Handles backendconfig API request. + int handle_backendconfig(); + // Handles configrevision API request. + int handle_configrevision(); + +private: + Worker *worker_; + // This points to the requested APIEndpoint struct. + const APIEndpoint *api_; + // The file descriptor for temporary file to store request body. + int fd_; + // true if we stop reading request body. + bool shutdown_read_; +}; + +} // namespace shrpx + +#endif // SHRPX_API_DOWNSTREAM_CONNECTION_H diff --git a/src/shrpx_client_handler.cc b/src/shrpx_client_handler.cc new file mode 100644 index 0000000..1f0c01c --- /dev/null +++ b/src/shrpx_client_handler.cc @@ -0,0 +1,1703 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_client_handler.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H + +#include <cerrno> + +#include "shrpx_upstream.h" +#include "shrpx_http2_upstream.h" +#include "shrpx_https_upstream.h" +#include "shrpx_config.h" +#include "shrpx_http_downstream_connection.h" +#include "shrpx_http2_downstream_connection.h" +#include "shrpx_tls.h" +#include "shrpx_worker.h" +#include "shrpx_downstream_connection_pool.h" +#include "shrpx_downstream.h" +#include "shrpx_http2_session.h" +#include "shrpx_connect_blocker.h" +#include "shrpx_api_downstream_connection.h" +#include "shrpx_health_monitor_downstream_connection.h" +#include "shrpx_null_downstream_connection.h" +#ifdef ENABLE_HTTP3 +# include "shrpx_http3_upstream.h" +#endif // ENABLE_HTTP3 +#include "shrpx_log.h" +#include "util.h" +#include "template.h" +#include "tls.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto handler = static_cast<ClientHandler *>(conn->data); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "Time out"; + } + + delete handler; +} +} // namespace + +namespace { +void shutdowncb(struct ev_loop *loop, ev_timer *w, int revents) { + auto handler = static_cast<ClientHandler *>(w->data); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "Close connection due to TLS renegotiation"; + } + + delete handler; +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto handler = static_cast<ClientHandler *>(conn->data); + + if (handler->do_read() != 0) { + delete handler; + return; + } +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto handler = static_cast<ClientHandler *>(conn->data); + + if (handler->do_write() != 0) { + delete handler; + return; + } +} +} // namespace + +int ClientHandler::noop() { return 0; } + +int ClientHandler::read_clear() { + auto should_break = false; + rb_.ensure_chunk(); + for (;;) { + if (rb_.rleft() && on_read() != 0) { + return -1; + } + if (rb_.rleft() == 0) { + rb_.reset(); + } else if (rb_.wleft() == 0) { + conn_.rlimit.stopw(); + return 0; + } + + if (!ev_is_active(&conn_.rev) || should_break) { + return 0; + } + + auto nread = conn_.read_clear(rb_.last(), rb_.wleft()); + + if (nread == 0) { + if (rb_.rleft() == 0) { + rb_.release_chunk(); + } + return 0; + } + + if (nread < 0) { + return -1; + } + + rb_.write(nread); + should_break = true; + } +} + +int ClientHandler::write_clear() { + std::array<iovec, 2> iov; + + for (;;) { + if (on_write() != 0) { + return -1; + } + + auto iovcnt = upstream_->response_riovec(iov.data(), iov.size()); + if (iovcnt == 0) { + break; + } + + auto nwrite = conn_.writev_clear(iov.data(), iovcnt); + if (nwrite < 0) { + return -1; + } + + if (nwrite == 0) { + return 0; + } + + upstream_->response_drain(nwrite); + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + return 0; +} + +int ClientHandler::proxy_protocol_peek_clear() { + rb_.ensure_chunk(); + + assert(rb_.rleft() == 0); + + auto nread = conn_.peek_clear(rb_.last(), rb_.wleft()); + if (nread < 0) { + return -1; + } + if (nread == 0) { + return 0; + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol: Peek " << nread + << " bytes from socket"; + } + + rb_.write(nread); + + if (on_read() != 0) { + return -1; + } + + rb_.reset(); + + return 0; +} + +int ClientHandler::tls_handshake() { + ev_timer_again(conn_.loop, &conn_.rt); + + ERR_clear_error(); + + auto rv = conn_.tls_handshake(); + + if (rv == SHRPX_ERR_INPROGRESS) { + return 0; + } + + if (rv < 0) { + return -1; + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "SSL/TLS handshake completed"; + } + + if (validate_next_proto() != 0) { + return -1; + } + + read_ = &ClientHandler::read_tls; + write_ = &ClientHandler::write_tls; + + return 0; +} + +int ClientHandler::read_tls() { + auto should_break = false; + + ERR_clear_error(); + + rb_.ensure_chunk(); + + for (;;) { + // we should process buffered data first before we read EOF. + if (rb_.rleft() && on_read() != 0) { + return -1; + } + if (rb_.rleft() == 0) { + rb_.reset(); + } else if (rb_.wleft() == 0) { + conn_.rlimit.stopw(); + return 0; + } + + if (!ev_is_active(&conn_.rev) || should_break) { + return 0; + } + + auto nread = conn_.read_tls(rb_.last(), rb_.wleft()); + + if (nread == 0) { + if (rb_.rleft() == 0) { + rb_.release_chunk(); + } + return 0; + } + + if (nread < 0) { + return -1; + } + + rb_.write(nread); + should_break = true; + } +} + +int ClientHandler::write_tls() { + struct iovec iov; + + ERR_clear_error(); + + if (on_write() != 0) { + return -1; + } + + auto iovcnt = upstream_->response_riovec(&iov, 1); + if (iovcnt == 0) { + conn_.start_tls_write_idle(); + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + return 0; + } + + for (;;) { + auto nwrite = conn_.write_tls(iov.iov_base, iov.iov_len); + if (nwrite < 0) { + return -1; + } + + if (nwrite == 0) { + return 0; + } + + upstream_->response_drain(nwrite); + + iovcnt = upstream_->response_riovec(&iov, 1); + if (iovcnt == 0) { + return 0; + } + } +} + +#ifdef ENABLE_HTTP3 +int ClientHandler::read_quic(const UpstreamAddr *faddr, + const Address &remote_addr, + const Address &local_addr, + const ngtcp2_pkt_info &pi, const uint8_t *data, + size_t datalen) { + auto upstream = static_cast<Http3Upstream *>(upstream_.get()); + + return upstream->on_read(faddr, remote_addr, local_addr, pi, data, datalen); +} + +int ClientHandler::write_quic() { return upstream_->on_write(); } +#endif // ENABLE_HTTP3 + +int ClientHandler::upstream_noop() { return 0; } + +int ClientHandler::upstream_read() { + assert(upstream_); + if (upstream_->on_read() != 0) { + return -1; + } + return 0; +} + +int ClientHandler::upstream_write() { + assert(upstream_); + if (upstream_->on_write() != 0) { + return -1; + } + + if (get_should_close_after_write() && upstream_->response_empty()) { + return -1; + } + + return 0; +} + +int ClientHandler::upstream_http2_connhd_read() { + auto nread = std::min(left_connhd_len_, rb_.rleft()); + if (memcmp(&NGHTTP2_CLIENT_MAGIC[NGHTTP2_CLIENT_MAGIC_LEN - left_connhd_len_], + rb_.pos(), nread) != 0) { + // There is no downgrade path here. Just drop the connection. + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "invalid client connection header"; + } + + return -1; + } + + left_connhd_len_ -= nread; + rb_.drain(nread); + conn_.rlimit.startw(); + + if (left_connhd_len_ == 0) { + on_read_ = &ClientHandler::upstream_read; + // Run on_read to process data left in buffer since they are not + // notified further + if (on_read() != 0) { + return -1; + } + return 0; + } + + return 0; +} + +int ClientHandler::upstream_http1_connhd_read() { + auto nread = std::min(left_connhd_len_, rb_.rleft()); + if (memcmp(&NGHTTP2_CLIENT_MAGIC[NGHTTP2_CLIENT_MAGIC_LEN - left_connhd_len_], + rb_.pos(), nread) != 0) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "This is HTTP/1.1 connection, " + << "but may be upgraded to HTTP/2 later."; + } + + // Reset header length for later HTTP/2 upgrade + left_connhd_len_ = NGHTTP2_CLIENT_MAGIC_LEN; + on_read_ = &ClientHandler::upstream_read; + on_write_ = &ClientHandler::upstream_write; + + if (on_read() != 0) { + return -1; + } + + return 0; + } + + left_connhd_len_ -= nread; + rb_.drain(nread); + conn_.rlimit.startw(); + + if (left_connhd_len_ == 0) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "direct HTTP/2 connection"; + } + + direct_http2_upgrade(); + on_read_ = &ClientHandler::upstream_read; + on_write_ = &ClientHandler::upstream_write; + + // Run on_read to process data left in buffer since they are not + // notified further + if (on_read() != 0) { + return -1; + } + + return 0; + } + + return 0; +} + +ClientHandler::ClientHandler(Worker *worker, int fd, SSL *ssl, + const StringRef &ipaddr, const StringRef &port, + int family, const UpstreamAddr *faddr) + : // We use balloc_ for TLS session ID (64), ipaddr (IPv6) (39), + // port (5), forwarded-for (IPv6) (41), alpn (5), proxyproto + // ipaddr (15), proxyproto port (5), sni (32, estimated). we + // need terminal NULL byte for each. We also require 8 bytes + // header for each allocation. We align at 16 bytes boundary, + // so the required space is 64 + 48 + 16 + 48 + 16 + 16 + 16 + + // 32 + 8 + 8 * 8 = 328. + balloc_(512, 512), + rb_(worker->get_mcpool()), + conn_(worker->get_loop(), fd, ssl, worker->get_mcpool(), + get_config()->conn.upstream.timeout.write, + get_config()->conn.upstream.timeout.read, + get_config()->conn.upstream.ratelimit.write, + get_config()->conn.upstream.ratelimit.read, writecb, readcb, + timeoutcb, this, get_config()->tls.dyn_rec.warmup_threshold, + get_config()->tls.dyn_rec.idle_timeout, + faddr->quic ? Proto::HTTP3 : Proto::NONE), + ipaddr_(make_string_ref(balloc_, ipaddr)), + port_(make_string_ref(balloc_, port)), + faddr_(faddr), + worker_(worker), + left_connhd_len_(NGHTTP2_CLIENT_MAGIC_LEN), + affinity_hash_(0), + should_close_after_write_(false), + affinity_hash_computed_(false) { + + ++worker_->get_worker_stat()->num_connections; + + ev_timer_init(&reneg_shutdown_timer_, shutdowncb, 0., 0.); + + reneg_shutdown_timer_.data = this; + + if (!faddr->quic) { + conn_.rlimit.startw(); + } + ev_timer_again(conn_.loop, &conn_.rt); + + auto config = get_config(); + + if (!faddr->quic) { + if (faddr_->accept_proxy_protocol || + config->conn.upstream.accept_proxy_protocol) { + read_ = &ClientHandler::proxy_protocol_peek_clear; + write_ = &ClientHandler::noop; + on_read_ = &ClientHandler::proxy_protocol_read; + on_write_ = &ClientHandler::upstream_noop; + } else { + setup_upstream_io_callback(); + } + } + + auto &fwdconf = config->http.forwarded; + + if (fwdconf.params & FORWARDED_FOR) { + if (fwdconf.for_node_type == ForwardedNode::OBFUSCATED) { + // 1 for '_' + auto len = SHRPX_OBFUSCATED_NODE_LENGTH + 1; + // 1 for terminating NUL. + auto buf = make_byte_ref(balloc_, len + 1); + auto p = buf.base; + *p++ = '_'; + p = util::random_alpha_digit(p, p + SHRPX_OBFUSCATED_NODE_LENGTH, + worker_->get_randgen()); + *p = '\0'; + + forwarded_for_ = StringRef{buf.base, p}; + } else { + init_forwarded_for(family, ipaddr_); + } + } +} + +void ClientHandler::init_forwarded_for(int family, const StringRef &ipaddr) { + if (family == AF_INET6) { + // 2 for '[' and ']' + auto len = 2 + ipaddr.size(); + // 1 for terminating NUL. + auto buf = make_byte_ref(balloc_, len + 1); + auto p = buf.base; + *p++ = '['; + p = std::copy(std::begin(ipaddr), std::end(ipaddr), p); + *p++ = ']'; + *p = '\0'; + + forwarded_for_ = StringRef{buf.base, p}; + } else { + // family == AF_INET or family == AF_UNIX + forwarded_for_ = ipaddr; + } +} + +void ClientHandler::setup_upstream_io_callback() { + if (conn_.tls.ssl) { + conn_.prepare_server_handshake(); + read_ = write_ = &ClientHandler::tls_handshake; + on_read_ = &ClientHandler::upstream_noop; + on_write_ = &ClientHandler::upstream_write; + } else { + // For non-TLS version, first create HttpsUpstream. It may be + // upgraded to HTTP/2 through HTTP Upgrade or direct HTTP/2 + // connection. + upstream_ = std::make_unique<HttpsUpstream>(this); + alpn_ = StringRef::from_lit("http/1.1"); + read_ = &ClientHandler::read_clear; + write_ = &ClientHandler::write_clear; + on_read_ = &ClientHandler::upstream_http1_connhd_read; + on_write_ = &ClientHandler::upstream_noop; + } +} + +#ifdef ENABLE_HTTP3 +void ClientHandler::setup_http3_upstream( + std::unique_ptr<Http3Upstream> &&upstream) { + upstream_ = std::move(upstream); + write_ = &ClientHandler::write_quic; + + auto config = get_config(); + + reset_upstream_read_timeout(config->conn.upstream.timeout.http3_read); +} +#endif // ENABLE_HTTP3 + +ClientHandler::~ClientHandler() { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Deleting"; + } + + if (upstream_) { + upstream_->on_handler_delete(); + } + + auto worker_stat = worker_->get_worker_stat(); + --worker_stat->num_connections; + + if (worker_stat->num_connections == 0) { + worker_->schedule_clear_mcpool(); + } + + ev_timer_stop(conn_.loop, &reneg_shutdown_timer_); + + // TODO If backend is http/2, and it is in CONNECTED state, signal + // it and make it loopbreak when output is zero. + if (worker_->get_graceful_shutdown() && worker_stat->num_connections == 0 && + worker_stat->num_close_waits == 0) { + ev_break(conn_.loop); + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Deleted"; + } +} + +Upstream *ClientHandler::get_upstream() { return upstream_.get(); } + +struct ev_loop *ClientHandler::get_loop() const { return conn_.loop; } + +void ClientHandler::reset_upstream_read_timeout(ev_tstamp t) { + conn_.rt.repeat = t; + if (ev_is_active(&conn_.rt)) { + ev_timer_again(conn_.loop, &conn_.rt); + } +} + +void ClientHandler::reset_upstream_write_timeout(ev_tstamp t) { + conn_.wt.repeat = t; + if (ev_is_active(&conn_.wt)) { + ev_timer_again(conn_.loop, &conn_.wt); + } +} + +void ClientHandler::repeat_read_timer() { + ev_timer_again(conn_.loop, &conn_.rt); +} + +void ClientHandler::stop_read_timer() { ev_timer_stop(conn_.loop, &conn_.rt); } + +int ClientHandler::validate_next_proto() { + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len = 0; + + // First set callback for catch all cases + on_read_ = &ClientHandler::upstream_read; + + SSL_get0_alpn_selected(conn_.tls.ssl, &next_proto, &next_proto_len); + + StringRef proto; + + if (next_proto) { + proto = StringRef{next_proto, next_proto_len}; + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "The negotiated next protocol: " << proto; + } + } else { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "No protocol negotiated. Fallback to HTTP/1.1"; + } + + proto = StringRef::from_lit("http/1.1"); + } + + if (!tls::in_proto_list(get_config()->tls.alpn_list, proto)) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "The negotiated protocol is not supported: " << proto; + } + return -1; + } + + if (util::check_h2_is_selected(proto)) { + on_read_ = &ClientHandler::upstream_http2_connhd_read; + + auto http2_upstream = std::make_unique<Http2Upstream>(this); + + upstream_ = std::move(http2_upstream); + alpn_ = make_string_ref(balloc_, proto); + + // At this point, input buffer is already filled with some bytes. + // The read callback is not called until new data come. So consume + // input buffer here. + if (on_read() != 0) { + return -1; + } + + return 0; + } + + if (proto == StringRef::from_lit("http/1.1")) { + upstream_ = std::make_unique<HttpsUpstream>(this); + alpn_ = StringRef::from_lit("http/1.1"); + + // At this point, input buffer is already filled with some bytes. + // The read callback is not called until new data come. So consume + // input buffer here. + if (on_read() != 0) { + return -1; + } + + return 0; + } + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "The negotiated protocol is not supported"; + } + return -1; +} + +int ClientHandler::do_read() { return read_(*this); } +int ClientHandler::do_write() { return write_(*this); } + +int ClientHandler::on_read() { + if (rb_.chunk_avail()) { + auto rv = on_read_(*this); + if (rv != 0) { + return rv; + } + } + conn_.handle_tls_pending_read(); + return 0; +} +int ClientHandler::on_write() { return on_write_(*this); } + +const StringRef &ClientHandler::get_ipaddr() const { return ipaddr_; } + +bool ClientHandler::get_should_close_after_write() const { + return should_close_after_write_; +} + +void ClientHandler::set_should_close_after_write(bool f) { + should_close_after_write_ = f; +} + +void ClientHandler::pool_downstream_connection( + std::unique_ptr<DownstreamConnection> dconn) { + if (!dconn->poolable()) { + return; + } + + dconn->set_client_handler(nullptr); + + auto &group = dconn->get_downstream_addr_group(); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Pooling downstream connection DCONN:" << dconn.get() + << " in group " << group; + } + + auto addr = dconn->get_addr(); + auto &dconn_pool = addr->dconn_pool; + dconn_pool->add_downstream_connection(std::move(dconn)); +} + +namespace { +// Computes 32bits hash for session affinity for IP address |ip|. +uint32_t compute_affinity_from_ip(const StringRef &ip) { + int rv; + std::array<uint8_t, 32> buf; + + rv = util::sha256(buf.data(), ip); + if (rv != 0) { + // Not sure when sha256 failed. Just fall back to another + // function. + return util::hash32(ip); + } + + return (static_cast<uint32_t>(buf[0]) << 24) | + (static_cast<uint32_t>(buf[1]) << 16) | + (static_cast<uint32_t>(buf[2]) << 8) | static_cast<uint32_t>(buf[3]); +} +} // namespace + +Http2Session *ClientHandler::get_http2_session( + const std::shared_ptr<DownstreamAddrGroup> &group, DownstreamAddr *addr) { + auto &shared_addr = group->shared_addr; + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Selected DownstreamAddr=" << addr + << ", index=" << (addr - shared_addr->addrs.data()); + } + + for (auto session = addr->http2_extra_freelist.head; session;) { + auto next = session->dlnext; + + if (session->max_concurrency_reached(0)) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) + << "Maximum streams have been reached for Http2Session(" << session + << "). Skip it"; + } + + session->remove_from_freelist(); + session = next; + + continue; + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Use Http2Session " << session + << " from http2_extra_freelist"; + } + + if (session->max_concurrency_reached(1)) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Maximum streams are reached for Http2Session(" + << session << ")."; + } + + session->remove_from_freelist(); + } + return session; + } + + auto session = new Http2Session(conn_.loop, worker_->get_cl_ssl_ctx(), + worker_, group, addr); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Create new Http2Session " << session; + } + + session->add_to_extra_freelist(); + + return session; +} + +uint32_t ClientHandler::get_affinity_cookie(Downstream *downstream, + const StringRef &cookie_name) { + auto h = downstream->find_affinity_cookie(cookie_name); + if (h) { + return h; + } + + auto d = std::uniform_int_distribution<uint32_t>(1); + auto rh = d(worker_->get_randgen()); + h = util::hash32(StringRef{reinterpret_cast<uint8_t *>(&rh), + reinterpret_cast<uint8_t *>(&rh) + sizeof(rh)}); + + downstream->renew_affinity_cookie(h); + + return h; +} + +namespace { +void reschedule_addr( + std::priority_queue<DownstreamAddrEntry, std::vector<DownstreamAddrEntry>, + DownstreamAddrEntryGreater> &pq, + DownstreamAddr *addr) { + auto penalty = MAX_DOWNSTREAM_ADDR_WEIGHT + addr->pending_penalty; + addr->cycle += penalty / addr->weight; + addr->pending_penalty = penalty % addr->weight; + + pq.push(DownstreamAddrEntry{addr, addr->seq, addr->cycle}); + addr->queued = true; +} +} // namespace + +namespace { +void reschedule_wg( + std::priority_queue<WeightGroupEntry, std::vector<WeightGroupEntry>, + WeightGroupEntryGreater> &pq, + WeightGroup *wg) { + auto penalty = MAX_DOWNSTREAM_ADDR_WEIGHT + wg->pending_penalty; + wg->cycle += penalty / wg->weight; + wg->pending_penalty = penalty % wg->weight; + + pq.push(WeightGroupEntry{wg, wg->seq, wg->cycle}); + wg->queued = true; +} +} // namespace + +DownstreamAddr *ClientHandler::get_downstream_addr(int &err, + DownstreamAddrGroup *group, + Downstream *downstream) { + err = 0; + + switch (faddr_->alt_mode) { + case UpstreamAltMode::API: + case UpstreamAltMode::HEALTHMON: + assert(0); + default: + break; + } + + auto &shared_addr = group->shared_addr; + + if (shared_addr->affinity.type != SessionAffinity::NONE) { + uint32_t hash; + switch (shared_addr->affinity.type) { + case SessionAffinity::IP: + if (!affinity_hash_computed_) { + affinity_hash_ = compute_affinity_from_ip(ipaddr_); + affinity_hash_computed_ = true; + } + hash = affinity_hash_; + break; + case SessionAffinity::COOKIE: + if (shared_addr->affinity.cookie.stickiness == + SessionAffinityCookieStickiness::STRICT) { + return get_downstream_addr_strict_affinity(err, shared_addr, + downstream); + } + + hash = get_affinity_cookie(downstream, shared_addr->affinity.cookie.name); + break; + default: + assert(0); + } + + const auto &affinity_hash = shared_addr->affinity_hash; + + auto it = std::lower_bound( + std::begin(affinity_hash), std::end(affinity_hash), hash, + [](const AffinityHash &lhs, uint32_t rhs) { return lhs.hash < rhs; }); + + if (it == std::end(affinity_hash)) { + it = std::begin(affinity_hash); + } + + auto aff_idx = + static_cast<size_t>(std::distance(std::begin(affinity_hash), it)); + auto idx = (*it).idx; + auto addr = &shared_addr->addrs[idx]; + + if (addr->connect_blocker->blocked()) { + size_t i; + for (i = aff_idx + 1; i != aff_idx; ++i) { + if (i == shared_addr->affinity_hash.size()) { + i = 0; + } + addr = &shared_addr->addrs[shared_addr->affinity_hash[i].idx]; + if (addr->connect_blocker->blocked()) { + continue; + } + break; + } + if (i == aff_idx) { + err = -1; + return nullptr; + } + } + + return addr; + } + + auto &wgpq = shared_addr->pq; + + for (;;) { + if (wgpq.empty()) { + CLOG(INFO, this) << "No working downstream address found"; + err = -1; + return nullptr; + } + + auto wg = wgpq.top().wg; + wgpq.pop(); + wg->queued = false; + + for (;;) { + if (wg->pq.empty()) { + break; + } + + auto addr = wg->pq.top().addr; + wg->pq.pop(); + addr->queued = false; + + if (addr->connect_blocker->blocked()) { + continue; + } + + reschedule_addr(wg->pq, addr); + reschedule_wg(wgpq, wg); + + return addr; + } + } +} + +DownstreamAddr *ClientHandler::get_downstream_addr_strict_affinity( + int &err, const std::shared_ptr<SharedDownstreamAddr> &shared_addr, + Downstream *downstream) { + const auto &affinity_hash = shared_addr->affinity_hash; + + auto h = downstream->find_affinity_cookie(shared_addr->affinity.cookie.name); + if (h) { + auto it = shared_addr->affinity_hash_map.find(h); + if (it != std::end(shared_addr->affinity_hash_map)) { + auto addr = &shared_addr->addrs[(*it).second]; + if (!addr->connect_blocker->blocked()) { + return addr; + } + } + } else { + auto d = std::uniform_int_distribution<uint32_t>(1); + auto rh = d(worker_->get_randgen()); + h = util::hash32(StringRef{reinterpret_cast<uint8_t *>(&rh), + reinterpret_cast<uint8_t *>(&rh) + sizeof(rh)}); + } + + // Client is not bound to a particular backend, or the bound backend + // is not found, or is blocked. Find new backend using h. Using + // existing h allows us to find new server in a deterministic way. + // It is preferable because multiple concurrent requests with the + // stale cookie might be in-flight. + auto it = std::lower_bound( + std::begin(affinity_hash), std::end(affinity_hash), h, + [](const AffinityHash &lhs, uint32_t rhs) { return lhs.hash < rhs; }); + + if (it == std::end(affinity_hash)) { + it = std::begin(affinity_hash); + } + + auto aff_idx = + static_cast<size_t>(std::distance(std::begin(affinity_hash), it)); + auto idx = (*it).idx; + auto addr = &shared_addr->addrs[idx]; + + if (addr->connect_blocker->blocked()) { + size_t i; + for (i = aff_idx + 1; i != aff_idx; ++i) { + if (i == shared_addr->affinity_hash.size()) { + i = 0; + } + addr = &shared_addr->addrs[shared_addr->affinity_hash[i].idx]; + if (addr->connect_blocker->blocked()) { + continue; + } + break; + } + if (i == aff_idx) { + err = -1; + return nullptr; + } + } + + downstream->renew_affinity_cookie(addr->affinity_hash); + + return addr; +} + +std::unique_ptr<DownstreamConnection> +ClientHandler::get_downstream_connection(int &err, Downstream *downstream) { + size_t group_idx; + auto &downstreamconf = *worker_->get_downstream_config(); + auto &routerconf = downstreamconf.router; + + auto catch_all = downstreamconf.addr_group_catch_all; + auto &groups = worker_->get_downstream_addr_groups(); + + auto &req = downstream->request(); + + err = 0; + + switch (faddr_->alt_mode) { + case UpstreamAltMode::API: { + auto dconn = std::make_unique<APIDownstreamConnection>(worker_); + dconn->set_client_handler(this); + return dconn; + } + case UpstreamAltMode::HEALTHMON: { + auto dconn = std::make_unique<HealthMonitorDownstreamConnection>(); + dconn->set_client_handler(this); + return dconn; + } + default: + break; + } + + auto &balloc = downstream->get_block_allocator(); + + StringRef authority, path; + + if (req.forwarded_once) { + if (groups.size() != 1) { + authority = req.orig_authority; + path = req.orig_path; + } + } else { + if (faddr_->sni_fwd) { + authority = sni_; + } else if (!req.authority.empty()) { + authority = req.authority; + } else { + auto h = req.fs.header(http2::HD_HOST); + if (h) { + authority = h->value; + } + } + + // CONNECT method does not have path. But we requires path in + // host-path mapping. As workaround, we assume that path is + // "/". + if (!req.regular_connect_method()) { + path = req.path; + } + + // Cache the authority and path used for the first-time backend + // selection because per-pattern mruby script can change them. + req.orig_authority = authority; + req.orig_path = path; + req.forwarded_once = true; + } + + // Fast path. If we have one group, it must be catch-all group. + if (groups.size() == 1) { + group_idx = 0; + } else { + group_idx = match_downstream_addr_group(routerconf, authority, path, groups, + catch_all, balloc); + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Downstream address group_idx: " << group_idx; + } + + if (groups[group_idx]->shared_addr->redirect_if_not_tls && !conn_.tls.ssl) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Downstream address group " << group_idx + << " requires frontend TLS connection."; + } + err = SHRPX_ERR_TLS_REQUIRED; + return nullptr; + } + + auto &group = groups[group_idx]; + + if (group->shared_addr->dnf) { + auto dconn = std::make_unique<NullDownstreamConnection>(group); + dconn->set_client_handler(this); + return dconn; + } + + auto addr = get_downstream_addr(err, group.get(), downstream); + if (addr == nullptr) { + return nullptr; + } + + if (addr->proto == Proto::HTTP1) { + auto dconn = addr->dconn_pool->pop_downstream_connection(); + if (dconn) { + dconn->set_client_handler(this); + return dconn; + } + + if (worker_->get_connect_blocker()->blocked()) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) + << "Worker wide backend connection was blocked temporarily"; + } + return nullptr; + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Downstream connection pool is empty." + << " Create new one"; + } + + dconn = std::make_unique<HttpDownstreamConnection>(group, addr, conn_.loop, + worker_); + dconn->set_client_handler(this); + return dconn; + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "Downstream connection pool is empty." + << " Create new one"; + } + + auto http2session = get_http2_session(group, addr); + auto dconn = std::make_unique<Http2DownstreamConnection>(http2session); + dconn->set_client_handler(this); + return dconn; +} + +MemchunkPool *ClientHandler::get_mcpool() { return worker_->get_mcpool(); } + +SSL *ClientHandler::get_ssl() const { return conn_.tls.ssl; } + +void ClientHandler::direct_http2_upgrade() { + upstream_ = std::make_unique<Http2Upstream>(this); + alpn_ = StringRef::from_lit(NGHTTP2_CLEARTEXT_PROTO_VERSION_ID); + on_read_ = &ClientHandler::upstream_read; + write_ = &ClientHandler::write_clear; +} + +int ClientHandler::perform_http2_upgrade(HttpsUpstream *http) { + auto upstream = std::make_unique<Http2Upstream>(this); + + auto output = upstream->get_response_buf(); + + // We might have written non-final header in response_buf, in this + // case, response_state is still INITIAL. If this non-final header + // and upgrade header fit in output buffer, do upgrade. Otherwise, + // to avoid to send this non-final header as response body in HTTP/2 + // upstream, fail upgrade. + auto downstream = http->get_downstream(); + auto input = downstream->get_response_buf(); + + if (upstream->upgrade_upstream(http) != 0) { + return -1; + } + // http pointer is now owned by upstream. + upstream_.release(); + // TODO We might get other version id in HTTP2-settings, if we + // support aliasing for h2, but we just use library default for now. + alpn_ = StringRef::from_lit(NGHTTP2_CLEARTEXT_PROTO_VERSION_ID); + on_read_ = &ClientHandler::upstream_http2_connhd_read; + write_ = &ClientHandler::write_clear; + + input->remove(*output, input->rleft()); + + constexpr auto res = + StringRef::from_lit("HTTP/1.1 101 Switching Protocols\r\n" + "Connection: Upgrade\r\n" + "Upgrade: " NGHTTP2_CLEARTEXT_PROTO_VERSION_ID "\r\n" + "\r\n"); + + output->append(res); + upstream_ = std::move(upstream); + + signal_write(); + return 0; +} + +bool ClientHandler::get_http2_upgrade_allowed() const { return !conn_.tls.ssl; } + +StringRef ClientHandler::get_upstream_scheme() const { + if (conn_.tls.ssl) { + return StringRef::from_lit("https"); + } else { + return StringRef::from_lit("http"); + } +} + +void ClientHandler::start_immediate_shutdown() { + ev_timer_start(conn_.loop, &reneg_shutdown_timer_); +} + +void ClientHandler::write_accesslog(Downstream *downstream) { + auto &req = downstream->request(); + + auto config = get_config(); + + if (!req.tstamp) { + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + req.tstamp = lgconf->tstamp; + } + + upstream_accesslog( + config->logging.access.format, + LogSpec{ + downstream, + ipaddr_, + alpn_, + sni_, + conn_.tls.ssl, + std::chrono::high_resolution_clock::now(), // request_end_time + port_, + faddr_->port, + config->pid, + }); +} + +ClientHandler::ReadBuf *ClientHandler::get_rb() { return &rb_; } + +void ClientHandler::signal_write() { conn_.wlimit.startw(); } + +RateLimit *ClientHandler::get_rlimit() { return &conn_.rlimit; } +RateLimit *ClientHandler::get_wlimit() { return &conn_.wlimit; } + +ev_io *ClientHandler::get_wev() { return &conn_.wev; } + +Worker *ClientHandler::get_worker() const { return worker_; } + +namespace { +ssize_t parse_proxy_line_port(const uint8_t *first, const uint8_t *last) { + auto p = first; + int32_t port = 0; + + if (p == last) { + return -1; + } + + if (*p == '0') { + if (p + 1 != last && util::is_digit(*(p + 1))) { + return -1; + } + return 1; + } + + for (; p != last && util::is_digit(*p); ++p) { + port *= 10; + port += *p - '0'; + + if (port > 65535) { + return -1; + } + } + + return p - first; +} +} // namespace + +int ClientHandler::on_proxy_protocol_finish() { + auto len = rb_.pos() - rb_.begin(); + + assert(len); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol: Draining " << len + << " bytes from socket"; + } + + rb_.reset(); + + if (conn_.read_nolim_clear(rb_.pos(), len) < 0) { + return -1; + } + + rb_.reset(); + + setup_upstream_io_callback(); + + return 0; +} + +namespace { +// PROXY-protocol v2 header signature +constexpr uint8_t PROXY_PROTO_V2_SIG[] = + "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"; + +// PROXY-protocol v2 header length +constexpr size_t PROXY_PROTO_V2_HDLEN = + str_size(PROXY_PROTO_V2_SIG) + /* ver_cmd(1) + fam(1) + len(2) = */ 4; +} // namespace + +// http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt +int ClientHandler::proxy_protocol_read() { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol: Started"; + } + + auto first = rb_.pos(); + + if (rb_.rleft() >= PROXY_PROTO_V2_HDLEN && + (*(first + str_size(PROXY_PROTO_V2_SIG)) & 0xf0) == 0x20) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol: Detected v2 header signature"; + } + return proxy_protocol_v2_read(); + } + + // NULL character really destroys functions which expects NULL + // terminated string. We won't expect it in PROXY protocol line, so + // find it here. + auto chrs = std::array<char, 2>{'\n', '\0'}; + + constexpr size_t MAX_PROXY_LINELEN = 107; + + auto bufend = rb_.pos() + std::min(MAX_PROXY_LINELEN, rb_.rleft()); + + auto end = + std::find_first_of(rb_.pos(), bufend, std::begin(chrs), std::end(chrs)); + + if (end == bufend || *end == '\0' || end == rb_.pos() || *(end - 1) != '\r') { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: No ending CR LF sequence found"; + } + return -1; + } + + --end; + + constexpr auto HEADER = StringRef::from_lit("PROXY "); + + if (static_cast<size_t>(end - rb_.pos()) < HEADER.size()) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: PROXY version 1 ID not found"; + } + return -1; + } + + if (!util::streq(HEADER, StringRef{rb_.pos(), HEADER.size()})) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Bad PROXY protocol version 1 ID"; + } + return -1; + } + + rb_.drain(HEADER.size()); + + int family; + + if (rb_.pos()[0] == 'T') { + if (end - rb_.pos() < 5) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: INET protocol family not found"; + } + return -1; + } + + if (rb_.pos()[1] != 'C' || rb_.pos()[2] != 'P') { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Unknown INET protocol family"; + } + return -1; + } + + switch (rb_.pos()[3]) { + case '4': + family = AF_INET; + break; + case '6': + family = AF_INET6; + break; + default: + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Unknown INET protocol family"; + } + return -1; + } + + rb_.drain(5); + } else { + if (end - rb_.pos() < 7) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: INET protocol family not found"; + } + return -1; + } + if (!util::streq_l("UNKNOWN", rb_.pos(), 7)) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Unknown INET protocol family"; + } + return -1; + } + + rb_.drain(end + 2 - rb_.pos()); + + return on_proxy_protocol_finish(); + } + + // source address + auto token_end = std::find(rb_.pos(), end, ' '); + if (token_end == end) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Source address not found"; + } + return -1; + } + + *token_end = '\0'; + if (!util::numeric_host(reinterpret_cast<const char *>(rb_.pos()), family)) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Invalid source address"; + } + return -1; + } + + auto src_addr = rb_.pos(); + auto src_addrlen = token_end - rb_.pos(); + + rb_.drain(token_end - rb_.pos() + 1); + + // destination address + token_end = std::find(rb_.pos(), end, ' '); + if (token_end == end) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Destination address not found"; + } + return -1; + } + + *token_end = '\0'; + if (!util::numeric_host(reinterpret_cast<const char *>(rb_.pos()), family)) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Invalid destination address"; + } + return -1; + } + + // Currently we don't use destination address + + rb_.drain(token_end - rb_.pos() + 1); + + // source port + auto n = parse_proxy_line_port(rb_.pos(), end); + if (n <= 0 || *(rb_.pos() + n) != ' ') { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Invalid source port"; + } + return -1; + } + + rb_.pos()[n] = '\0'; + auto src_port = rb_.pos(); + auto src_portlen = n; + + rb_.drain(n + 1); + + // destination port + n = parse_proxy_line_port(rb_.pos(), end); + if (n <= 0 || rb_.pos() + n != end) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Invalid destination port"; + } + return -1; + } + + // Currently we don't use destination port + + rb_.drain(end + 2 - rb_.pos()); + + ipaddr_ = + make_string_ref(balloc_, StringRef{src_addr, src_addr + src_addrlen}); + port_ = make_string_ref(balloc_, StringRef{src_port, src_port + src_portlen}); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v1: Finished, " << (rb_.pos() - first) + << " bytes read"; + } + + auto config = get_config(); + auto &fwdconf = config->http.forwarded; + + if ((fwdconf.params & FORWARDED_FOR) && + fwdconf.for_node_type == ForwardedNode::IP) { + init_forwarded_for(family, ipaddr_); + } + + return on_proxy_protocol_finish(); +} + +int ClientHandler::proxy_protocol_v2_read() { + // Assume that first str_size(PROXY_PROTO_V2_SIG) octets match v2 + // protocol signature and followed by the bytes which indicates v2. + assert(rb_.rleft() >= PROXY_PROTO_V2_HDLEN); + + auto p = rb_.pos() + str_size(PROXY_PROTO_V2_SIG); + + assert(((*p) & 0xf0) == 0x20); + + enum { LOCAL, PROXY } cmd; + + auto cmd_bits = (*p++) & 0xf; + switch (cmd_bits) { + case 0x0: + cmd = LOCAL; + break; + case 0x01: + cmd = PROXY; + break; + default: + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Unknown command " << log::hex + << cmd_bits; + } + return -1; + } + + auto fam = *p++; + uint16_t len; + memcpy(&len, p, sizeof(len)); + len = ntohs(len); + + p += sizeof(len); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Detected family=" << log::hex << fam + << ", len=" << log::dec << len; + } + + if (rb_.last() - p < len) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) + << "PROXY-protocol-v2: Prematurely truncated header block; require " + << len << " bytes, " << rb_.last() - p << " bytes left"; + } + return -1; + } + + int family; + std::array<char, std::max(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)> src_addr, + dst_addr; + size_t addrlen; + + switch (fam) { + case 0x11: + case 0x12: + if (len < 12) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Too short AF_INET addresses"; + } + return -1; + } + family = AF_INET; + addrlen = 4; + break; + case 0x21: + case 0x22: + if (len < 36) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Too short AF_INET6 addresses"; + } + return -1; + } + family = AF_INET6; + addrlen = 16; + break; + case 0x31: + case 0x32: + if (len < 216) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Too short AF_UNIX addresses"; + } + return -1; + } + // fall through + case 0x00: { + // UNSPEC and UNIX are just ignored. + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Ignore combination of address " + "family and protocol " + << log::hex << fam; + } + rb_.drain(PROXY_PROTO_V2_HDLEN + len); + return on_proxy_protocol_finish(); + } + default: + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Unknown combination of address " + "family and protocol " + << log::hex << fam; + } + return -1; + } + + if (cmd != PROXY) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Ignore non-PROXY command"; + } + rb_.drain(PROXY_PROTO_V2_HDLEN + len); + return on_proxy_protocol_finish(); + } + + if (inet_ntop(family, p, src_addr.data(), src_addr.size()) == nullptr) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Unable to parse source address"; + } + return -1; + } + + p += addrlen; + + if (inet_ntop(family, p, dst_addr.data(), dst_addr.size()) == nullptr) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) + << "PROXY-protocol-v2: Unable to parse destination address"; + } + return -1; + } + + p += addrlen; + + uint16_t src_port; + + memcpy(&src_port, p, sizeof(src_port)); + src_port = ntohs(src_port); + + // We don't use destination port. + p += 4; + + ipaddr_ = make_string_ref(balloc_, StringRef{src_addr.data()}); + port_ = util::make_string_ref_uint(balloc_, src_port); + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, this) << "PROXY-protocol-v2: Finished reading proxy addresses, " + << p - rb_.pos() << " bytes read, " + << PROXY_PROTO_V2_HDLEN + len - (p - rb_.pos()) + << " bytes left"; + } + + auto config = get_config(); + auto &fwdconf = config->http.forwarded; + + if ((fwdconf.params & FORWARDED_FOR) && + fwdconf.for_node_type == ForwardedNode::IP) { + init_forwarded_for(family, ipaddr_); + } + + rb_.drain(PROXY_PROTO_V2_HDLEN + len); + return on_proxy_protocol_finish(); +} + +StringRef ClientHandler::get_forwarded_by() const { + auto &fwdconf = get_config()->http.forwarded; + + if (fwdconf.by_node_type == ForwardedNode::OBFUSCATED) { + return fwdconf.by_obfuscated; + } + + return faddr_->hostport; +} + +StringRef ClientHandler::get_forwarded_for() const { return forwarded_for_; } + +const UpstreamAddr *ClientHandler::get_upstream_addr() const { return faddr_; } + +Connection *ClientHandler::get_connection() { return &conn_; }; + +void ClientHandler::set_tls_sni(const StringRef &sni) { + sni_ = make_string_ref(balloc_, sni); +} + +StringRef ClientHandler::get_tls_sni() const { return sni_; } + +StringRef ClientHandler::get_alpn() const { return alpn_; } + +BlockAllocator &ClientHandler::get_block_allocator() { return balloc_; } + +void ClientHandler::set_alpn_from_conn() { + const unsigned char *alpn; + unsigned int alpnlen; + + SSL_get0_alpn_selected(conn_.tls.ssl, &alpn, &alpnlen); + + alpn_ = make_string_ref(balloc_, StringRef{alpn, alpnlen}); +} + +} // namespace shrpx diff --git a/src/shrpx_client_handler.h b/src/shrpx_client_handler.h new file mode 100644 index 0000000..511dd91 --- /dev/null +++ b/src/shrpx_client_handler.h @@ -0,0 +1,236 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_CLIENT_HANDLER_H +#define SHRPX_CLIENT_HANDLER_H + +#include "shrpx.h" + +#include <memory> + +#include <ev.h> + +#include <openssl/ssl.h> + +#include "shrpx_rate_limit.h" +#include "shrpx_connection.h" +#include "buffer.h" +#include "memchunk.h" +#include "allocator.h" + +using namespace nghttp2; + +namespace shrpx { + +class Upstream; +class DownstreamConnection; +class HttpsUpstream; +class ConnectBlocker; +class DownstreamConnectionPool; +class Worker; +class Downstream; +struct WorkerStat; +struct DownstreamAddrGroup; +struct SharedDownstreamAddr; +struct DownstreamAddr; +#ifdef ENABLE_HTTP3 +class Http3Upstream; +#endif // ENABLE_HTTP3 + +class ClientHandler { +public: + ClientHandler(Worker *worker, int fd, SSL *ssl, const StringRef &ipaddr, + const StringRef &port, int family, const UpstreamAddr *faddr); + ~ClientHandler(); + + int noop(); + // Performs clear text I/O + int read_clear(); + int write_clear(); + // Specialized for PROXY-protocol use; peek data from socket. + int proxy_protocol_peek_clear(); + // Performs TLS handshake + int tls_handshake(); + // Performs TLS I/O + int read_tls(); + int write_tls(); + + int upstream_noop(); + int upstream_read(); + int upstream_http2_connhd_read(); + int upstream_http1_connhd_read(); + int upstream_write(); + + int proxy_protocol_read(); + int proxy_protocol_v2_read(); + int on_proxy_protocol_finish(); + + // Performs I/O operation. Internally calls on_read()/on_write(). + int do_read(); + int do_write(); + + // Processes buffers. No underlying I/O operation will be done. + int on_read(); + int on_write(); + + struct ev_loop *get_loop() const; + void reset_upstream_read_timeout(ev_tstamp t); + void reset_upstream_write_timeout(ev_tstamp t); + + int validate_next_proto(); + const StringRef &get_ipaddr() const; + bool get_should_close_after_write() const; + void set_should_close_after_write(bool f); + Upstream *get_upstream(); + + void pool_downstream_connection(std::unique_ptr<DownstreamConnection> dconn); + void remove_downstream_connection(DownstreamConnection *dconn); + DownstreamAddr *get_downstream_addr(int &err, DownstreamAddrGroup *group, + Downstream *downstream); + // Returns DownstreamConnection object based on request path. This + // function returns non-null DownstreamConnection, and assigns 0 to + // |err| if it succeeds, or returns nullptr, and assigns negative + // error code to |err|. + std::unique_ptr<DownstreamConnection> + get_downstream_connection(int &err, Downstream *downstream); + MemchunkPool *get_mcpool(); + SSL *get_ssl() const; + // Call this function when HTTP/2 connection header is received at + // the start of the connection. + void direct_http2_upgrade(); + // Performs HTTP/2 Upgrade from the connection managed by + // |http|. If this function fails, the connection must be + // terminated. This function returns 0 if it succeeds, or -1. + int perform_http2_upgrade(HttpsUpstream *http); + bool get_http2_upgrade_allowed() const; + // Returns upstream scheme, either "http" or "https" + StringRef get_upstream_scheme() const; + void start_immediate_shutdown(); + + // Writes upstream accesslog using |downstream|. The |downstream| + // must not be nullptr. + void write_accesslog(Downstream *downstream); + + Worker *get_worker() const; + + // Initializes forwarded_for_. + void init_forwarded_for(int family, const StringRef &ipaddr); + + using ReadBuf = DefaultMemchunkBuffer; + + ReadBuf *get_rb(); + + RateLimit *get_rlimit(); + RateLimit *get_wlimit(); + + void signal_write(); + ev_io *get_wev(); + + void setup_upstream_io_callback(); + +#ifdef ENABLE_HTTP3 + void setup_http3_upstream(std::unique_ptr<Http3Upstream> &&upstream); + int read_quic(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen); + int write_quic(); +#endif // ENABLE_HTTP3 + + // Returns string suitable for use in "by" parameter of Forwarded + // header field. + StringRef get_forwarded_by() const; + // Returns string suitable for use in "for" parameter of Forwarded + // header field. + StringRef get_forwarded_for() const; + + Http2Session * + get_http2_session(const std::shared_ptr<DownstreamAddrGroup> &group, + DownstreamAddr *addr); + + // Returns an affinity cookie value for |downstream|. |cookie_name| + // is used to inspect cookie header field in request header fields. + uint32_t get_affinity_cookie(Downstream *downstream, + const StringRef &cookie_name); + + DownstreamAddr *get_downstream_addr_strict_affinity( + int &err, const std::shared_ptr<SharedDownstreamAddr> &shared_addr, + Downstream *downstream); + + const UpstreamAddr *get_upstream_addr() const; + + void repeat_read_timer(); + void stop_read_timer(); + + Connection *get_connection(); + + // Stores |sni| which is TLS SNI extension value client sent in this + // connection. + void set_tls_sni(const StringRef &sni); + // Returns TLS SNI extension value client sent in this connection. + StringRef get_tls_sni() const; + + // Returns ALPN negotiated in this connection. + StringRef get_alpn() const; + + BlockAllocator &get_block_allocator(); + + void set_alpn_from_conn(); + +private: + // Allocator to allocate memory for connection-wide objects. Make + // sure that the allocations must be bounded, and not proportional + // to the number of requests. + BlockAllocator balloc_; + DefaultMemchunkBuffer rb_; + Connection conn_; + ev_timer reneg_shutdown_timer_; + std::unique_ptr<Upstream> upstream_; + // IP address of client. If UNIX domain socket is used, this is + // "localhost". + StringRef ipaddr_; + StringRef port_; + // The ALPN identifier negotiated for this connection. + StringRef alpn_; + // The client address used in "for" parameter of Forwarded header + // field. + StringRef forwarded_for_; + // lowercased TLS SNI which client sent. + StringRef sni_; + std::function<int(ClientHandler &)> read_, write_; + std::function<int(ClientHandler &)> on_read_, on_write_; + // Address of frontend listening socket + const UpstreamAddr *faddr_; + Worker *worker_; + // The number of bytes of HTTP/2 client connection header to read + size_t left_connhd_len_; + // hash for session affinity using client IP + uint32_t affinity_hash_; + bool should_close_after_write_; + // true if affinity_hash_ is computed + bool affinity_hash_computed_; +}; + +} // namespace shrpx + +#endif // SHRPX_CLIENT_HANDLER_H diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc new file mode 100644 index 0000000..d6d0d07 --- /dev/null +++ b/src/shrpx_config.cc @@ -0,0 +1,4694 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_config.h" + +#ifdef HAVE_PWD_H +# include <pwd.h> +#endif // HAVE_PWD_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H +#ifdef HAVE_SYSLOG_H +# include <syslog.h> +#endif // HAVE_SYSLOG_H +#include <sys/types.h> +#include <sys/stat.h> +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <dirent.h> + +#include <cstring> +#include <cerrno> +#include <limits> +#include <fstream> +#include <unordered_map> + +#include <nghttp2/nghttp2.h> + +#include "url-parser/url_parser.h" + +#include "shrpx_log.h" +#include "shrpx_tls.h" +#include "shrpx_http.h" +#ifdef HAVE_MRUBY +# include "shrpx_mruby.h" +#endif // HAVE_MRUBY +#include "util.h" +#include "base64.h" +#include "ssl_compat.h" +#include "xsi_strerror.h" + +namespace shrpx { + +namespace { +Config *config; +} // namespace + +constexpr auto SHRPX_UNIX_PATH_PREFIX = StringRef::from_lit("unix:"); + +const Config *get_config() { return config; } + +Config *mod_config() { return config; } + +std::unique_ptr<Config> replace_config(std::unique_ptr<Config> another) { + auto p = config; + config = another.release(); + return std::unique_ptr<Config>(p); +} + +void create_config() { config = new Config(); } + +Config::~Config() { + auto &upstreamconf = http2.upstream; + + nghttp2_option_del(upstreamconf.option); + nghttp2_option_del(upstreamconf.alt_mode_option); + nghttp2_session_callbacks_del(upstreamconf.callbacks); + + auto &downstreamconf = http2.downstream; + + nghttp2_option_del(downstreamconf.option); + nghttp2_session_callbacks_del(downstreamconf.callbacks); + + auto &dumpconf = http2.upstream.debug.dump; + + if (dumpconf.request_header) { + fclose(dumpconf.request_header); + } + + if (dumpconf.response_header) { + fclose(dumpconf.response_header); + } +} + +TicketKeys::~TicketKeys() { + /* Erase keys from memory */ + for (auto &key : keys) { + memset(&key, 0, sizeof(key)); + } +} + +namespace { +int split_host_port(char *host, size_t hostlen, uint16_t *port_ptr, + const StringRef &hostport, const StringRef &opt) { + // host and port in |hostport| is separated by single ','. + auto sep = std::find(std::begin(hostport), std::end(hostport), ','); + if (sep == std::end(hostport)) { + LOG(ERROR) << opt << ": Invalid host, port: " << hostport; + return -1; + } + size_t len = sep - std::begin(hostport); + if (hostlen < len + 1) { + LOG(ERROR) << opt << ": Hostname too long: " << hostport; + return -1; + } + std::copy(std::begin(hostport), sep, host); + host[len] = '\0'; + + auto portstr = StringRef{sep + 1, std::end(hostport)}; + auto d = util::parse_uint(portstr); + if (1 <= d && d <= std::numeric_limits<uint16_t>::max()) { + *port_ptr = d; + return 0; + } + + LOG(ERROR) << opt << ": Port is invalid: " << portstr; + return -1; +} +} // namespace + +namespace { +bool is_secure(const StringRef &filename) { + struct stat buf; + int rv = stat(filename.c_str(), &buf); + if (rv == 0) { + if ((buf.st_mode & S_IRWXU) && !(buf.st_mode & S_IRWXG) && + !(buf.st_mode & S_IRWXO)) { + return true; + } + } + + return false; +} +} // namespace + +std::unique_ptr<TicketKeys> +read_tls_ticket_key_file(const std::vector<StringRef> &files, + const EVP_CIPHER *cipher, const EVP_MD *hmac) { + auto ticket_keys = std::make_unique<TicketKeys>(); + auto &keys = ticket_keys->keys; + keys.resize(files.size()); + auto enc_keylen = EVP_CIPHER_key_length(cipher); + auto hmac_keylen = EVP_MD_size(hmac); + if (cipher == EVP_aes_128_cbc()) { + // backward compatibility, as a legacy of using same file format + // with nginx and apache. + hmac_keylen = 16; + } + auto expectedlen = keys[0].data.name.size() + enc_keylen + hmac_keylen; + char buf[256]; + assert(sizeof(buf) >= expectedlen); + + size_t i = 0; + for (auto &file : files) { + struct stat fst {}; + + if (stat(file.c_str(), &fst) == -1) { + auto error = errno; + LOG(ERROR) << "tls-ticket-key-file: could not stat file " << file + << ", errno=" << error; + return nullptr; + } + + if (static_cast<size_t>(fst.st_size) != expectedlen) { + LOG(ERROR) << "tls-ticket-key-file: the expected file size is " + << expectedlen << ", the actual file size is " << fst.st_size; + return nullptr; + } + + std::ifstream f(file.c_str()); + if (!f) { + LOG(ERROR) << "tls-ticket-key-file: could not open file " << file; + return nullptr; + } + + f.read(buf, expectedlen); + if (static_cast<size_t>(f.gcount()) != expectedlen) { + LOG(ERROR) << "tls-ticket-key-file: want to read " << expectedlen + << " bytes but only read " << f.gcount() << " bytes from " + << file; + return nullptr; + } + + auto &key = keys[i++]; + key.cipher = cipher; + key.hmac = hmac; + key.hmac_keylen = hmac_keylen; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "enc_keylen=" << enc_keylen + << ", hmac_keylen=" << key.hmac_keylen; + } + + auto p = buf; + std::copy_n(p, key.data.name.size(), std::begin(key.data.name)); + p += key.data.name.size(); + std::copy_n(p, enc_keylen, std::begin(key.data.enc_key)); + p += enc_keylen; + std::copy_n(p, hmac_keylen, std::begin(key.data.hmac_key)); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "session ticket key: " << util::format_hex(key.data.name); + } + } + return ticket_keys; +} + +#ifdef ENABLE_HTTP3 +std::shared_ptr<QUICKeyingMaterials> +read_quic_secret_file(const StringRef &path) { + constexpr size_t expectedlen = + SHRPX_QUIC_SECRET_RESERVEDLEN + SHRPX_QUIC_SECRETLEN + SHRPX_QUIC_SALTLEN; + + auto qkms = std::make_shared<QUICKeyingMaterials>(); + auto &kms = qkms->keying_materials; + + std::ifstream f(path.c_str()); + if (!f) { + LOG(ERROR) << "frontend-quic-secret-file: could not open file " << path; + return nullptr; + } + + std::array<char, 4096> buf; + + while (f.getline(buf.data(), buf.size())) { + auto len = strlen(buf.data()); + if (len == 0 || buf[0] == '#') { + continue; + } + + auto s = StringRef{std::begin(buf), std::begin(buf) + len}; + if (s.size() != expectedlen * 2 || !util::is_hex_string(s)) { + LOG(ERROR) << "frontend-quic-secret-file: each line must be a " + << expectedlen * 2 << " bytes hex encoded string"; + return nullptr; + } + + kms.emplace_back(); + auto &qkm = kms.back(); + + auto p = std::begin(s); + + util::decode_hex(std::begin(qkm.reserved), + StringRef{p, p + qkm.reserved.size()}); + p += qkm.reserved.size() * 2; + util::decode_hex(std::begin(qkm.secret), + StringRef{p, p + qkm.secret.size()}); + p += qkm.secret.size() * 2; + util::decode_hex(std::begin(qkm.salt), StringRef{p, p + qkm.salt.size()}); + p += qkm.salt.size() * 2; + + assert(static_cast<size_t>(p - std::begin(s)) == expectedlen * 2); + + qkm.id = qkm.reserved[0] & 0xc0; + + if (kms.size() == 4) { + break; + } + } + + if (f.bad() || (!f.eof() && f.fail())) { + LOG(ERROR) + << "frontend-quic-secret-file: error occurred while reading file " + << path; + return nullptr; + } + + if (kms.empty()) { + LOG(WARN) + << "frontend-quic-secret-file: no keying materials are present in file " + << path; + return nullptr; + } + + return qkms; +} +#endif // ENABLE_HTTP3 + +FILE *open_file_for_write(const char *filename) { + std::array<char, STRERROR_BUFSIZE> errbuf; + +#ifdef O_CLOEXEC + auto fd = open(filename, O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR); +#else + auto fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + + // We get race condition if execve is called at the same time. + if (fd != -1) { + util::make_socket_closeonexec(fd); + } +#endif + if (fd == -1) { + auto error = errno; + LOG(ERROR) << "Failed to open " << filename << " for writing. Cause: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return nullptr; + } + auto f = fdopen(fd, "wb"); + if (f == nullptr) { + auto error = errno; + LOG(ERROR) << "Failed to open " << filename << " for writing. Cause: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return nullptr; + } + + return f; +} + +namespace { +// Read passwd from |filename| +std::string read_passwd_from_file(const StringRef &opt, + const StringRef &filename) { + std::string line; + + if (!is_secure(filename)) { + LOG(ERROR) << opt << ": Private key passwd file " << filename + << " has insecure mode."; + return line; + } + + std::ifstream in(filename.c_str(), std::ios::binary); + if (!in) { + LOG(ERROR) << opt << ": Could not open key passwd file " << filename; + return line; + } + + std::getline(in, line); + return line; +} +} // namespace + +HeaderRefs::value_type parse_header(BlockAllocator &balloc, + const StringRef &optarg) { + auto colon = std::find(std::begin(optarg), std::end(optarg), ':'); + + if (colon == std::end(optarg) || colon == std::begin(optarg)) { + return {}; + } + + auto value = colon + 1; + for (; *value == '\t' || *value == ' '; ++value) + ; + + auto name_iov = + make_byte_ref(balloc, std::distance(std::begin(optarg), colon) + 1); + auto p = name_iov.base; + p = std::copy(std::begin(optarg), colon, p); + util::inp_strlower(name_iov.base, p); + *p = '\0'; + + auto nv = + HeaderRef(StringRef{name_iov.base, p}, + make_string_ref(balloc, StringRef{value, std::end(optarg)})); + + if (!nghttp2_check_header_name(nv.name.byte(), nv.name.size()) || + !nghttp2_check_header_value_rfc9113(nv.value.byte(), nv.value.size())) { + return {}; + } + + return nv; +} + +template <typename T> +int parse_uint(T *dest, const StringRef &opt, const StringRef &optarg) { + auto val = util::parse_uint(optarg); + if (val == -1) { + LOG(ERROR) << opt << ": bad value. Specify an integer >= 0."; + return -1; + } + + *dest = val; + + return 0; +} + +namespace { +template <typename T> +int parse_uint_with_unit(T *dest, const StringRef &opt, + const StringRef &optarg) { + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + LOG(ERROR) << opt << ": bad value: '" << optarg << "'"; + return -1; + } + + if (static_cast<uint64_t>(std::numeric_limits<T>::max()) < + static_cast<uint64_t>(n)) { + LOG(ERROR) << opt + << ": too large. The value should be less than or equal to " + << std::numeric_limits<T>::max(); + return -1; + } + + *dest = n; + + return 0; +} +} // namespace + +namespace { +int parse_altsvc(AltSvc &altsvc, const StringRef &opt, + const StringRef &optarg) { + // PROTOID, PORT, HOST, ORIGIN, PARAMS. + auto tokens = util::split_str(optarg, ',', 5); + + if (tokens.size() < 2) { + // Requires at least protocol_id and port + LOG(ERROR) << opt << ": too few parameters: " << optarg; + return -1; + } + + int port; + + if (parse_uint(&port, opt, tokens[1]) != 0) { + return -1; + } + + if (port < 1 || + port > static_cast<int>(std::numeric_limits<uint16_t>::max())) { + LOG(ERROR) << opt << ": port is invalid: " << tokens[1]; + return -1; + } + + altsvc.protocol_id = make_string_ref(config->balloc, tokens[0]); + + altsvc.port = port; + altsvc.service = make_string_ref(config->balloc, tokens[1]); + + if (tokens.size() > 2) { + if (!tokens[2].empty()) { + altsvc.host = make_string_ref(config->balloc, tokens[2]); + } + + if (tokens.size() > 3) { + if (!tokens[3].empty()) { + altsvc.origin = make_string_ref(config->balloc, tokens[3]); + } + + if (tokens.size() > 4) { + if (!tokens[4].empty()) { + altsvc.params = make_string_ref(config->balloc, tokens[4]); + } + } + } + } + + return 0; +} +} // namespace + +namespace { +// generated by gennghttpxfun.py +LogFragmentType log_var_lookup_token(const char *name, size_t namelen) { + switch (namelen) { + case 3: + switch (name[2]) { + case 'd': + if (util::strieq_l("pi", name, 2)) { + return LogFragmentType::PID; + } + break; + } + break; + case 4: + switch (name[3]) { + case 'h': + if (util::strieq_l("pat", name, 3)) { + return LogFragmentType::PATH; + } + break; + case 'n': + if (util::strieq_l("alp", name, 3)) { + return LogFragmentType::ALPN; + } + break; + } + break; + case 6: + switch (name[5]) { + case 'd': + if (util::strieq_l("metho", name, 5)) { + return LogFragmentType::METHOD; + } + break; + case 's': + if (util::strieq_l("statu", name, 5)) { + return LogFragmentType::STATUS; + } + break; + } + break; + case 7: + switch (name[6]) { + case 'i': + if (util::strieq_l("tls_sn", name, 6)) { + return LogFragmentType::TLS_SNI; + } + break; + case 't': + if (util::strieq_l("reques", name, 6)) { + return LogFragmentType::REQUEST; + } + break; + } + break; + case 10: + switch (name[9]) { + case 'l': + if (util::strieq_l("time_loca", name, 9)) { + return LogFragmentType::TIME_LOCAL; + } + break; + case 'r': + if (util::strieq_l("ssl_ciphe", name, 9)) { + return LogFragmentType::SSL_CIPHER; + } + if (util::strieq_l("tls_ciphe", name, 9)) { + return LogFragmentType::TLS_CIPHER; + } + break; + } + break; + case 11: + switch (name[10]) { + case 'r': + if (util::strieq_l("remote_add", name, 10)) { + return LogFragmentType::REMOTE_ADDR; + } + break; + case 't': + if (util::strieq_l("remote_por", name, 10)) { + return LogFragmentType::REMOTE_PORT; + } + if (util::strieq_l("server_por", name, 10)) { + return LogFragmentType::SERVER_PORT; + } + break; + } + break; + case 12: + switch (name[11]) { + case '1': + if (util::strieq_l("time_iso860", name, 11)) { + return LogFragmentType::TIME_ISO8601; + } + break; + case 'e': + if (util::strieq_l("request_tim", name, 11)) { + return LogFragmentType::REQUEST_TIME; + } + break; + case 'l': + if (util::strieq_l("ssl_protoco", name, 11)) { + return LogFragmentType::SSL_PROTOCOL; + } + if (util::strieq_l("tls_protoco", name, 11)) { + return LogFragmentType::TLS_PROTOCOL; + } + break; + case 't': + if (util::strieq_l("backend_hos", name, 11)) { + return LogFragmentType::BACKEND_HOST; + } + if (util::strieq_l("backend_por", name, 11)) { + return LogFragmentType::BACKEND_PORT; + } + break; + } + break; + case 14: + switch (name[13]) { + case 'd': + if (util::strieq_l("ssl_session_i", name, 13)) { + return LogFragmentType::SSL_SESSION_ID; + } + if (util::strieq_l("tls_session_i", name, 13)) { + return LogFragmentType::TLS_SESSION_ID; + } + break; + } + break; + case 15: + switch (name[14]) { + case 't': + if (util::strieq_l("body_bytes_sen", name, 14)) { + return LogFragmentType::BODY_BYTES_SENT; + } + break; + } + break; + case 16: + switch (name[15]) { + case 'n': + if (util::strieq_l("protocol_versio", name, 15)) { + return LogFragmentType::PROTOCOL_VERSION; + } + break; + } + break; + case 17: + switch (name[16]) { + case 'l': + if (util::strieq_l("tls_client_seria", name, 16)) { + return LogFragmentType::TLS_CLIENT_SERIAL; + } + break; + } + break; + case 18: + switch (name[17]) { + case 'd': + if (util::strieq_l("ssl_session_reuse", name, 17)) { + return LogFragmentType::SSL_SESSION_REUSED; + } + if (util::strieq_l("tls_session_reuse", name, 17)) { + return LogFragmentType::TLS_SESSION_REUSED; + } + break; + case 'y': + if (util::strieq_l("path_without_quer", name, 17)) { + return LogFragmentType::PATH_WITHOUT_QUERY; + } + break; + } + break; + case 22: + switch (name[21]) { + case 'e': + if (util::strieq_l("tls_client_issuer_nam", name, 21)) { + return LogFragmentType::TLS_CLIENT_ISSUER_NAME; + } + break; + } + break; + case 23: + switch (name[22]) { + case 'e': + if (util::strieq_l("tls_client_subject_nam", name, 22)) { + return LogFragmentType::TLS_CLIENT_SUBJECT_NAME; + } + break; + } + break; + case 27: + switch (name[26]) { + case '1': + if (util::strieq_l("tls_client_fingerprint_sha", name, 26)) { + return LogFragmentType::TLS_CLIENT_FINGERPRINT_SHA1; + } + break; + } + break; + case 29: + switch (name[28]) { + case '6': + if (util::strieq_l("tls_client_fingerprint_sha25", name, 28)) { + return LogFragmentType::TLS_CLIENT_FINGERPRINT_SHA256; + } + break; + } + break; + } + return LogFragmentType::NONE; +} +} // namespace + +namespace { +bool var_token(char c) { + return util::is_alpha(c) || util::is_digit(c) || c == '_'; +} +} // namespace + +std::vector<LogFragment> parse_log_format(BlockAllocator &balloc, + const StringRef &optarg) { + auto literal_start = std::begin(optarg); + auto p = literal_start; + auto eop = std::end(optarg); + + auto res = std::vector<LogFragment>(); + + for (; p != eop;) { + if (*p != '$') { + ++p; + continue; + } + + auto var_start = p; + + ++p; + + const char *var_name; + size_t var_namelen; + if (p != eop && *p == '{') { + var_name = ++p; + for (; p != eop && var_token(*p); ++p) + ; + + if (p == eop || *p != '}') { + LOG(WARN) << "Missing '}' after " << StringRef{var_start, p}; + continue; + } + + var_namelen = p - var_name; + ++p; + } else { + var_name = p; + for (; p != eop && var_token(*p); ++p) + ; + + var_namelen = p - var_name; + } + + const char *value = nullptr; + + auto type = log_var_lookup_token(var_name, var_namelen); + + if (type == LogFragmentType::NONE) { + if (util::istarts_with_l(StringRef{var_name, var_namelen}, "http_")) { + if (util::streq_l("host", StringRef{var_name + str_size("http_"), + var_namelen - str_size("http_")})) { + // Special handling of host header field. We will use + // :authority header field if host header is missing. This + // is a typical case in HTTP/2. + type = LogFragmentType::AUTHORITY; + } else { + type = LogFragmentType::HTTP; + value = var_name + str_size("http_"); + } + } else { + LOG(WARN) << "Unrecognized log format variable: " + << StringRef{var_name, var_namelen}; + continue; + } + } + + if (literal_start < var_start) { + res.emplace_back( + LogFragmentType::LITERAL, + make_string_ref(balloc, StringRef{literal_start, var_start})); + } + + literal_start = p; + + if (value == nullptr) { + res.emplace_back(type); + continue; + } + + { + auto iov = make_byte_ref( + balloc, std::distance(value, var_name + var_namelen) + 1); + auto p = iov.base; + p = std::copy(value, var_name + var_namelen, p); + for (auto cp = iov.base; cp != p; ++cp) { + if (*cp == '_') { + *cp = '-'; + } + } + *p = '\0'; + res.emplace_back(type, StringRef{iov.base, p}); + } + } + + if (literal_start != eop) { + res.emplace_back(LogFragmentType::LITERAL, + make_string_ref(balloc, StringRef{literal_start, eop})); + } + + return res; +} + +namespace { +int parse_address_family(int *dest, const StringRef &opt, + const StringRef &optarg) { + if (util::strieq_l("auto", optarg)) { + *dest = AF_UNSPEC; + return 0; + } + if (util::strieq_l("IPv4", optarg)) { + *dest = AF_INET; + return 0; + } + if (util::strieq_l("IPv6", optarg)) { + *dest = AF_INET6; + return 0; + } + + LOG(ERROR) << opt << ": bad value: '" << optarg << "'"; + return -1; +} +} // namespace + +namespace { +int parse_duration(ev_tstamp *dest, const StringRef &opt, + const StringRef &optarg) { + auto t = util::parse_duration_with_unit(optarg); + if (t == std::numeric_limits<double>::infinity()) { + LOG(ERROR) << opt << ": bad value: '" << optarg << "'"; + return -1; + } + + *dest = t; + + return 0; +} +} // namespace + +namespace { +int parse_tls_proto_version(int &dest, const StringRef &opt, + const StringRef &optarg) { + auto v = tls::proto_version_from_string(optarg); + if (v == -1) { + LOG(ERROR) << opt << ": invalid TLS protocol version: " << optarg; + return -1; + } + + dest = v; + + return 0; +} +} // namespace + +struct MemcachedConnectionParams { + bool tls; +}; + +namespace { +// Parses memcached connection configuration parameter |src_params|, +// and stores parsed results into |out|. This function returns 0 if +// it succeeds, or -1. +int parse_memcached_connection_params(MemcachedConnectionParams &out, + const StringRef &src_params, + const StringRef &opt) { + auto last = std::end(src_params); + for (auto first = std::begin(src_params); first != last;) { + auto end = std::find(first, last, ';'); + auto param = StringRef{first, end}; + + if (util::strieq_l("tls", param)) { + out.tls = true; + } else if (util::strieq_l("no-tls", param)) { + out.tls = false; + } else if (!param.empty()) { + LOG(ERROR) << opt << ": " << param << ": unknown keyword"; + return -1; + } + + if (end == last) { + break; + } + + first = end + 1; + } + + return 0; +} +} // namespace + +struct UpstreamParams { + UpstreamAltMode alt_mode; + bool tls; + bool sni_fwd; + bool proxyproto; + bool quic; +}; + +namespace { +// Parses upstream configuration parameter |src_params|, and stores +// parsed results into |out|. This function returns 0 if it succeeds, +// or -1. +int parse_upstream_params(UpstreamParams &out, const StringRef &src_params) { + auto last = std::end(src_params); + for (auto first = std::begin(src_params); first != last;) { + auto end = std::find(first, last, ';'); + auto param = StringRef{first, end}; + + if (util::strieq_l("tls", param)) { + out.tls = true; + } else if (util::strieq_l("sni-fwd", param)) { + out.sni_fwd = true; + } else if (util::strieq_l("no-tls", param)) { + out.tls = false; + } else if (util::strieq_l("api", param)) { + if (out.alt_mode != UpstreamAltMode::NONE && + out.alt_mode != UpstreamAltMode::API) { + LOG(ERROR) << "frontend: api and healthmon are mutually exclusive"; + return -1; + } + out.alt_mode = UpstreamAltMode::API; + } else if (util::strieq_l("healthmon", param)) { + if (out.alt_mode != UpstreamAltMode::NONE && + out.alt_mode != UpstreamAltMode::HEALTHMON) { + LOG(ERROR) << "frontend: api and healthmon are mutually exclusive"; + return -1; + } + out.alt_mode = UpstreamAltMode::HEALTHMON; + } else if (util::strieq_l("proxyproto", param)) { + out.proxyproto = true; + } else if (util::strieq_l("quic", param)) { +#ifdef ENABLE_HTTP3 + out.quic = true; +#else // !ENABLE_HTTP3 + LOG(ERROR) << "quic: QUIC is disabled at compile time"; + return -1; +#endif // !ENABLE_HTTP3 + } else if (!param.empty()) { + LOG(ERROR) << "frontend: " << param << ": unknown keyword"; + return -1; + } + + if (end == last) { + break; + } + + first = end + 1; + } + + return 0; +} +} // namespace + +struct DownstreamParams { + StringRef sni; + StringRef mruby; + StringRef group; + AffinityConfig affinity; + ev_tstamp read_timeout; + ev_tstamp write_timeout; + size_t fall; + size_t rise; + uint32_t weight; + uint32_t group_weight; + Proto proto; + bool tls; + bool dns; + bool redirect_if_not_tls; + bool upgrade_scheme; + bool dnf; +}; + +namespace { +// Parses |value| of parameter named |name| as duration. This +// function returns 0 if it succeeds and the parsed value is assigned +// to |dest|, or -1. +int parse_downstream_param_duration(ev_tstamp &dest, const StringRef &name, + const StringRef &value) { + auto t = util::parse_duration_with_unit(value); + if (t == std::numeric_limits<double>::infinity()) { + LOG(ERROR) << "backend: " << name << ": bad value: '" << value << "'"; + return -1; + } + dest = t; + return 0; +} +} // namespace + +namespace { +// Parses downstream configuration parameter |src_params|, and stores +// parsed results into |out|. This function returns 0 if it succeeds, +// or -1. +int parse_downstream_params(DownstreamParams &out, + const StringRef &src_params) { + auto last = std::end(src_params); + for (auto first = std::begin(src_params); first != last;) { + auto end = std::find(first, last, ';'); + auto param = StringRef{first, end}; + + if (util::istarts_with_l(param, "proto=")) { + auto protostr = StringRef{first + str_size("proto="), end}; + if (protostr.empty()) { + LOG(ERROR) << "backend: proto: protocol is empty"; + return -1; + } + + if (util::streq_l("h2", std::begin(protostr), protostr.size())) { + out.proto = Proto::HTTP2; + } else if (util::streq_l("http/1.1", std::begin(protostr), + protostr.size())) { + out.proto = Proto::HTTP1; + } else { + LOG(ERROR) << "backend: proto: unknown protocol " << protostr; + return -1; + } + } else if (util::istarts_with_l(param, "fall=")) { + auto valstr = StringRef{first + str_size("fall="), end}; + if (valstr.empty()) { + LOG(ERROR) << "backend: fall: non-negative integer is expected"; + return -1; + } + + auto n = util::parse_uint(valstr); + if (n == -1) { + LOG(ERROR) << "backend: fall: non-negative integer is expected"; + return -1; + } + + out.fall = n; + } else if (util::istarts_with_l(param, "rise=")) { + auto valstr = StringRef{first + str_size("rise="), end}; + if (valstr.empty()) { + LOG(ERROR) << "backend: rise: non-negative integer is expected"; + return -1; + } + + auto n = util::parse_uint(valstr); + if (n == -1) { + LOG(ERROR) << "backend: rise: non-negative integer is expected"; + return -1; + } + + out.rise = n; + } else if (util::strieq_l("tls", param)) { + out.tls = true; + } else if (util::strieq_l("no-tls", param)) { + out.tls = false; + } else if (util::istarts_with_l(param, "sni=")) { + out.sni = StringRef{first + str_size("sni="), end}; + } else if (util::istarts_with_l(param, "affinity=")) { + auto valstr = StringRef{first + str_size("affinity="), end}; + if (util::strieq_l("none", valstr)) { + out.affinity.type = SessionAffinity::NONE; + } else if (util::strieq_l("ip", valstr)) { + out.affinity.type = SessionAffinity::IP; + } else if (util::strieq_l("cookie", valstr)) { + out.affinity.type = SessionAffinity::COOKIE; + } else { + LOG(ERROR) + << "backend: affinity: value must be one of none, ip, and cookie"; + return -1; + } + } else if (util::istarts_with_l(param, "affinity-cookie-name=")) { + auto val = StringRef{first + str_size("affinity-cookie-name="), end}; + if (val.empty()) { + LOG(ERROR) + << "backend: affinity-cookie-name: non empty string is expected"; + return -1; + } + out.affinity.cookie.name = val; + } else if (util::istarts_with_l(param, "affinity-cookie-path=")) { + out.affinity.cookie.path = + StringRef{first + str_size("affinity-cookie-path="), end}; + } else if (util::istarts_with_l(param, "affinity-cookie-secure=")) { + auto valstr = StringRef{first + str_size("affinity-cookie-secure="), end}; + if (util::strieq_l("auto", valstr)) { + out.affinity.cookie.secure = SessionAffinityCookieSecure::AUTO; + } else if (util::strieq_l("yes", valstr)) { + out.affinity.cookie.secure = SessionAffinityCookieSecure::YES; + } else if (util::strieq_l("no", valstr)) { + out.affinity.cookie.secure = SessionAffinityCookieSecure::NO; + } else { + LOG(ERROR) << "backend: affinity-cookie-secure: value must be one of " + "auto, yes, and no"; + return -1; + } + } else if (util::istarts_with_l(param, "affinity-cookie-stickiness=")) { + auto valstr = + StringRef{first + str_size("affinity-cookie-stickiness="), end}; + if (util::strieq_l("loose", valstr)) { + out.affinity.cookie.stickiness = SessionAffinityCookieStickiness::LOOSE; + } else if (util::strieq_l("strict", valstr)) { + out.affinity.cookie.stickiness = + SessionAffinityCookieStickiness::STRICT; + } else { + LOG(ERROR) << "backend: affinity-cookie-stickiness: value must be " + "either loose or strict"; + return -1; + } + } else if (util::strieq_l("dns", param)) { + out.dns = true; + } else if (util::strieq_l("redirect-if-not-tls", param)) { + out.redirect_if_not_tls = true; + } else if (util::strieq_l("upgrade-scheme", param)) { + out.upgrade_scheme = true; + } else if (util::istarts_with_l(param, "mruby=")) { + auto valstr = StringRef{first + str_size("mruby="), end}; + out.mruby = valstr; + } else if (util::istarts_with_l(param, "read-timeout=")) { + if (parse_downstream_param_duration( + out.read_timeout, StringRef::from_lit("read-timeout"), + StringRef{first + str_size("read-timeout="), end}) == -1) { + return -1; + } + } else if (util::istarts_with_l(param, "write-timeout=")) { + if (parse_downstream_param_duration( + out.write_timeout, StringRef::from_lit("write-timeout"), + StringRef{first + str_size("write-timeout="), end}) == -1) { + return -1; + } + } else if (util::istarts_with_l(param, "weight=")) { + auto valstr = StringRef{first + str_size("weight="), end}; + if (valstr.empty()) { + LOG(ERROR) + << "backend: weight: non-negative integer [1, 256] is expected"; + return -1; + } + + auto n = util::parse_uint(valstr); + if (n < 1 || n > 256) { + LOG(ERROR) + << "backend: weight: non-negative integer [1, 256] is expected"; + return -1; + } + out.weight = n; + } else if (util::istarts_with_l(param, "group=")) { + auto valstr = StringRef{first + str_size("group="), end}; + if (valstr.empty()) { + LOG(ERROR) << "backend: group: empty string is not allowed"; + return -1; + } + out.group = valstr; + } else if (util::istarts_with_l(param, "group-weight=")) { + auto valstr = StringRef{first + str_size("group-weight="), end}; + if (valstr.empty()) { + LOG(ERROR) << "backend: group-weight: non-negative integer [1, 256] is " + "expected"; + return -1; + } + + auto n = util::parse_uint(valstr); + if (n < 1 || n > 256) { + LOG(ERROR) << "backend: group-weight: non-negative integer [1, 256] is " + "expected"; + return -1; + } + out.group_weight = n; + } else if (util::strieq_l("dnf", param)) { + out.dnf = true; + } else if (!param.empty()) { + LOG(ERROR) << "backend: " << param << ": unknown keyword"; + return -1; + } + + if (end == last) { + break; + } + + first = end + 1; + } + + return 0; +} +} // namespace + +namespace { +// Parses host-path mapping patterns in |src_pattern|, and stores +// mappings in config. We will store each host-path pattern found in +// |src| with |addr|. |addr| will be copied accordingly. Also we +// make a group based on the pattern. The "/" pattern is considered +// as catch-all. We also parse protocol specified in |src_proto|. +// +// This function returns 0 if it succeeds, or -1. +int parse_mapping(Config *config, DownstreamAddrConfig &addr, + std::map<StringRef, size_t> &pattern_addr_indexer, + const StringRef &src_pattern, const StringRef &src_params) { + // This returns at least 1 element (it could be empty string). We + // will append '/' to all patterns, so it becomes catch-all pattern. + auto mapping = util::split_str(src_pattern, ':'); + assert(!mapping.empty()); + auto &downstreamconf = *config->conn.downstream; + auto &addr_groups = downstreamconf.addr_groups; + + DownstreamParams params{}; + params.proto = Proto::HTTP1; + params.weight = 1; + + if (parse_downstream_params(params, src_params) != 0) { + return -1; + } + + if (addr.host_unix && params.dns) { + LOG(ERROR) << "backend: dns: cannot be used for UNIX domain socket"; + return -1; + } + + if (params.affinity.type == SessionAffinity::COOKIE && + params.affinity.cookie.name.empty()) { + LOG(ERROR) << "backend: affinity-cookie-name is mandatory if " + "affinity=cookie is specified"; + return -1; + } + + addr.fall = params.fall; + addr.rise = params.rise; + addr.weight = params.weight; + addr.group = make_string_ref(downstreamconf.balloc, params.group); + addr.group_weight = params.group_weight; + addr.proto = params.proto; + addr.tls = params.tls; + addr.sni = make_string_ref(downstreamconf.balloc, params.sni); + addr.dns = params.dns; + addr.upgrade_scheme = params.upgrade_scheme; + addr.dnf = params.dnf; + + auto &routerconf = downstreamconf.router; + auto &router = routerconf.router; + auto &rw_router = routerconf.rev_wildcard_router; + auto &wildcard_patterns = routerconf.wildcard_patterns; + + for (const auto &raw_pattern : mapping) { + StringRef pattern; + auto slash = std::find(std::begin(raw_pattern), std::end(raw_pattern), '/'); + if (slash == std::end(raw_pattern)) { + // This effectively makes empty pattern to "/". 2 for '/' and + // terminal NULL character. + auto iov = make_byte_ref(downstreamconf.balloc, raw_pattern.size() + 2); + auto p = iov.base; + p = std::copy(std::begin(raw_pattern), std::end(raw_pattern), p); + util::inp_strlower(iov.base, p); + *p++ = '/'; + *p = '\0'; + pattern = StringRef{iov.base, p}; + } else { + auto path = http2::normalize_path_colon( + downstreamconf.balloc, StringRef{slash, std::end(raw_pattern)}, + StringRef{}); + auto iov = make_byte_ref(downstreamconf.balloc, + std::distance(std::begin(raw_pattern), slash) + + path.size() + 1); + auto p = iov.base; + p = std::copy(std::begin(raw_pattern), slash, p); + util::inp_strlower(iov.base, p); + p = std::copy(std::begin(path), std::end(path), p); + *p = '\0'; + pattern = StringRef{iov.base, p}; + } + auto it = pattern_addr_indexer.find(pattern); + if (it != std::end(pattern_addr_indexer)) { + auto &g = addr_groups[(*it).second]; + // Last value wins if we have multiple different affinity + // value under one group. + if (params.affinity.type != SessionAffinity::NONE) { + if (g.affinity.type == SessionAffinity::NONE) { + g.affinity.type = params.affinity.type; + if (params.affinity.type == SessionAffinity::COOKIE) { + g.affinity.cookie.name = make_string_ref( + downstreamconf.balloc, params.affinity.cookie.name); + if (!params.affinity.cookie.path.empty()) { + g.affinity.cookie.path = make_string_ref( + downstreamconf.balloc, params.affinity.cookie.path); + } + g.affinity.cookie.secure = params.affinity.cookie.secure; + g.affinity.cookie.stickiness = params.affinity.cookie.stickiness; + } + } else if (g.affinity.type != params.affinity.type || + g.affinity.cookie.name != params.affinity.cookie.name || + g.affinity.cookie.path != params.affinity.cookie.path || + g.affinity.cookie.secure != params.affinity.cookie.secure || + g.affinity.cookie.stickiness != + params.affinity.cookie.stickiness) { + LOG(ERROR) << "backend: affinity: multiple different affinity " + "configurations found in a single group"; + return -1; + } + } + // If at least one backend requires frontend TLS connection, + // enable it for all backends sharing the same pattern. + if (params.redirect_if_not_tls) { + g.redirect_if_not_tls = true; + } + // All backends in the same group must have the same mruby path. + // If some backends do not specify mruby file, and there is at + // least one backend with mruby file, it is used for all + // backends in the group. + if (!params.mruby.empty()) { + if (g.mruby_file.empty()) { + g.mruby_file = make_string_ref(downstreamconf.balloc, params.mruby); + } else if (g.mruby_file != params.mruby) { + LOG(ERROR) << "backend: mruby: multiple different mruby file found " + "in a single group"; + return -1; + } + } + // All backends in the same group must have the same read/write + // timeout. If some backends do not specify read/write timeout, + // and there is at least one backend with read/write timeout, it + // is used for all backends in the group. + if (params.read_timeout > 1e-9) { + if (g.timeout.read < 1e-9) { + g.timeout.read = params.read_timeout; + } else if (fabs(g.timeout.read - params.read_timeout) > 1e-9) { + LOG(ERROR) + << "backend: read-timeout: multiple different read-timeout " + "found in a single group"; + return -1; + } + } + if (params.write_timeout > 1e-9) { + if (g.timeout.write < 1e-9) { + g.timeout.write = params.write_timeout; + } else if (fabs(g.timeout.write - params.write_timeout) > 1e-9) { + LOG(ERROR) << "backend: write-timeout: multiple different " + "write-timeout found in a single group"; + return -1; + } + } + // All backends in the same group must have the same dnf + // setting. If some backends do not specify dnf, and there is + // at least one backend with dnf, it is used for all backends in + // the group. In general, multiple backends are not necessary + // for dnf because there is no need for load balancing. + if (params.dnf) { + g.dnf = true; + } + + g.addrs.push_back(addr); + continue; + } + + auto idx = addr_groups.size(); + pattern_addr_indexer.emplace(pattern, idx); + addr_groups.emplace_back(pattern); + auto &g = addr_groups.back(); + g.addrs.push_back(addr); + g.affinity.type = params.affinity.type; + if (params.affinity.type == SessionAffinity::COOKIE) { + g.affinity.cookie.name = + make_string_ref(downstreamconf.balloc, params.affinity.cookie.name); + if (!params.affinity.cookie.path.empty()) { + g.affinity.cookie.path = + make_string_ref(downstreamconf.balloc, params.affinity.cookie.path); + } + g.affinity.cookie.secure = params.affinity.cookie.secure; + g.affinity.cookie.stickiness = params.affinity.cookie.stickiness; + } + g.redirect_if_not_tls = params.redirect_if_not_tls; + g.mruby_file = make_string_ref(downstreamconf.balloc, params.mruby); + g.timeout.read = params.read_timeout; + g.timeout.write = params.write_timeout; + g.dnf = params.dnf; + + if (pattern[0] == '*') { + // wildcard pattern + auto path_first = + std::find(std::begin(g.pattern), std::end(g.pattern), '/'); + + auto host = StringRef{std::begin(g.pattern) + 1, path_first}; + auto path = StringRef{path_first, std::end(g.pattern)}; + + auto path_is_wildcard = false; + if (path[path.size() - 1] == '*') { + path = StringRef{std::begin(path), std::begin(path) + path.size() - 1}; + path_is_wildcard = true; + } + + auto it = std::find_if( + std::begin(wildcard_patterns), std::end(wildcard_patterns), + [&host](const WildcardPattern &wp) { return wp.host == host; }); + + if (it == std::end(wildcard_patterns)) { + wildcard_patterns.emplace_back(host); + + auto &router = wildcard_patterns.back().router; + router.add_route(path, idx, path_is_wildcard); + + auto iov = make_byte_ref(downstreamconf.balloc, host.size() + 1); + auto p = iov.base; + p = std::reverse_copy(std::begin(host), std::end(host), p); + *p = '\0'; + auto rev_host = StringRef{iov.base, p}; + + rw_router.add_route(rev_host, wildcard_patterns.size() - 1); + } else { + (*it).router.add_route(path, idx, path_is_wildcard); + } + + continue; + } + + auto path_is_wildcard = false; + if (pattern[pattern.size() - 1] == '*') { + pattern = StringRef{std::begin(pattern), + std::begin(pattern) + pattern.size() - 1}; + path_is_wildcard = true; + } + + router.add_route(pattern, idx, path_is_wildcard); + } + return 0; +} +} // namespace + +namespace { +ForwardedNode parse_forwarded_node_type(const StringRef &optarg) { + if (util::strieq_l("obfuscated", optarg)) { + return ForwardedNode::OBFUSCATED; + } + + if (util::strieq_l("ip", optarg)) { + return ForwardedNode::IP; + } + + if (optarg.size() < 2 || optarg[0] != '_') { + return static_cast<ForwardedNode>(-1); + } + + if (std::find_if_not(std::begin(optarg), std::end(optarg), [](char c) { + return util::is_alpha(c) || util::is_digit(c) || c == '.' || c == '_' || + c == '-'; + }) != std::end(optarg)) { + return static_cast<ForwardedNode>(-1); + } + + return ForwardedNode::OBFUSCATED; +} +} // namespace + +namespace { +int parse_error_page(std::vector<ErrorPage> &error_pages, const StringRef &opt, + const StringRef &optarg) { + std::array<char, STRERROR_BUFSIZE> errbuf; + + auto eq = std::find(std::begin(optarg), std::end(optarg), '='); + if (eq == std::end(optarg) || eq + 1 == std::end(optarg)) { + LOG(ERROR) << opt << ": bad value: '" << optarg << "'"; + return -1; + } + + auto codestr = StringRef{std::begin(optarg), eq}; + unsigned int code; + + if (codestr == StringRef::from_lit("*")) { + code = 0; + } else { + auto n = util::parse_uint(codestr); + + if (n == -1 || n < 400 || n > 599) { + LOG(ERROR) << opt << ": bad code: '" << codestr << "'"; + return -1; + } + + code = static_cast<unsigned int>(n); + } + + auto path = StringRef{eq + 1, std::end(optarg)}; + + std::vector<uint8_t> content; + auto fd = open(path.c_str(), O_RDONLY); + if (fd == -1) { + auto error = errno; + LOG(ERROR) << opt << ": " << optarg << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + auto fd_closer = defer(close, fd); + + std::array<uint8_t, 4096> buf; + for (;;) { + auto n = read(fd, buf.data(), buf.size()); + if (n == -1) { + auto error = errno; + LOG(ERROR) << opt << ": " << optarg << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + if (n == 0) { + break; + } + content.insert(std::end(content), std::begin(buf), std::begin(buf) + n); + } + + error_pages.push_back(ErrorPage{std::move(content), code}); + + return 0; +} +} // namespace + +namespace { +// Maximum size of SCT extension payload length. +constexpr size_t MAX_SCT_EXT_LEN = 16_k; +} // namespace + +struct SubcertParams { + StringRef sct_dir; +}; + +namespace { +// Parses subcert parameter |src_params|, and stores parsed results +// into |out|. This function returns 0 if it succeeds, or -1. +int parse_subcert_params(SubcertParams &out, const StringRef &src_params) { + auto last = std::end(src_params); + for (auto first = std::begin(src_params); first != last;) { + auto end = std::find(first, last, ';'); + auto param = StringRef{first, end}; + + if (util::istarts_with_l(param, "sct-dir=")) { +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + auto sct_dir = + StringRef{std::begin(param) + str_size("sct-dir="), std::end(param)}; + if (sct_dir.empty()) { + LOG(ERROR) << "subcert: " << param << ": empty sct-dir"; + return -1; + } + out.sct_dir = sct_dir; +#else // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_BORINGSSL + LOG(WARN) << "subcert: sct-dir is ignored because underlying TLS library " + "does not support SCT"; +#endif // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_BORINGSSL + } else if (!param.empty()) { + LOG(ERROR) << "subcert: " << param << ": unknown keyword"; + return -1; + } + + if (end == last) { + break; + } + + first = end + 1; + } + + return 0; +} +} // namespace + +namespace { +// Reads *.sct files from directory denoted by |dir_path|. |dir_path| +// must be NULL-terminated string. +int read_tls_sct_from_dir(std::vector<uint8_t> &dst, const StringRef &opt, + const StringRef &dir_path) { + std::array<char, STRERROR_BUFSIZE> errbuf; + + auto dir = opendir(dir_path.c_str()); + if (dir == nullptr) { + auto error = errno; + LOG(ERROR) << opt << ": " << dir_path << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + auto closer = defer(closedir, dir); + + // 2 bytes total length field + auto len_idx = std::distance(std::begin(dst), std::end(dst)); + dst.insert(std::end(dst), 2, 0); + + for (;;) { + errno = 0; + auto ent = readdir(dir); + if (ent == nullptr) { + if (errno != 0) { + auto error = errno; + LOG(ERROR) << opt << ": failed to read directory " << dir_path << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + break; + } + + auto name = StringRef{ent->d_name}; + + if (name[0] == '.' || !util::iends_with_l(name, ".sct")) { + continue; + } + + std::string path; + path.resize(dir_path.size() + 1 + name.size()); + { + auto p = std::begin(path); + p = std::copy(std::begin(dir_path), std::end(dir_path), p); + *p++ = '/'; + std::copy(std::begin(name), std::end(name), p); + } + + auto fd = open(path.c_str(), O_RDONLY); + if (fd == -1) { + auto error = errno; + LOG(ERROR) << opt << ": failed to read SCT from " << path << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + auto closer = defer(close, fd); + + // 2 bytes length field for this SCT. + auto len_idx = std::distance(std::begin(dst), std::end(dst)); + dst.insert(std::end(dst), 2, 0); + + // *.sct file tends to be small; around 110+ bytes. + std::array<char, 256> buf; + for (;;) { + ssize_t nread; + while ((nread = read(fd, buf.data(), buf.size())) == -1 && errno == EINTR) + ; + + if (nread == -1) { + auto error = errno; + LOG(ERROR) << opt << ": failed to read SCT data from " << path << ": " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + return -1; + } + + if (nread == 0) { + break; + } + + dst.insert(std::end(dst), std::begin(buf), std::begin(buf) + nread); + + if (dst.size() > MAX_SCT_EXT_LEN) { + LOG(ERROR) << opt << ": the concatenated SCT data from " << dir_path + << " is too large. Max " << MAX_SCT_EXT_LEN; + return -1; + } + } + + auto len = dst.size() - len_idx - 2; + + if (len == 0) { + dst.resize(dst.size() - 2); + continue; + } + + dst[len_idx] = len >> 8; + dst[len_idx + 1] = len; + } + + auto len = dst.size() - len_idx - 2; + + if (len == 0) { + dst.resize(dst.size() - 2); + return 0; + } + + dst[len_idx] = len >> 8; + dst[len_idx + 1] = len; + + return 0; +} +} // namespace + +#ifndef OPENSSL_NO_PSK +namespace { +// Reads PSK secrets from path, and parses each line. The result is +// directly stored into config->tls.psk_secrets. This function +// returns 0 if it succeeds, or -1. +int parse_psk_secrets(Config *config, const StringRef &path) { + auto &tlsconf = config->tls; + + std::ifstream f(path.c_str(), std::ios::binary); + if (!f) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS << ": could not open file " << path; + return -1; + } + + size_t lineno = 0; + std::string line; + while (std::getline(f, line)) { + ++lineno; + if (line.empty() || line[0] == '#') { + continue; + } + + auto sep_it = std::find(std::begin(line), std::end(line), ':'); + if (sep_it == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS + << ": could not fine separator at line " << lineno; + return -1; + } + + if (sep_it == std::begin(line)) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS << ": empty identity at line " + << lineno; + return -1; + } + + if (sep_it + 1 == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS << ": empty secret at line " + << lineno; + return -1; + } + + if (!util::is_hex_string(StringRef{sep_it + 1, std::end(line)})) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS + << ": secret must be hex string at line " << lineno; + return -1; + } + + auto identity = + make_string_ref(config->balloc, StringRef{std::begin(line), sep_it}); + + auto secret = + util::decode_hex(config->balloc, StringRef{sep_it + 1, std::end(line)}); + + auto rv = tlsconf.psk_secrets.emplace(identity, secret); + if (!rv.second) { + LOG(ERROR) << SHRPX_OPT_PSK_SECRETS + << ": identity has already been registered at line " << lineno; + return -1; + } + } + + return 0; +} +} // namespace +#endif // !OPENSSL_NO_PSK + +#ifndef OPENSSL_NO_PSK +namespace { +// Reads PSK secrets from path, and parses each line. The result is +// directly stored into config->tls.client.psk. This function returns +// 0 if it succeeds, or -1. +int parse_client_psk_secrets(Config *config, const StringRef &path) { + auto &tlsconf = config->tls; + + std::ifstream f(path.c_str(), std::ios::binary); + if (!f) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS << ": could not open file " + << path; + return -1; + } + + size_t lineno = 0; + std::string line; + while (std::getline(f, line)) { + ++lineno; + if (line.empty() || line[0] == '#') { + continue; + } + + auto sep_it = std::find(std::begin(line), std::end(line), ':'); + if (sep_it == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS + << ": could not find separator at line " << lineno; + return -1; + } + + if (sep_it == std::begin(line)) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS << ": empty identity at line " + << lineno; + return -1; + } + + if (sep_it + 1 == std::end(line)) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS << ": empty secret at line " + << lineno; + return -1; + } + + if (!util::is_hex_string(StringRef{sep_it + 1, std::end(line)})) { + LOG(ERROR) << SHRPX_OPT_CLIENT_PSK_SECRETS + << ": secret must be hex string at line " << lineno; + return -1; + } + + tlsconf.client.psk.identity = + make_string_ref(config->balloc, StringRef{std::begin(line), sep_it}); + + tlsconf.client.psk.secret = + util::decode_hex(config->balloc, StringRef{sep_it + 1, std::end(line)}); + + return 0; + } + + return 0; +} +} // namespace +#endif // !OPENSSL_NO_PSK + +// generated by gennghttpxfun.py +int option_lookup_token(const char *name, size_t namelen) { + switch (namelen) { + case 4: + switch (name[3]) { + case 'f': + if (util::strieq_l("con", name, 3)) { + return SHRPX_OPTID_CONF; + } + break; + case 'r': + if (util::strieq_l("use", name, 3)) { + return SHRPX_OPTID_USER; + } + break; + } + break; + case 6: + switch (name[5]) { + case 'a': + if (util::strieq_l("no-vi", name, 5)) { + return SHRPX_OPTID_NO_VIA; + } + break; + case 'c': + if (util::strieq_l("altsv", name, 5)) { + return SHRPX_OPTID_ALTSVC; + } + break; + case 'n': + if (util::strieq_l("daemo", name, 5)) { + return SHRPX_OPTID_DAEMON; + } + break; + case 't': + if (util::strieq_l("cacer", name, 5)) { + return SHRPX_OPTID_CACERT; + } + if (util::strieq_l("clien", name, 5)) { + return SHRPX_OPTID_CLIENT; + } + break; + } + break; + case 7: + switch (name[6]) { + case 'd': + if (util::strieq_l("backen", name, 6)) { + return SHRPX_OPTID_BACKEND; + } + break; + case 'e': + if (util::strieq_l("includ", name, 6)) { + return SHRPX_OPTID_INCLUDE; + } + break; + case 'g': + if (util::strieq_l("backlo", name, 6)) { + return SHRPX_OPTID_BACKLOG; + } + if (util::strieq_l("paddin", name, 6)) { + return SHRPX_OPTID_PADDING; + } + break; + case 'p': + if (util::strieq_l("no-ocs", name, 6)) { + return SHRPX_OPTID_NO_OCSP; + } + break; + case 's': + if (util::strieq_l("cipher", name, 6)) { + return SHRPX_OPTID_CIPHERS; + } + if (util::strieq_l("worker", name, 6)) { + return SHRPX_OPTID_WORKERS; + } + break; + case 't': + if (util::strieq_l("subcer", name, 6)) { + return SHRPX_OPTID_SUBCERT; + } + break; + } + break; + case 8: + switch (name[7]) { + case 'd': + if (util::strieq_l("fronten", name, 7)) { + return SHRPX_OPTID_FRONTEND; + } + break; + case 'e': + if (util::strieq_l("insecur", name, 7)) { + return SHRPX_OPTID_INSECURE; + } + if (util::strieq_l("pid-fil", name, 7)) { + return SHRPX_OPTID_PID_FILE; + } + break; + case 'n': + if (util::strieq_l("fastope", name, 7)) { + return SHRPX_OPTID_FASTOPEN; + } + break; + case 's': + if (util::strieq_l("tls-ktl", name, 7)) { + return SHRPX_OPTID_TLS_KTLS; + } + break; + case 't': + if (util::strieq_l("npn-lis", name, 7)) { + return SHRPX_OPTID_NPN_LIST; + } + break; + } + break; + case 9: + switch (name[8]) { + case 'e': + if (util::strieq_l("no-kqueu", name, 8)) { + return SHRPX_OPTID_NO_KQUEUE; + } + if (util::strieq_l("read-rat", name, 8)) { + return SHRPX_OPTID_READ_RATE; + } + break; + case 'l': + if (util::strieq_l("log-leve", name, 8)) { + return SHRPX_OPTID_LOG_LEVEL; + } + break; + case 't': + if (util::strieq_l("alpn-lis", name, 8)) { + return SHRPX_OPTID_ALPN_LIST; + } + break; + } + break; + case 10: + switch (name[9]) { + case 'e': + if (util::strieq_l("error-pag", name, 9)) { + return SHRPX_OPTID_ERROR_PAGE; + } + if (util::strieq_l("mruby-fil", name, 9)) { + return SHRPX_OPTID_MRUBY_FILE; + } + if (util::strieq_l("write-rat", name, 9)) { + return SHRPX_OPTID_WRITE_RATE; + } + break; + case 't': + if (util::strieq_l("read-burs", name, 9)) { + return SHRPX_OPTID_READ_BURST; + } + break; + } + break; + case 11: + switch (name[10]) { + case 'e': + if (util::strieq_l("server-nam", name, 10)) { + return SHRPX_OPTID_SERVER_NAME; + } + break; + case 'f': + if (util::strieq_l("no-quic-bp", name, 10)) { + return SHRPX_OPTID_NO_QUIC_BPF; + } + break; + case 'r': + if (util::strieq_l("tls-sct-di", name, 10)) { + return SHRPX_OPTID_TLS_SCT_DIR; + } + break; + case 's': + if (util::strieq_l("backend-tl", name, 10)) { + return SHRPX_OPTID_BACKEND_TLS; + } + if (util::strieq_l("ecdh-curve", name, 10)) { + return SHRPX_OPTID_ECDH_CURVES; + } + if (util::strieq_l("psk-secret", name, 10)) { + return SHRPX_OPTID_PSK_SECRETS; + } + break; + case 't': + if (util::strieq_l("write-burs", name, 10)) { + return SHRPX_OPTID_WRITE_BURST; + } + break; + case 'y': + if (util::strieq_l("dns-max-tr", name, 10)) { + return SHRPX_OPTID_DNS_MAX_TRY; + } + if (util::strieq_l("http2-prox", name, 10)) { + return SHRPX_OPTID_HTTP2_PROXY; + } + break; + } + break; + case 12: + switch (name[11]) { + case '4': + if (util::strieq_l("backend-ipv", name, 11)) { + return SHRPX_OPTID_BACKEND_IPV4; + } + break; + case '6': + if (util::strieq_l("backend-ipv", name, 11)) { + return SHRPX_OPTID_BACKEND_IPV6; + } + break; + case 'c': + if (util::strieq_l("http2-altsv", name, 11)) { + return SHRPX_OPTID_HTTP2_ALTSVC; + } + break; + case 'e': + if (util::strieq_l("host-rewrit", name, 11)) { + return SHRPX_OPTID_HOST_REWRITE; + } + if (util::strieq_l("http2-bridg", name, 11)) { + return SHRPX_OPTID_HTTP2_BRIDGE; + } + break; + case 'p': + if (util::strieq_l("ocsp-startu", name, 11)) { + return SHRPX_OPTID_OCSP_STARTUP; + } + break; + case 'y': + if (util::strieq_l("client-prox", name, 11)) { + return SHRPX_OPTID_CLIENT_PROXY; + } + if (util::strieq_l("forwarded-b", name, 11)) { + return SHRPX_OPTID_FORWARDED_BY; + } + break; + } + break; + case 13: + switch (name[12]) { + case 'd': + if (util::strieq_l("add-forwarde", name, 12)) { + return SHRPX_OPTID_ADD_FORWARDED; + } + if (util::strieq_l("single-threa", name, 12)) { + return SHRPX_OPTID_SINGLE_THREAD; + } + break; + case 'e': + if (util::strieq_l("dh-param-fil", name, 12)) { + return SHRPX_OPTID_DH_PARAM_FILE; + } + if (util::strieq_l("errorlog-fil", name, 12)) { + return SHRPX_OPTID_ERRORLOG_FILE; + } + if (util::strieq_l("rlimit-nofil", name, 12)) { + return SHRPX_OPTID_RLIMIT_NOFILE; + } + break; + case 'r': + if (util::strieq_l("forwarded-fo", name, 12)) { + return SHRPX_OPTID_FORWARDED_FOR; + } + break; + case 's': + if (util::strieq_l("tls13-cipher", name, 12)) { + return SHRPX_OPTID_TLS13_CIPHERS; + } + break; + case 't': + if (util::strieq_l("verify-clien", name, 12)) { + return SHRPX_OPTID_VERIFY_CLIENT; + } + break; + } + break; + case 14: + switch (name[13]) { + case 'd': + if (util::strieq_l("quic-server-i", name, 13)) { + return SHRPX_OPTID_QUIC_SERVER_ID; + } + break; + case 'e': + if (util::strieq_l("accesslog-fil", name, 13)) { + return SHRPX_OPTID_ACCESSLOG_FILE; + } + break; + case 'h': + if (util::strieq_l("no-server-pus", name, 13)) { + return SHRPX_OPTID_NO_SERVER_PUSH; + } + break; + case 'k': + if (util::strieq_l("rlimit-memloc", name, 13)) { + return SHRPX_OPTID_RLIMIT_MEMLOCK; + } + break; + case 'p': + if (util::strieq_l("no-verify-ocs", name, 13)) { + return SHRPX_OPTID_NO_VERIFY_OCSP; + } + break; + case 's': + if (util::strieq_l("backend-no-tl", name, 13)) { + return SHRPX_OPTID_BACKEND_NO_TLS; + } + if (util::strieq_l("client-cipher", name, 13)) { + return SHRPX_OPTID_CLIENT_CIPHERS; + } + if (util::strieq_l("single-proces", name, 13)) { + return SHRPX_OPTID_SINGLE_PROCESS; + } + break; + case 't': + if (util::strieq_l("tls-proto-lis", name, 13)) { + return SHRPX_OPTID_TLS_PROTO_LIST; + } + break; + } + break; + case 15: + switch (name[14]) { + case 'e': + if (util::strieq_l("no-host-rewrit", name, 14)) { + return SHRPX_OPTID_NO_HOST_REWRITE; + } + break; + case 'g': + if (util::strieq_l("errorlog-syslo", name, 14)) { + return SHRPX_OPTID_ERRORLOG_SYSLOG; + } + break; + case 's': + if (util::strieq_l("frontend-no-tl", name, 14)) { + return SHRPX_OPTID_FRONTEND_NO_TLS; + } + break; + case 'y': + if (util::strieq_l("syslog-facilit", name, 14)) { + return SHRPX_OPTID_SYSLOG_FACILITY; + } + break; + } + break; + case 16: + switch (name[15]) { + case 'e': + if (util::strieq_l("certificate-fil", name, 15)) { + return SHRPX_OPTID_CERTIFICATE_FILE; + } + if (util::strieq_l("client-cert-fil", name, 15)) { + return SHRPX_OPTID_CLIENT_CERT_FILE; + } + if (util::strieq_l("private-key-fil", name, 15)) { + return SHRPX_OPTID_PRIVATE_KEY_FILE; + } + if (util::strieq_l("worker-read-rat", name, 15)) { + return SHRPX_OPTID_WORKER_READ_RATE; + } + break; + case 'g': + if (util::strieq_l("accesslog-syslo", name, 15)) { + return SHRPX_OPTID_ACCESSLOG_SYSLOG; + } + break; + case 't': + if (util::strieq_l("accesslog-forma", name, 15)) { + return SHRPX_OPTID_ACCESSLOG_FORMAT; + } + break; + } + break; + case 17: + switch (name[16]) { + case 'e': + if (util::strieq_l("no-server-rewrit", name, 16)) { + return SHRPX_OPTID_NO_SERVER_REWRITE; + } + if (util::strieq_l("worker-write-rat", name, 16)) { + return SHRPX_OPTID_WORKER_WRITE_RATE; + } + break; + case 's': + if (util::strieq_l("backend-http1-tl", name, 16)) { + return SHRPX_OPTID_BACKEND_HTTP1_TLS; + } + if (util::strieq_l("max-header-field", name, 16)) { + return SHRPX_OPTID_MAX_HEADER_FIELDS; + } + break; + case 't': + if (util::strieq_l("dns-cache-timeou", name, 16)) { + return SHRPX_OPTID_DNS_CACHE_TIMEOUT; + } + if (util::strieq_l("worker-read-burs", name, 16)) { + return SHRPX_OPTID_WORKER_READ_BURST; + } + break; + } + break; + case 18: + switch (name[17]) { + case 'a': + if (util::strieq_l("tls-max-early-dat", name, 17)) { + return SHRPX_OPTID_TLS_MAX_EARLY_DATA; + } + break; + case 'r': + if (util::strieq_l("add-request-heade", name, 17)) { + return SHRPX_OPTID_ADD_REQUEST_HEADER; + } + break; + case 's': + if (util::strieq_l("client-psk-secret", name, 17)) { + return SHRPX_OPTID_CLIENT_PSK_SECRETS; + } + break; + case 't': + if (util::strieq_l("dns-lookup-timeou", name, 17)) { + return SHRPX_OPTID_DNS_LOOKUP_TIMEOUT; + } + if (util::strieq_l("worker-write-burs", name, 17)) { + return SHRPX_OPTID_WORKER_WRITE_BURST; + } + break; + } + break; + case 19: + switch (name[18]) { + case 'e': + if (util::strieq_l("no-location-rewrit", name, 18)) { + return SHRPX_OPTID_NO_LOCATION_REWRITE; + } + if (util::strieq_l("require-http-schem", name, 18)) { + return SHRPX_OPTID_REQUIRE_HTTP_SCHEME; + } + if (util::strieq_l("tls-ticket-key-fil", name, 18)) { + return SHRPX_OPTID_TLS_TICKET_KEY_FILE; + } + break; + case 'f': + if (util::strieq_l("backend-max-backof", name, 18)) { + return SHRPX_OPTID_BACKEND_MAX_BACKOFF; + } + break; + case 'r': + if (util::strieq_l("add-response-heade", name, 18)) { + return SHRPX_OPTID_ADD_RESPONSE_HEADER; + } + if (util::strieq_l("add-x-forwarded-fo", name, 18)) { + return SHRPX_OPTID_ADD_X_FORWARDED_FOR; + } + if (util::strieq_l("header-field-buffe", name, 18)) { + return SHRPX_OPTID_HEADER_FIELD_BUFFER; + } + break; + case 't': + if (util::strieq_l("redirect-https-por", name, 18)) { + return SHRPX_OPTID_REDIRECT_HTTPS_PORT; + } + if (util::strieq_l("stream-read-timeou", name, 18)) { + return SHRPX_OPTID_STREAM_READ_TIMEOUT; + } + break; + } + break; + case 20: + switch (name[19]) { + case 'g': + if (util::strieq_l("frontend-frame-debu", name, 19)) { + return SHRPX_OPTID_FRONTEND_FRAME_DEBUG; + } + break; + case 'l': + if (util::strieq_l("ocsp-update-interva", name, 19)) { + return SHRPX_OPTID_OCSP_UPDATE_INTERVAL; + } + break; + case 's': + if (util::strieq_l("max-worker-processe", name, 19)) { + return SHRPX_OPTID_MAX_WORKER_PROCESSES; + } + if (util::strieq_l("tls13-client-cipher", name, 19)) { + return SHRPX_OPTID_TLS13_CLIENT_CIPHERS; + } + break; + case 't': + if (util::strieq_l("backend-read-timeou", name, 19)) { + return SHRPX_OPTID_BACKEND_READ_TIMEOUT; + } + if (util::strieq_l("stream-write-timeou", name, 19)) { + return SHRPX_OPTID_STREAM_WRITE_TIMEOUT; + } + if (util::strieq_l("verify-client-cacer", name, 19)) { + return SHRPX_OPTID_VERIFY_CLIENT_CACERT; + } + break; + case 'y': + if (util::strieq_l("api-max-request-bod", name, 19)) { + return SHRPX_OPTID_API_MAX_REQUEST_BODY; + } + break; + } + break; + case 21: + switch (name[20]) { + case 'd': + if (util::strieq_l("backend-tls-sni-fiel", name, 20)) { + return SHRPX_OPTID_BACKEND_TLS_SNI_FIELD; + } + break; + case 'e': + if (util::strieq_l("quic-bpf-program-fil", name, 20)) { + return SHRPX_OPTID_QUIC_BPF_PROGRAM_FILE; + } + break; + case 'l': + if (util::strieq_l("accept-proxy-protoco", name, 20)) { + return SHRPX_OPTID_ACCEPT_PROXY_PROTOCOL; + } + break; + case 'n': + if (util::strieq_l("tls-max-proto-versio", name, 20)) { + return SHRPX_OPTID_TLS_MAX_PROTO_VERSION; + } + if (util::strieq_l("tls-min-proto-versio", name, 20)) { + return SHRPX_OPTID_TLS_MIN_PROTO_VERSION; + } + break; + case 'r': + if (util::strieq_l("tls-ticket-key-ciphe", name, 20)) { + return SHRPX_OPTID_TLS_TICKET_KEY_CIPHER; + } + break; + case 's': + if (util::strieq_l("frontend-max-request", name, 20)) { + return SHRPX_OPTID_FRONTEND_MAX_REQUESTS; + } + break; + case 't': + if (util::strieq_l("backend-write-timeou", name, 20)) { + return SHRPX_OPTID_BACKEND_WRITE_TIMEOUT; + } + if (util::strieq_l("frontend-read-timeou", name, 20)) { + return SHRPX_OPTID_FRONTEND_READ_TIMEOUT; + } + break; + case 'y': + if (util::strieq_l("accesslog-write-earl", name, 20)) { + return SHRPX_OPTID_ACCESSLOG_WRITE_EARLY; + } + break; + } + break; + case 22: + switch (name[21]) { + case 'i': + if (util::strieq_l("backend-http-proxy-ur", name, 21)) { + return SHRPX_OPTID_BACKEND_HTTP_PROXY_URI; + } + break; + case 'r': + if (util::strieq_l("backend-request-buffe", name, 21)) { + return SHRPX_OPTID_BACKEND_REQUEST_BUFFER; + } + if (util::strieq_l("frontend-quic-qlog-di", name, 21)) { + return SHRPX_OPTID_FRONTEND_QUIC_QLOG_DIR; + } + break; + case 't': + if (util::strieq_l("frontend-write-timeou", name, 21)) { + return SHRPX_OPTID_FRONTEND_WRITE_TIMEOUT; + } + break; + case 'y': + if (util::strieq_l("backend-address-famil", name, 21)) { + return SHRPX_OPTID_BACKEND_ADDRESS_FAMILY; + } + break; + } + break; + case 23: + switch (name[22]) { + case 'e': + if (util::strieq_l("client-private-key-fil", name, 22)) { + return SHRPX_OPTID_CLIENT_PRIVATE_KEY_FILE; + } + if (util::strieq_l("private-key-passwd-fil", name, 22)) { + return SHRPX_OPTID_PRIVATE_KEY_PASSWD_FILE; + } + break; + case 'g': + if (util::strieq_l("frontend-quic-debug-lo", name, 22)) { + return SHRPX_OPTID_FRONTEND_QUIC_DEBUG_LOG; + } + break; + case 'r': + if (util::strieq_l("backend-response-buffe", name, 22)) { + return SHRPX_OPTID_BACKEND_RESPONSE_BUFFER; + } + break; + case 't': + if (util::strieq_l("backend-connect-timeou", name, 22)) { + return SHRPX_OPTID_BACKEND_CONNECT_TIMEOUT; + } + break; + } + break; + case 24: + switch (name[23]) { + case 'a': + if (util::strieq_l("frontend-quic-early-dat", name, 23)) { + return SHRPX_OPTID_FRONTEND_QUIC_EARLY_DATA; + } + break; + case 'd': + if (util::strieq_l("strip-incoming-forwarde", name, 23)) { + return SHRPX_OPTID_STRIP_INCOMING_FORWARDED; + } + if (util::strieq_l("tls-ticket-key-memcache", name, 23)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED; + } + break; + case 'e': + if (util::strieq_l("fetch-ocsp-response-fil", name, 23)) { + return SHRPX_OPTID_FETCH_OCSP_RESPONSE_FILE; + } + break; + case 'o': + if (util::strieq_l("no-add-x-forwarded-prot", name, 23)) { + return SHRPX_OPTID_NO_ADD_X_FORWARDED_PROTO; + } + break; + case 't': + if (util::strieq_l("listener-disable-timeou", name, 23)) { + return SHRPX_OPTID_LISTENER_DISABLE_TIMEOUT; + } + if (util::strieq_l("tls-dyn-rec-idle-timeou", name, 23)) { + return SHRPX_OPTID_TLS_DYN_REC_IDLE_TIMEOUT; + } + break; + } + break; + case 25: + switch (name[24]) { + case 'e': + if (util::strieq_l("backend-http2-window-siz", name, 24)) { + return SHRPX_OPTID_BACKEND_HTTP2_WINDOW_SIZE; + } + if (util::strieq_l("frontend-quic-secret-fil", name, 24)) { + return SHRPX_OPTID_FRONTEND_QUIC_SECRET_FILE; + } + break; + case 'g': + if (util::strieq_l("http2-no-cookie-crumblin", name, 24)) { + return SHRPX_OPTID_HTTP2_NO_COOKIE_CRUMBLING; + } + break; + case 's': + if (util::strieq_l("backend-http2-window-bit", name, 24)) { + return SHRPX_OPTID_BACKEND_HTTP2_WINDOW_BITS; + } + if (util::strieq_l("max-request-header-field", name, 24)) { + return SHRPX_OPTID_MAX_REQUEST_HEADER_FIELDS; + } + break; + case 't': + if (util::strieq_l("frontend-quic-initial-rt", name, 24)) { + return SHRPX_OPTID_FRONTEND_QUIC_INITIAL_RTT; + } + break; + } + break; + case 26: + switch (name[25]) { + case 'a': + if (util::strieq_l("tls-no-postpone-early-dat", name, 25)) { + return SHRPX_OPTID_TLS_NO_POSTPONE_EARLY_DATA; + } + break; + case 'e': + if (util::strieq_l("frontend-http2-window-siz", name, 25)) { + return SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_SIZE; + } + if (util::strieq_l("frontend-http3-window-siz", name, 25)) { + return SHRPX_OPTID_FRONTEND_HTTP3_WINDOW_SIZE; + } + break; + case 's': + if (util::strieq_l("frontend-http2-window-bit", name, 25)) { + return SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_BITS; + } + if (util::strieq_l("max-response-header-field", name, 25)) { + return SHRPX_OPTID_MAX_RESPONSE_HEADER_FIELDS; + } + break; + case 't': + if (util::strieq_l("backend-keep-alive-timeou", name, 25)) { + return SHRPX_OPTID_BACKEND_KEEP_ALIVE_TIMEOUT; + } + if (util::strieq_l("frontend-quic-idle-timeou", name, 25)) { + return SHRPX_OPTID_FRONTEND_QUIC_IDLE_TIMEOUT; + } + if (util::strieq_l("no-http2-cipher-black-lis", name, 25)) { + return SHRPX_OPTID_NO_HTTP2_CIPHER_BLACK_LIST; + } + if (util::strieq_l("no-http2-cipher-block-lis", name, 25)) { + return SHRPX_OPTID_NO_HTTP2_CIPHER_BLOCK_LIST; + } + break; + } + break; + case 27: + switch (name[26]) { + case 'd': + if (util::strieq_l("tls-session-cache-memcache", name, 26)) { + return SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED; + } + break; + case 'n': + if (util::strieq_l("frontend-quic-require-toke", name, 26)) { + return SHRPX_OPTID_FRONTEND_QUIC_REQUIRE_TOKEN; + } + break; + case 'r': + if (util::strieq_l("request-header-field-buffe", name, 26)) { + return SHRPX_OPTID_REQUEST_HEADER_FIELD_BUFFER; + } + break; + case 's': + if (util::strieq_l("worker-frontend-connection", name, 26)) { + return SHRPX_OPTID_WORKER_FRONTEND_CONNECTIONS; + } + break; + case 't': + if (util::strieq_l("frontend-http2-read-timeou", name, 26)) { + return SHRPX_OPTID_FRONTEND_HTTP2_READ_TIMEOUT; + } + if (util::strieq_l("frontend-http3-read-timeou", name, 26)) { + return SHRPX_OPTID_FRONTEND_HTTP3_READ_TIMEOUT; + } + if (util::strieq_l("frontend-keep-alive-timeou", name, 26)) { + return SHRPX_OPTID_FRONTEND_KEEP_ALIVE_TIMEOUT; + } + break; + } + break; + case 28: + switch (name[27]) { + case 'a': + if (util::strieq_l("no-strip-incoming-early-dat", name, 27)) { + return SHRPX_OPTID_NO_STRIP_INCOMING_EARLY_DATA; + } + break; + case 'd': + if (util::strieq_l("tls-dyn-rec-warmup-threshol", name, 27)) { + return SHRPX_OPTID_TLS_DYN_REC_WARMUP_THRESHOLD; + } + break; + case 'r': + if (util::strieq_l("response-header-field-buffe", name, 27)) { + return SHRPX_OPTID_RESPONSE_HEADER_FIELD_BUFFER; + } + break; + case 's': + if (util::strieq_l("http2-max-concurrent-stream", name, 27)) { + return SHRPX_OPTID_HTTP2_MAX_CONCURRENT_STREAMS; + } + if (util::strieq_l("tls-ticket-key-memcached-tl", name, 27)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_TLS; + } + break; + case 't': + if (util::strieq_l("backend-connections-per-hos", name, 27)) { + return SHRPX_OPTID_BACKEND_CONNECTIONS_PER_HOST; + } + break; + } + break; + case 30: + switch (name[29]) { + case 'd': + if (util::strieq_l("verify-client-tolerate-expire", name, 29)) { + return SHRPX_OPTID_VERIFY_CLIENT_TOLERATE_EXPIRED; + } + break; + case 'e': + if (util::strieq_l("frontend-http3-max-window-siz", name, 29)) { + return SHRPX_OPTID_FRONTEND_HTTP3_MAX_WINDOW_SIZE; + } + break; + case 'r': + if (util::strieq_l("ignore-per-pattern-mruby-erro", name, 29)) { + return SHRPX_OPTID_IGNORE_PER_PATTERN_MRUBY_ERROR; + } + if (util::strieq_l("strip-incoming-x-forwarded-fo", name, 29)) { + return SHRPX_OPTID_STRIP_INCOMING_X_FORWARDED_FOR; + } + break; + case 't': + if (util::strieq_l("backend-http2-settings-timeou", name, 29)) { + return SHRPX_OPTID_BACKEND_HTTP2_SETTINGS_TIMEOUT; + } + break; + } + break; + case 31: + switch (name[30]) { + case 's': + if (util::strieq_l("tls-session-cache-memcached-tl", name, 30)) { + return SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_TLS; + } + break; + case 't': + if (util::strieq_l("frontend-http2-settings-timeou", name, 30)) { + return SHRPX_OPTID_FRONTEND_HTTP2_SETTINGS_TIMEOUT; + } + break; + } + break; + case 32: + switch (name[31]) { + case 'd': + if (util::strieq_l("backend-connections-per-fronten", name, 31)) { + return SHRPX_OPTID_BACKEND_CONNECTIONS_PER_FRONTEND; + } + break; + } + break; + case 33: + switch (name[32]) { + case 'l': + if (util::strieq_l("tls-ticket-key-memcached-interva", name, 32)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_INTERVAL; + } + if (util::strieq_l("tls-ticket-key-memcached-max-fai", name, 32)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL; + } + break; + case 't': + if (util::strieq_l("client-no-http2-cipher-black-lis", name, 32)) { + return SHRPX_OPTID_CLIENT_NO_HTTP2_CIPHER_BLACK_LIST; + } + if (util::strieq_l("client-no-http2-cipher-block-lis", name, 32)) { + return SHRPX_OPTID_CLIENT_NO_HTTP2_CIPHER_BLOCK_LIST; + } + break; + } + break; + case 34: + switch (name[33]) { + case 'e': + if (util::strieq_l("tls-ticket-key-memcached-cert-fil", name, 33)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_CERT_FILE; + } + break; + case 'r': + if (util::strieq_l("frontend-http2-dump-request-heade", name, 33)) { + return SHRPX_OPTID_FRONTEND_HTTP2_DUMP_REQUEST_HEADER; + } + break; + case 't': + if (util::strieq_l("backend-http1-connections-per-hos", name, 33)) { + return SHRPX_OPTID_BACKEND_HTTP1_CONNECTIONS_PER_HOST; + } + break; + case 'y': + if (util::strieq_l("tls-ticket-key-memcached-max-retr", name, 33)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_MAX_RETRY; + } + break; + } + break; + case 35: + switch (name[34]) { + case 'e': + if (util::strieq_l("frontend-http2-optimize-window-siz", name, 34)) { + return SHRPX_OPTID_FRONTEND_HTTP2_OPTIMIZE_WINDOW_SIZE; + } + break; + case 'o': + if (util::strieq_l("no-strip-incoming-x-forwarded-prot", name, 34)) { + return SHRPX_OPTID_NO_STRIP_INCOMING_X_FORWARDED_PROTO; + } + break; + case 'r': + if (util::strieq_l("frontend-http2-dump-response-heade", name, 34)) { + return SHRPX_OPTID_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER; + } + if (util::strieq_l("frontend-quic-congestion-controlle", name, 34)) { + return SHRPX_OPTID_FRONTEND_QUIC_CONGESTION_CONTROLLER; + } + break; + } + break; + case 36: + switch (name[35]) { + case 'd': + if (util::strieq_l("worker-process-grace-shutdown-perio", name, 35)) { + return SHRPX_OPTID_WORKER_PROCESS_GRACE_SHUTDOWN_PERIOD; + } + break; + case 'e': + if (util::strieq_l("backend-http2-connection-window-siz", name, 35)) { + return SHRPX_OPTID_BACKEND_HTTP2_CONNECTION_WINDOW_SIZE; + } + break; + case 'r': + if (util::strieq_l("backend-http2-connections-per-worke", name, 35)) { + return SHRPX_OPTID_BACKEND_HTTP2_CONNECTIONS_PER_WORKER; + } + break; + case 's': + if (util::strieq_l("backend-http2-connection-window-bit", name, 35)) { + return SHRPX_OPTID_BACKEND_HTTP2_CONNECTION_WINDOW_BITS; + } + if (util::strieq_l("backend-http2-max-concurrent-stream", name, 35)) { + return SHRPX_OPTID_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS; + } + break; + } + break; + case 37: + switch (name[36]) { + case 'e': + if (util::strieq_l("frontend-http2-connection-window-siz", name, 36)) { + return SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE; + } + if (util::strieq_l("frontend-http3-connection-window-siz", name, 36)) { + return SHRPX_OPTID_FRONTEND_HTTP3_CONNECTION_WINDOW_SIZE; + } + if (util::strieq_l("tls-session-cache-memcached-cert-fil", name, 36)) { + return SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_CERT_FILE; + } + break; + case 's': + if (util::strieq_l("frontend-http2-connection-window-bit", name, 36)) { + return SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS; + } + if (util::strieq_l("frontend-http2-max-concurrent-stream", name, 36)) { + return SHRPX_OPTID_FRONTEND_HTTP2_MAX_CONCURRENT_STREAMS; + } + if (util::strieq_l("frontend-http3-max-concurrent-stream", name, 36)) { + return SHRPX_OPTID_FRONTEND_HTTP3_MAX_CONCURRENT_STREAMS; + } + break; + } + break; + case 38: + switch (name[37]) { + case 'd': + if (util::strieq_l("backend-http1-connections-per-fronten", name, 37)) { + return SHRPX_OPTID_BACKEND_HTTP1_CONNECTIONS_PER_FRONTEND; + } + break; + } + break; + case 39: + switch (name[38]) { + case 'y': + if (util::strieq_l("tls-ticket-key-memcached-address-famil", name, 38)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_ADDRESS_FAMILY; + } + break; + } + break; + case 40: + switch (name[39]) { + case 'e': + if (util::strieq_l("backend-http2-decoder-dynamic-table-siz", name, 39)) { + return SHRPX_OPTID_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE; + } + if (util::strieq_l("backend-http2-encoder-dynamic-table-siz", name, 39)) { + return SHRPX_OPTID_BACKEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE; + } + break; + } + break; + case 41: + switch (name[40]) { + case 'e': + if (util::strieq_l("frontend-http2-decoder-dynamic-table-siz", name, + 40)) { + return SHRPX_OPTID_FRONTEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE; + } + if (util::strieq_l("frontend-http2-encoder-dynamic-table-siz", name, + 40)) { + return SHRPX_OPTID_FRONTEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE; + } + if (util::strieq_l("frontend-http2-optimize-write-buffer-siz", name, + 40)) { + return SHRPX_OPTID_FRONTEND_HTTP2_OPTIMIZE_WRITE_BUFFER_SIZE; + } + if (util::strieq_l("frontend-http3-max-connection-window-siz", name, + 40)) { + return SHRPX_OPTID_FRONTEND_HTTP3_MAX_CONNECTION_WINDOW_SIZE; + } + if (util::strieq_l("tls-ticket-key-memcached-private-key-fil", name, + 40)) { + return SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_PRIVATE_KEY_FILE; + } + break; + } + break; + case 42: + switch (name[41]) { + case 'y': + if (util::strieq_l("tls-session-cache-memcached-address-famil", name, + 41)) { + return SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY; + } + break; + } + break; + case 44: + switch (name[43]) { + case 'e': + if (util::strieq_l("tls-session-cache-memcached-private-key-fil", name, + 43)) { + return SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_PRIVATE_KEY_FILE; + } + break; + } + break; + } + return -1; +} + +int parse_config(Config *config, const StringRef &opt, const StringRef &optarg, + std::set<StringRef> &included_set, + std::map<StringRef, size_t> &pattern_addr_indexer) { + auto optid = option_lookup_token(opt.c_str(), opt.size()); + return parse_config(config, optid, opt, optarg, included_set, + pattern_addr_indexer); +} + +int parse_config(Config *config, int optid, const StringRef &opt, + const StringRef &optarg, std::set<StringRef> &included_set, + std::map<StringRef, size_t> &pattern_addr_indexer) { + std::array<char, STRERROR_BUFSIZE> errbuf; + char host[NI_MAXHOST]; + uint16_t port; + + switch (optid) { + case SHRPX_OPTID_BACKEND: { + auto &downstreamconf = *config->conn.downstream; + auto addr_end = std::find(std::begin(optarg), std::end(optarg), ';'); + + DownstreamAddrConfig addr{}; + if (util::istarts_with(optarg, SHRPX_UNIX_PATH_PREFIX)) { + auto path = std::begin(optarg) + SHRPX_UNIX_PATH_PREFIX.size(); + addr.host = + make_string_ref(downstreamconf.balloc, StringRef{path, addr_end}); + addr.host_unix = true; + } else { + if (split_host_port(host, sizeof(host), &port, + StringRef{std::begin(optarg), addr_end}, opt) == -1) { + return -1; + } + + addr.host = make_string_ref(downstreamconf.balloc, StringRef{host}); + addr.port = port; + } + + auto mapping = addr_end == std::end(optarg) ? addr_end : addr_end + 1; + auto mapping_end = std::find(mapping, std::end(optarg), ';'); + + auto params = + mapping_end == std::end(optarg) ? mapping_end : mapping_end + 1; + + if (parse_mapping(config, addr, pattern_addr_indexer, + StringRef{mapping, mapping_end}, + StringRef{params, std::end(optarg)}) != 0) { + return -1; + } + + return 0; + } + case SHRPX_OPTID_FRONTEND: { + auto &apiconf = config->api; + + auto addr_end = std::find(std::begin(optarg), std::end(optarg), ';'); + auto src_params = StringRef{addr_end, std::end(optarg)}; + + UpstreamParams params{}; + params.tls = true; + + if (parse_upstream_params(params, src_params) != 0) { + return -1; + } + + if (params.sni_fwd && !params.tls) { + LOG(ERROR) << "frontend: sni_fwd requires tls"; + return -1; + } + + if (params.quic) { + if (params.alt_mode != UpstreamAltMode::NONE) { + LOG(ERROR) << "frontend: api or healthmon cannot be used with quic"; + return -1; + } + + if (!params.tls) { + LOG(ERROR) << "frontend: quic requires TLS"; + return -1; + } + } + + UpstreamAddr addr{}; + addr.fd = -1; + addr.tls = params.tls; + addr.sni_fwd = params.sni_fwd; + addr.alt_mode = params.alt_mode; + addr.accept_proxy_protocol = params.proxyproto; + addr.quic = params.quic; + + if (addr.alt_mode == UpstreamAltMode::API) { + apiconf.enabled = true; + } + +#ifdef ENABLE_HTTP3 + auto &addrs = params.quic ? config->conn.quic_listener.addrs + : config->conn.listener.addrs; +#else // !ENABLE_HTTP3 + auto &addrs = config->conn.listener.addrs; +#endif // !ENABLE_HTTP3 + + if (util::istarts_with(optarg, SHRPX_UNIX_PATH_PREFIX)) { + if (addr.quic) { + LOG(ERROR) << "frontend: quic cannot be used on UNIX domain socket"; + return -1; + } + + auto path = std::begin(optarg) + SHRPX_UNIX_PATH_PREFIX.size(); + addr.host = make_string_ref(config->balloc, StringRef{path, addr_end}); + addr.host_unix = true; + addr.index = addrs.size(); + + addrs.push_back(std::move(addr)); + + return 0; + } + + if (split_host_port(host, sizeof(host), &port, + StringRef{std::begin(optarg), addr_end}, opt) == -1) { + return -1; + } + + addr.host = make_string_ref(config->balloc, StringRef{host}); + addr.port = port; + + if (util::numeric_host(host, AF_INET)) { + addr.family = AF_INET; + addr.index = addrs.size(); + addrs.push_back(std::move(addr)); + return 0; + } + + if (util::numeric_host(host, AF_INET6)) { + addr.family = AF_INET6; + addr.index = addrs.size(); + addrs.push_back(std::move(addr)); + return 0; + } + + addr.family = AF_INET; + addr.index = addrs.size(); + addrs.push_back(addr); + + addr.family = AF_INET6; + addr.index = addrs.size(); + addrs.push_back(std::move(addr)); + + return 0; + } + case SHRPX_OPTID_WORKERS: +#ifdef NOTHREADS + LOG(WARN) << "Threading disabled at build time, no threads created."; + return 0; +#else // !NOTHREADS + return parse_uint(&config->num_worker, opt, optarg); +#endif // !NOTHREADS + case SHRPX_OPTID_HTTP2_MAX_CONCURRENT_STREAMS: { + LOG(WARN) << opt << ": deprecated. Use " + << SHRPX_OPT_FRONTEND_HTTP2_MAX_CONCURRENT_STREAMS << " and " + << SHRPX_OPT_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS << " instead."; + size_t n; + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + auto &http2conf = config->http2; + http2conf.upstream.max_concurrent_streams = n; + http2conf.downstream.max_concurrent_streams = n; + + return 0; + } + case SHRPX_OPTID_LOG_LEVEL: { + auto level = Log::get_severity_level_by_name(optarg); + if (level == -1) { + LOG(ERROR) << opt << ": Invalid severity level: " << optarg; + return -1; + } + config->logging.severity = level; + + return 0; + } + case SHRPX_OPTID_DAEMON: + config->daemon = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_HTTP2_PROXY: + config->http2_proxy = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_HTTP2_BRIDGE: + LOG(ERROR) << opt + << ": deprecated. Use backend=<addr>,<port>;;proto=h2;tls"; + return -1; + case SHRPX_OPTID_CLIENT_PROXY: + LOG(ERROR) + << opt + << ": deprecated. Use http2-proxy, frontend=<addr>,<port>;no-tls " + "and backend=<addr>,<port>;;proto=h2;tls"; + return -1; + case SHRPX_OPTID_ADD_X_FORWARDED_FOR: + config->http.xff.add = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_STRIP_INCOMING_X_FORWARDED_FOR: + config->http.xff.strip_incoming = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_NO_VIA: + config->http.no_via = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_READ_TIMEOUT: + return parse_duration(&config->conn.upstream.timeout.http2_read, opt, + optarg); + case SHRPX_OPTID_FRONTEND_READ_TIMEOUT: + return parse_duration(&config->conn.upstream.timeout.read, opt, optarg); + case SHRPX_OPTID_FRONTEND_WRITE_TIMEOUT: + return parse_duration(&config->conn.upstream.timeout.write, opt, optarg); + case SHRPX_OPTID_BACKEND_READ_TIMEOUT: + return parse_duration(&config->conn.downstream->timeout.read, opt, optarg); + case SHRPX_OPTID_BACKEND_WRITE_TIMEOUT: + return parse_duration(&config->conn.downstream->timeout.write, opt, optarg); + case SHRPX_OPTID_BACKEND_CONNECT_TIMEOUT: + return parse_duration(&config->conn.downstream->timeout.connect, opt, + optarg); + case SHRPX_OPTID_STREAM_READ_TIMEOUT: + return parse_duration(&config->http2.timeout.stream_read, opt, optarg); + case SHRPX_OPTID_STREAM_WRITE_TIMEOUT: + return parse_duration(&config->http2.timeout.stream_write, opt, optarg); + case SHRPX_OPTID_ACCESSLOG_FILE: + config->logging.access.file = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_ACCESSLOG_SYSLOG: + config->logging.access.syslog = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_ACCESSLOG_FORMAT: + config->logging.access.format = parse_log_format(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_ERRORLOG_FILE: + config->logging.error.file = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_ERRORLOG_SYSLOG: + config->logging.error.syslog = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_FASTOPEN: + return parse_uint(&config->conn.listener.fastopen, opt, optarg); + case SHRPX_OPTID_BACKEND_KEEP_ALIVE_TIMEOUT: + return parse_duration(&config->conn.downstream->timeout.idle_read, opt, + optarg); + case SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_BITS: + case SHRPX_OPTID_BACKEND_HTTP2_WINDOW_BITS: { + LOG(WARN) << opt << ": deprecated. Use " + << (optid == SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_BITS + ? SHRPX_OPT_FRONTEND_HTTP2_WINDOW_SIZE + : SHRPX_OPT_BACKEND_HTTP2_WINDOW_SIZE); + int32_t *resp; + + if (optid == SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_BITS) { + resp = &config->http2.upstream.window_size; + } else { + resp = &config->http2.downstream.window_size; + } + + errno = 0; + + int n; + + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + + if (n >= 31) { + LOG(ERROR) << opt + << ": specify the integer in the range [0, 30], inclusive"; + return -1; + } + + // Make 16 bits to the HTTP/2 default 64KiB - 1. This is the same + // behaviour of previous code. + *resp = (1 << n) - 1; + + return 0; + } + case SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS: + case SHRPX_OPTID_BACKEND_HTTP2_CONNECTION_WINDOW_BITS: { + LOG(WARN) << opt << ": deprecated. Use " + << (optid == SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS + ? SHRPX_OPT_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE + : SHRPX_OPT_BACKEND_HTTP2_CONNECTION_WINDOW_SIZE); + int32_t *resp; + + if (optid == SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS) { + resp = &config->http2.upstream.connection_window_size; + } else { + resp = &config->http2.downstream.connection_window_size; + } + + errno = 0; + + int n; + + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + + if (n < 16 || n >= 31) { + LOG(ERROR) << opt + << ": specify the integer in the range [16, 30], inclusive"; + return -1; + } + + *resp = (1 << n) - 1; + + return 0; + } + case SHRPX_OPTID_FRONTEND_NO_TLS: + LOG(WARN) << opt << ": deprecated. Use no-tls keyword in " + << SHRPX_OPT_FRONTEND; + return 0; + case SHRPX_OPTID_BACKEND_NO_TLS: + LOG(WARN) << opt + << ": deprecated. backend connection is not encrypted by " + "default. See also " + << SHRPX_OPT_BACKEND_TLS; + return 0; + case SHRPX_OPTID_BACKEND_TLS_SNI_FIELD: + LOG(WARN) << opt + << ": deprecated. Use sni keyword in --backend option. " + "For now, all sni values of all backends are " + "overridden by the given value " + << optarg; + config->tls.backend_sni_name = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_PID_FILE: + config->pid_file = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_USER: { + auto pwd = getpwnam(optarg.c_str()); + if (!pwd) { + LOG(ERROR) << opt << ": failed to get uid from " << optarg << ": " + << xsi_strerror(errno, errbuf.data(), errbuf.size()); + return -1; + } + config->user = make_string_ref(config->balloc, StringRef{pwd->pw_name}); + config->uid = pwd->pw_uid; + config->gid = pwd->pw_gid; + + return 0; + } + case SHRPX_OPTID_PRIVATE_KEY_FILE: + config->tls.private_key_file = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_PRIVATE_KEY_PASSWD_FILE: { + auto passwd = read_passwd_from_file(opt, optarg); + if (passwd.empty()) { + LOG(ERROR) << opt << ": Couldn't read key file's passwd from " << optarg; + return -1; + } + config->tls.private_key_passwd = + make_string_ref(config->balloc, StringRef{passwd}); + + return 0; + } + case SHRPX_OPTID_CERTIFICATE_FILE: + config->tls.cert_file = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_DH_PARAM_FILE: + config->tls.dh_param_file = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_SUBCERT: { + auto end_keys = std::find(std::begin(optarg), std::end(optarg), ';'); + auto src_params = StringRef{end_keys, std::end(optarg)}; + + SubcertParams params; + if (parse_subcert_params(params, src_params) != 0) { + return -1; + } + + std::vector<uint8_t> sct_data; + + if (!params.sct_dir.empty()) { + // Make sure that dir_path is NULL terminated string. + if (read_tls_sct_from_dir(sct_data, opt, + StringRef{params.sct_dir.str()}) != 0) { + return -1; + } + } + + // Private Key file and certificate file separated by ':'. + auto sp = std::find(std::begin(optarg), end_keys, ':'); + if (sp == end_keys) { + LOG(ERROR) << opt << ": missing ':' in " + << StringRef{std::begin(optarg), end_keys}; + return -1; + } + + auto private_key_file = StringRef{std::begin(optarg), sp}; + + if (private_key_file.empty()) { + LOG(ERROR) << opt << ": missing private key file: " + << StringRef{std::begin(optarg), end_keys}; + return -1; + } + + auto cert_file = StringRef{sp + 1, end_keys}; + + if (cert_file.empty()) { + LOG(ERROR) << opt << ": missing certificate file: " + << StringRef{std::begin(optarg), end_keys}; + return -1; + } + + config->tls.subcerts.emplace_back( + make_string_ref(config->balloc, private_key_file), + make_string_ref(config->balloc, cert_file), std::move(sct_data)); + + return 0; + } + case SHRPX_OPTID_SYSLOG_FACILITY: { + int facility = int_syslog_facility(optarg); + if (facility == -1) { + LOG(ERROR) << opt << ": Unknown syslog facility: " << optarg; + return -1; + } + config->logging.syslog_facility = facility; + + return 0; + } + case SHRPX_OPTID_BACKLOG: + return parse_uint(&config->conn.listener.backlog, opt, optarg); + case SHRPX_OPTID_CIPHERS: + config->tls.ciphers = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_TLS13_CIPHERS: + config->tls.tls13_ciphers = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_CLIENT: + LOG(ERROR) << opt + << ": deprecated. Use frontend=<addr>,<port>;no-tls, " + "backend=<addr>,<port>;;proto=h2;tls"; + return -1; + case SHRPX_OPTID_INSECURE: + config->tls.insecure = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_CACERT: + config->tls.cacert = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_BACKEND_IPV4: + LOG(WARN) << opt + << ": deprecated. Use backend-address-family=IPv4 instead."; + + config->conn.downstream->family = AF_INET; + + return 0; + case SHRPX_OPTID_BACKEND_IPV6: + LOG(WARN) << opt + << ": deprecated. Use backend-address-family=IPv6 instead."; + + config->conn.downstream->family = AF_INET6; + + return 0; + case SHRPX_OPTID_BACKEND_HTTP_PROXY_URI: { + auto &proxy = config->downstream_http_proxy; + // Reset here so that multiple option occurrence does not merge + // the results. + proxy = {}; + // parse URI and get hostname, port and optionally userinfo. + http_parser_url u{}; + int rv = http_parser_parse_url(optarg.c_str(), optarg.size(), 0, &u); + if (rv == 0) { + if (u.field_set & UF_USERINFO) { + auto uf = util::get_uri_field(optarg.c_str(), u, UF_USERINFO); + // Surprisingly, u.field_set & UF_USERINFO is nonzero even if + // userinfo component is empty string. + if (!uf.empty()) { + proxy.userinfo = util::percent_decode(config->balloc, uf); + } + } + if (u.field_set & UF_HOST) { + proxy.host = make_string_ref( + config->balloc, util::get_uri_field(optarg.c_str(), u, UF_HOST)); + } else { + LOG(ERROR) << opt << ": no hostname specified"; + return -1; + } + if (u.field_set & UF_PORT) { + proxy.port = u.port; + } else { + LOG(ERROR) << opt << ": no port specified"; + return -1; + } + } else { + LOG(ERROR) << opt << ": parse error"; + return -1; + } + + return 0; + } + case SHRPX_OPTID_READ_RATE: + return parse_uint_with_unit(&config->conn.upstream.ratelimit.read.rate, opt, + optarg); + case SHRPX_OPTID_READ_BURST: + return parse_uint_with_unit(&config->conn.upstream.ratelimit.read.burst, + opt, optarg); + case SHRPX_OPTID_WRITE_RATE: + return parse_uint_with_unit(&config->conn.upstream.ratelimit.write.rate, + opt, optarg); + case SHRPX_OPTID_WRITE_BURST: + return parse_uint_with_unit(&config->conn.upstream.ratelimit.write.burst, + opt, optarg); + case SHRPX_OPTID_WORKER_READ_RATE: + LOG(WARN) << opt << ": not implemented yet"; + return 0; + case SHRPX_OPTID_WORKER_READ_BURST: + LOG(WARN) << opt << ": not implemented yet"; + return 0; + case SHRPX_OPTID_WORKER_WRITE_RATE: + LOG(WARN) << opt << ": not implemented yet"; + return 0; + case SHRPX_OPTID_WORKER_WRITE_BURST: + LOG(WARN) << opt << ": not implemented yet"; + return 0; + case SHRPX_OPTID_TLS_PROTO_LIST: { + LOG(WARN) << opt + << ": deprecated. Use tls-min-proto-version and " + "tls-max-proto-version instead."; + auto list = util::split_str(optarg, ','); + config->tls.tls_proto_list.resize(list.size()); + for (size_t i = 0; i < list.size(); ++i) { + config->tls.tls_proto_list[i] = make_string_ref(config->balloc, list[i]); + } + + return 0; + } + case SHRPX_OPTID_VERIFY_CLIENT: + config->tls.client_verify.enabled = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_VERIFY_CLIENT_CACERT: + config->tls.client_verify.cacert = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_CLIENT_PRIVATE_KEY_FILE: + config->tls.client.private_key_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_CLIENT_CERT_FILE: + config->tls.client.cert_file = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_DUMP_REQUEST_HEADER: + config->http2.upstream.debug.dump.request_header_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER: + config->http2.upstream.debug.dump.response_header_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_HTTP2_NO_COOKIE_CRUMBLING: + config->http2.no_cookie_crumbling = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_FRONTEND_FRAME_DEBUG: + config->http2.upstream.debug.frame_debug = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_PADDING: + return parse_uint(&config->padding, opt, optarg); + case SHRPX_OPTID_ALTSVC: { + AltSvc altsvc{}; + + if (parse_altsvc(altsvc, opt, optarg) != 0) { + return -1; + } + + config->http.altsvcs.push_back(std::move(altsvc)); + + return 0; + } + case SHRPX_OPTID_ADD_REQUEST_HEADER: + case SHRPX_OPTID_ADD_RESPONSE_HEADER: { + auto p = parse_header(config->balloc, optarg); + if (p.name.empty()) { + LOG(ERROR) << opt << ": invalid header field: " << optarg; + return -1; + } + if (optid == SHRPX_OPTID_ADD_REQUEST_HEADER) { + config->http.add_request_headers.push_back(std::move(p)); + } else { + config->http.add_response_headers.push_back(std::move(p)); + } + return 0; + } + case SHRPX_OPTID_WORKER_FRONTEND_CONNECTIONS: + return parse_uint(&config->conn.upstream.worker_connections, opt, optarg); + case SHRPX_OPTID_NO_LOCATION_REWRITE: + config->http.no_location_rewrite = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_NO_HOST_REWRITE: + LOG(WARN) << SHRPX_OPT_NO_HOST_REWRITE + << ": deprecated. :authority and host header fields are NOT " + "altered by default. To rewrite these headers, use " + "--host-rewrite option."; + + return 0; + case SHRPX_OPTID_BACKEND_HTTP1_CONNECTIONS_PER_HOST: + LOG(WARN) << opt + << ": deprecated. Use backend-connections-per-host instead."; + // fall through + case SHRPX_OPTID_BACKEND_CONNECTIONS_PER_HOST: { + int n; + + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + + if (n == 0) { + LOG(ERROR) << opt << ": specify an integer strictly more than 0"; + + return -1; + } + + config->conn.downstream->connections_per_host = n; + + return 0; + } + case SHRPX_OPTID_BACKEND_HTTP1_CONNECTIONS_PER_FRONTEND: + LOG(WARN) << opt << ": deprecated. Use " + << SHRPX_OPT_BACKEND_CONNECTIONS_PER_FRONTEND << " instead."; + // fall through + case SHRPX_OPTID_BACKEND_CONNECTIONS_PER_FRONTEND: + return parse_uint(&config->conn.downstream->connections_per_frontend, opt, + optarg); + case SHRPX_OPTID_LISTENER_DISABLE_TIMEOUT: + return parse_duration(&config->conn.listener.timeout.sleep, opt, optarg); + case SHRPX_OPTID_TLS_TICKET_KEY_FILE: + config->tls.ticket.files.emplace_back( + make_string_ref(config->balloc, optarg)); + return 0; + case SHRPX_OPTID_RLIMIT_NOFILE: { + int n; + + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + + if (n < 0) { + LOG(ERROR) << opt << ": specify the integer more than or equal to 0"; + + return -1; + } + + config->rlimit_nofile = n; + + return 0; + } + case SHRPX_OPTID_BACKEND_REQUEST_BUFFER: + case SHRPX_OPTID_BACKEND_RESPONSE_BUFFER: { + size_t n; + if (parse_uint_with_unit(&n, opt, optarg) != 0) { + return -1; + } + + if (n == 0) { + LOG(ERROR) << opt << ": specify an integer strictly more than 0"; + + return -1; + } + + if (optid == SHRPX_OPTID_BACKEND_REQUEST_BUFFER) { + config->conn.downstream->request_buffer_size = n; + } else { + config->conn.downstream->response_buffer_size = n; + } + + return 0; + } + + case SHRPX_OPTID_NO_SERVER_PUSH: + config->http2.no_server_push = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_BACKEND_HTTP2_CONNECTIONS_PER_WORKER: + LOG(WARN) << opt << ": deprecated."; + return 0; + case SHRPX_OPTID_FETCH_OCSP_RESPONSE_FILE: + config->tls.ocsp.fetch_ocsp_response_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_OCSP_UPDATE_INTERVAL: + return parse_duration(&config->tls.ocsp.update_interval, opt, optarg); + case SHRPX_OPTID_NO_OCSP: + config->tls.ocsp.disabled = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_HEADER_FIELD_BUFFER: + LOG(WARN) << opt + << ": deprecated. Use request-header-field-buffer instead."; + // fall through + case SHRPX_OPTID_REQUEST_HEADER_FIELD_BUFFER: + return parse_uint_with_unit(&config->http.request_header_field_buffer, opt, + optarg); + case SHRPX_OPTID_MAX_HEADER_FIELDS: + LOG(WARN) << opt << ": deprecated. Use max-request-header-fields instead."; + // fall through + case SHRPX_OPTID_MAX_REQUEST_HEADER_FIELDS: + return parse_uint(&config->http.max_request_header_fields, opt, optarg); + case SHRPX_OPTID_RESPONSE_HEADER_FIELD_BUFFER: + return parse_uint_with_unit(&config->http.response_header_field_buffer, opt, + optarg); + case SHRPX_OPTID_MAX_RESPONSE_HEADER_FIELDS: + return parse_uint(&config->http.max_response_header_fields, opt, optarg); + case SHRPX_OPTID_INCLUDE: { + if (included_set.count(optarg)) { + LOG(ERROR) << opt << ": " << optarg << " has already been included"; + return -1; + } + + included_set.insert(optarg); + auto rv = + load_config(config, optarg.c_str(), included_set, pattern_addr_indexer); + included_set.erase(optarg); + + if (rv != 0) { + return -1; + } + + return 0; + } + case SHRPX_OPTID_TLS_TICKET_KEY_CIPHER: + if (util::strieq_l("aes-128-cbc", optarg)) { + config->tls.ticket.cipher = EVP_aes_128_cbc(); + } else if (util::strieq_l("aes-256-cbc", optarg)) { + config->tls.ticket.cipher = EVP_aes_256_cbc(); + } else { + LOG(ERROR) << opt + << ": unsupported cipher for ticket encryption: " << optarg; + return -1; + } + config->tls.ticket.cipher_given = true; + + return 0; + case SHRPX_OPTID_HOST_REWRITE: + config->http.no_host_rewrite = !util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED: + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED: { + auto addr_end = std::find(std::begin(optarg), std::end(optarg), ';'); + auto src_params = StringRef{addr_end, std::end(optarg)}; + + MemcachedConnectionParams params{}; + if (parse_memcached_connection_params(params, src_params, StringRef{opt}) != + 0) { + return -1; + } + + if (split_host_port(host, sizeof(host), &port, + StringRef{std::begin(optarg), addr_end}, opt) == -1) { + return -1; + } + + switch (optid) { + case SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED: { + auto &memcachedconf = config->tls.session_cache.memcached; + memcachedconf.host = make_string_ref(config->balloc, StringRef{host}); + memcachedconf.port = port; + memcachedconf.tls = params.tls; + break; + } + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED: { + auto &memcachedconf = config->tls.ticket.memcached; + memcachedconf.host = make_string_ref(config->balloc, StringRef{host}); + memcachedconf.port = port; + memcachedconf.tls = params.tls; + break; + } + }; + + return 0; + } + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_INTERVAL: + return parse_duration(&config->tls.ticket.memcached.interval, opt, optarg); + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_MAX_RETRY: { + int n; + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + + if (n > 30) { + LOG(ERROR) << opt << ": must be smaller than or equal to 30"; + return -1; + } + + config->tls.ticket.memcached.max_retry = n; + return 0; + } + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL: + return parse_uint(&config->tls.ticket.memcached.max_fail, opt, optarg); + case SHRPX_OPTID_TLS_DYN_REC_WARMUP_THRESHOLD: { + size_t n; + if (parse_uint_with_unit(&n, opt, optarg) != 0) { + return -1; + } + + config->tls.dyn_rec.warmup_threshold = n; + + return 0; + } + + case SHRPX_OPTID_TLS_DYN_REC_IDLE_TIMEOUT: + return parse_duration(&config->tls.dyn_rec.idle_timeout, opt, optarg); + + case SHRPX_OPTID_MRUBY_FILE: +#ifdef HAVE_MRUBY + config->mruby_file = make_string_ref(config->balloc, optarg); +#else // !HAVE_MRUBY + LOG(WARN) << opt + << ": ignored because mruby support is disabled at build time."; +#endif // !HAVE_MRUBY + return 0; + case SHRPX_OPTID_ACCEPT_PROXY_PROTOCOL: + LOG(WARN) << opt << ": deprecated. Use proxyproto keyword in " + << SHRPX_OPT_FRONTEND << " instead."; + config->conn.upstream.accept_proxy_protocol = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_ADD_FORWARDED: { + auto &fwdconf = config->http.forwarded; + fwdconf.params = FORWARDED_NONE; + for (const auto ¶m : util::split_str(optarg, ',')) { + if (util::strieq_l("by", param)) { + fwdconf.params |= FORWARDED_BY; + continue; + } + if (util::strieq_l("for", param)) { + fwdconf.params |= FORWARDED_FOR; + continue; + } + if (util::strieq_l("host", param)) { + fwdconf.params |= FORWARDED_HOST; + continue; + } + if (util::strieq_l("proto", param)) { + fwdconf.params |= FORWARDED_PROTO; + continue; + } + + LOG(ERROR) << opt << ": unknown parameter " << optarg; + + return -1; + } + + return 0; + } + case SHRPX_OPTID_STRIP_INCOMING_FORWARDED: + config->http.forwarded.strip_incoming = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_FORWARDED_BY: + case SHRPX_OPTID_FORWARDED_FOR: { + auto type = parse_forwarded_node_type(optarg); + + if (type == static_cast<ForwardedNode>(-1) || + (optid == SHRPX_OPTID_FORWARDED_FOR && optarg[0] == '_')) { + LOG(ERROR) << opt << ": unknown node type or illegal obfuscated string " + << optarg; + return -1; + } + + auto &fwdconf = config->http.forwarded; + + switch (optid) { + case SHRPX_OPTID_FORWARDED_BY: + fwdconf.by_node_type = type; + if (optarg[0] == '_') { + fwdconf.by_obfuscated = make_string_ref(config->balloc, optarg); + } else { + fwdconf.by_obfuscated = StringRef::from_lit(""); + } + break; + case SHRPX_OPTID_FORWARDED_FOR: + fwdconf.for_node_type = type; + break; + } + + return 0; + } + case SHRPX_OPTID_NO_HTTP2_CIPHER_BLACK_LIST: + LOG(WARN) << opt << ": deprecated. Use " + << SHRPX_OPT_NO_HTTP2_CIPHER_BLOCK_LIST << " instead."; + // fall through + case SHRPX_OPTID_NO_HTTP2_CIPHER_BLOCK_LIST: + config->tls.no_http2_cipher_block_list = util::strieq_l("yes", optarg); + return 0; + case SHRPX_OPTID_BACKEND_HTTP1_TLS: + case SHRPX_OPTID_BACKEND_TLS: + LOG(WARN) << opt << ": deprecated. Use tls keyword in " + << SHRPX_OPT_BACKEND << " instead."; + return 0; + case SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_TLS: + LOG(WARN) << opt << ": deprecated. Use tls keyword in " + << SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED; + return 0; + case SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_CERT_FILE: + config->tls.session_cache.memcached.cert_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_PRIVATE_KEY_FILE: + config->tls.session_cache.memcached.private_key_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_TLS: + LOG(WARN) << opt << ": deprecated. Use tls keyword in " + << SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED; + return 0; + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_CERT_FILE: + config->tls.ticket.memcached.cert_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_PRIVATE_KEY_FILE: + config->tls.ticket.memcached.private_key_file = + make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_ADDRESS_FAMILY: + return parse_address_family(&config->tls.ticket.memcached.family, opt, + optarg); + case SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY: + return parse_address_family(&config->tls.session_cache.memcached.family, + opt, optarg); + case SHRPX_OPTID_BACKEND_ADDRESS_FAMILY: + return parse_address_family(&config->conn.downstream->family, opt, optarg); + case SHRPX_OPTID_FRONTEND_HTTP2_MAX_CONCURRENT_STREAMS: + return parse_uint(&config->http2.upstream.max_concurrent_streams, opt, + optarg); + case SHRPX_OPTID_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS: + return parse_uint(&config->http2.downstream.max_concurrent_streams, opt, + optarg); + case SHRPX_OPTID_ERROR_PAGE: + return parse_error_page(config->http.error_pages, opt, optarg); + case SHRPX_OPTID_NO_KQUEUE: + if ((ev_supported_backends() & EVBACKEND_KQUEUE) == 0) { + LOG(WARN) << opt << ": kqueue is not supported on this platform"; + return 0; + } + + config->ev_loop_flags = ev_recommended_backends() & ~EVBACKEND_KQUEUE; + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_SETTINGS_TIMEOUT: + return parse_duration(&config->http2.upstream.timeout.settings, opt, + optarg); + case SHRPX_OPTID_BACKEND_HTTP2_SETTINGS_TIMEOUT: + return parse_duration(&config->http2.downstream.timeout.settings, opt, + optarg); + case SHRPX_OPTID_API_MAX_REQUEST_BODY: + return parse_uint_with_unit(&config->api.max_request_body, opt, optarg); + case SHRPX_OPTID_BACKEND_MAX_BACKOFF: + return parse_duration(&config->conn.downstream->timeout.max_backoff, opt, + optarg); + case SHRPX_OPTID_SERVER_NAME: + config->http.server_name = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_NO_SERVER_REWRITE: + config->http.no_server_rewrite = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_OPTIMIZE_WRITE_BUFFER_SIZE: + config->http2.upstream.optimize_write_buffer_size = + util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_OPTIMIZE_WINDOW_SIZE: + config->http2.upstream.optimize_window_size = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_SIZE: + if (parse_uint_with_unit(&config->http2.upstream.window_size, opt, + optarg) != 0) { + return -1; + } + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE: + if (parse_uint_with_unit(&config->http2.upstream.connection_window_size, + opt, optarg) != 0) { + return -1; + } + + return 0; + case SHRPX_OPTID_BACKEND_HTTP2_WINDOW_SIZE: + if (parse_uint_with_unit(&config->http2.downstream.window_size, opt, + optarg) != 0) { + return -1; + } + + return 0; + case SHRPX_OPTID_BACKEND_HTTP2_CONNECTION_WINDOW_SIZE: + if (parse_uint_with_unit(&config->http2.downstream.connection_window_size, + opt, optarg) != 0) { + return -1; + } + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE: + if (parse_uint_with_unit(&config->http2.upstream.encoder_dynamic_table_size, + opt, optarg) != 0) { + return -1; + } + + nghttp2_option_set_max_deflate_dynamic_table_size( + config->http2.upstream.option, + config->http2.upstream.encoder_dynamic_table_size); + nghttp2_option_set_max_deflate_dynamic_table_size( + config->http2.upstream.alt_mode_option, + config->http2.upstream.encoder_dynamic_table_size); + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE: + return parse_uint_with_unit( + &config->http2.upstream.decoder_dynamic_table_size, opt, optarg); + case SHRPX_OPTID_BACKEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE: + if (parse_uint_with_unit( + &config->http2.downstream.encoder_dynamic_table_size, opt, + optarg) != 0) { + return -1; + } + + nghttp2_option_set_max_deflate_dynamic_table_size( + config->http2.downstream.option, + config->http2.downstream.encoder_dynamic_table_size); + + return 0; + case SHRPX_OPTID_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE: + return parse_uint_with_unit( + &config->http2.downstream.decoder_dynamic_table_size, opt, optarg); + case SHRPX_OPTID_ECDH_CURVES: + config->tls.ecdh_curves = make_string_ref(config->balloc, optarg); + return 0; + case SHRPX_OPTID_TLS_SCT_DIR: +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + return read_tls_sct_from_dir(config->tls.sct_data, opt, optarg); +#else // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_BORINGSSL + LOG(WARN) + << opt + << ": ignored because underlying TLS library does not support SCT"; + return 0; +#endif // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_BORINGSSL + case SHRPX_OPTID_DNS_CACHE_TIMEOUT: + return parse_duration(&config->dns.timeout.cache, opt, optarg); + case SHRPX_OPTID_DNS_LOOKUP_TIMEOUT: + return parse_duration(&config->dns.timeout.lookup, opt, optarg); + case SHRPX_OPTID_DNS_MAX_TRY: { + int n; + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + + if (n > 5) { + LOG(ERROR) << opt << ": must be smaller than or equal to 5"; + return -1; + } + + config->dns.max_try = n; + return 0; + } + case SHRPX_OPTID_FRONTEND_KEEP_ALIVE_TIMEOUT: + return parse_duration(&config->conn.upstream.timeout.idle_read, opt, + optarg); + case SHRPX_OPTID_PSK_SECRETS: +#ifndef OPENSSL_NO_PSK + return parse_psk_secrets(config, optarg); +#else // OPENSSL_NO_PSK + LOG(WARN) + << opt + << ": ignored because underlying TLS library does not support PSK"; + return 0; +#endif // OPENSSL_NO_PSK + case SHRPX_OPTID_CLIENT_PSK_SECRETS: +#ifndef OPENSSL_NO_PSK + return parse_client_psk_secrets(config, optarg); +#else // OPENSSL_NO_PSK + LOG(WARN) + << opt + << ": ignored because underlying TLS library does not support PSK"; + return 0; +#endif // OPENSSL_NO_PSK + case SHRPX_OPTID_CLIENT_NO_HTTP2_CIPHER_BLACK_LIST: + LOG(WARN) << opt << ": deprecated. Use " + << SHRPX_OPT_CLIENT_NO_HTTP2_CIPHER_BLOCK_LIST << " instead."; + // fall through + case SHRPX_OPTID_CLIENT_NO_HTTP2_CIPHER_BLOCK_LIST: + config->tls.client.no_http2_cipher_block_list = + util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_CLIENT_CIPHERS: + config->tls.client.ciphers = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_TLS13_CLIENT_CIPHERS: + config->tls.client.tls13_ciphers = make_string_ref(config->balloc, optarg); + + return 0; + case SHRPX_OPTID_ACCESSLOG_WRITE_EARLY: + config->logging.access.write_early = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_TLS_MIN_PROTO_VERSION: + return parse_tls_proto_version(config->tls.min_proto_version, opt, optarg); + case SHRPX_OPTID_TLS_MAX_PROTO_VERSION: + return parse_tls_proto_version(config->tls.max_proto_version, opt, optarg); + case SHRPX_OPTID_REDIRECT_HTTPS_PORT: { + auto n = util::parse_uint(optarg); + if (n == -1 || n < 0 || n > 65535) { + LOG(ERROR) << opt + << ": bad value. Specify an integer in the range [0, " + "65535], inclusive"; + return -1; + } + config->http.redirect_https_port = make_string_ref(config->balloc, optarg); + return 0; + } + case SHRPX_OPTID_FRONTEND_MAX_REQUESTS: + return parse_uint(&config->http.max_requests, opt, optarg); + case SHRPX_OPTID_SINGLE_THREAD: + config->single_thread = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_SINGLE_PROCESS: + config->single_process = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_NO_ADD_X_FORWARDED_PROTO: + config->http.xfp.add = !util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_NO_STRIP_INCOMING_X_FORWARDED_PROTO: + config->http.xfp.strip_incoming = !util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_OCSP_STARTUP: + config->tls.ocsp.startup = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_NO_VERIFY_OCSP: + config->tls.ocsp.no_verify = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_VERIFY_CLIENT_TOLERATE_EXPIRED: + config->tls.client_verify.tolerate_expired = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_IGNORE_PER_PATTERN_MRUBY_ERROR: + config->ignore_per_pattern_mruby_error = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_TLS_NO_POSTPONE_EARLY_DATA: + config->tls.no_postpone_early_data = util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_TLS_MAX_EARLY_DATA: { + return parse_uint_with_unit(&config->tls.max_early_data, opt, optarg); + } + case SHRPX_OPTID_NO_STRIP_INCOMING_EARLY_DATA: + config->http.early_data.strip_incoming = !util::strieq_l("yes", optarg); + + return 0; + case SHRPX_OPTID_QUIC_BPF_PROGRAM_FILE: +#ifdef ENABLE_HTTP3 + config->quic.bpf.prog_file = make_string_ref(config->balloc, optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_NO_QUIC_BPF: +#ifdef ENABLE_HTTP3 + config->quic.bpf.disabled = util::strieq_l("yes", optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_HTTP2_ALTSVC: { + AltSvc altsvc{}; + + if (parse_altsvc(altsvc, opt, optarg) != 0) { + return -1; + } + + config->http.http2_altsvcs.push_back(std::move(altsvc)); + + return 0; + } + case SHRPX_OPTID_FRONTEND_HTTP3_READ_TIMEOUT: +#ifdef ENABLE_HTTP3 + return parse_duration(&config->conn.upstream.timeout.http3_read, opt, + optarg); +#else // !ENABLE_HTTP3 + return 0; +#endif // !ENABLE_HTTP3 + case SHRPX_OPTID_FRONTEND_QUIC_IDLE_TIMEOUT: +#ifdef ENABLE_HTTP3 + return parse_duration(&config->quic.upstream.timeout.idle, opt, optarg); +#else // !ENABLE_HTTP3 + return 0; +#endif // !ENABLE_HTTP3 + case SHRPX_OPTID_FRONTEND_QUIC_DEBUG_LOG: +#ifdef ENABLE_HTTP3 + config->quic.upstream.debug.log = util::strieq_l("yes", optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP3_WINDOW_SIZE: +#ifdef ENABLE_HTTP3 + if (parse_uint_with_unit(&config->http3.upstream.window_size, opt, + optarg) != 0) { + return -1; + } +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP3_CONNECTION_WINDOW_SIZE: +#ifdef ENABLE_HTTP3 + if (parse_uint_with_unit(&config->http3.upstream.connection_window_size, + opt, optarg) != 0) { + return -1; + } +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP3_MAX_WINDOW_SIZE: +#ifdef ENABLE_HTTP3 + if (parse_uint_with_unit(&config->http3.upstream.max_window_size, opt, + optarg) != 0) { + return -1; + } +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP3_MAX_CONNECTION_WINDOW_SIZE: +#ifdef ENABLE_HTTP3 + if (parse_uint_with_unit(&config->http3.upstream.max_connection_window_size, + opt, optarg) != 0) { + return -1; + } +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_HTTP3_MAX_CONCURRENT_STREAMS: +#ifdef ENABLE_HTTP3 + return parse_uint(&config->http3.upstream.max_concurrent_streams, opt, + optarg); +#else // !ENABLE_HTTP3 + return 0; +#endif // !ENABLE_HTTP3 + case SHRPX_OPTID_FRONTEND_QUIC_EARLY_DATA: +#ifdef ENABLE_HTTP3 + config->quic.upstream.early_data = util::strieq_l("yes", optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_QUIC_QLOG_DIR: +#ifdef ENABLE_HTTP3 + config->quic.upstream.qlog.dir = make_string_ref(config->balloc, optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_QUIC_REQUIRE_TOKEN: +#ifdef ENABLE_HTTP3 + config->quic.upstream.require_token = util::strieq_l("yes", optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_QUIC_CONGESTION_CONTROLLER: +#ifdef ENABLE_HTTP3 + if (util::strieq_l("cubic", optarg)) { + config->quic.upstream.congestion_controller = NGTCP2_CC_ALGO_CUBIC; + } else if (util::strieq_l("bbr", optarg)) { + config->quic.upstream.congestion_controller = NGTCP2_CC_ALGO_BBR; + } else { + LOG(ERROR) << opt << ": must be either cubic or bbr"; + return -1; + } +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_QUIC_SERVER_ID: +#ifdef ENABLE_HTTP3 + if (optarg.size() != config->quic.server_id.size() * 2 || + !util::is_hex_string(optarg)) { + LOG(ERROR) << opt << ": must be a hex-string"; + return -1; + } + util::decode_hex(std::begin(config->quic.server_id), optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_FRONTEND_QUIC_SECRET_FILE: +#ifdef ENABLE_HTTP3 + config->quic.upstream.secret_file = make_string_ref(config->balloc, optarg); +#endif // ENABLE_HTTP3 + + return 0; + case SHRPX_OPTID_RLIMIT_MEMLOCK: { + int n; + + if (parse_uint(&n, opt, optarg) != 0) { + return -1; + } + + if (n < 0) { + LOG(ERROR) << opt << ": specify the integer more than or equal to 0"; + + return -1; + } + + config->rlimit_memlock = n; + + return 0; + } + case SHRPX_OPTID_MAX_WORKER_PROCESSES: + return parse_uint(&config->max_worker_processes, opt, optarg); + case SHRPX_OPTID_WORKER_PROCESS_GRACE_SHUTDOWN_PERIOD: + return parse_duration(&config->worker_process_grace_shutdown_period, opt, + optarg); + case SHRPX_OPTID_FRONTEND_QUIC_INITIAL_RTT: { +#ifdef ENABLE_HTTP3 + return parse_duration(&config->quic.upstream.initial_rtt, opt, optarg); +#endif // ENABLE_HTTP3 + + return 0; + } + case SHRPX_OPTID_REQUIRE_HTTP_SCHEME: + config->http.require_http_scheme = util::strieq_l("yes", optarg); + return 0; + case SHRPX_OPTID_TLS_KTLS: + config->tls.ktls = util::strieq_l("yes", optarg); + return 0; + case SHRPX_OPTID_NPN_LIST: + LOG(WARN) << opt << ": deprecated. Use alpn-list instead."; + // fall through + case SHRPX_OPTID_ALPN_LIST: { + auto list = util::split_str(optarg, ','); + config->tls.alpn_list.resize(list.size()); + for (size_t i = 0; i < list.size(); ++i) { + config->tls.alpn_list[i] = make_string_ref(config->balloc, list[i]); + } + + return 0; + } + case SHRPX_OPTID_CONF: + LOG(WARN) << "conf: ignored"; + + return 0; + } + + LOG(ERROR) << "Unknown option: " << opt; + + return -1; +} + +int load_config(Config *config, const char *filename, + std::set<StringRef> &include_set, + std::map<StringRef, size_t> &pattern_addr_indexer) { + std::ifstream in(filename, std::ios::binary); + if (!in) { + LOG(ERROR) << "Could not open config file " << filename; + return -1; + } + std::string line; + int linenum = 0; + while (std::getline(in, line)) { + ++linenum; + if (line.empty() || line[0] == '#') { + continue; + } + auto eq = std::find(std::begin(line), std::end(line), '='); + if (eq == std::end(line)) { + LOG(ERROR) << "Bad configuration format in " << filename << " at line " + << linenum; + return -1; + } + *eq = '\0'; + + if (parse_config(config, StringRef{std::begin(line), eq}, + StringRef{eq + 1, std::end(line)}, include_set, + pattern_addr_indexer) != 0) { + return -1; + } + } + return 0; +} + +StringRef str_syslog_facility(int facility) { + switch (facility) { + case (LOG_AUTH): + return StringRef::from_lit("auth"); +#ifdef LOG_AUTHPRIV + case (LOG_AUTHPRIV): + return StringRef::from_lit("authpriv"); +#endif // LOG_AUTHPRIV + case (LOG_CRON): + return StringRef::from_lit("cron"); + case (LOG_DAEMON): + return StringRef::from_lit("daemon"); +#ifdef LOG_FTP + case (LOG_FTP): + return StringRef::from_lit("ftp"); +#endif // LOG_FTP + case (LOG_KERN): + return StringRef::from_lit("kern"); + case (LOG_LOCAL0): + return StringRef::from_lit("local0"); + case (LOG_LOCAL1): + return StringRef::from_lit("local1"); + case (LOG_LOCAL2): + return StringRef::from_lit("local2"); + case (LOG_LOCAL3): + return StringRef::from_lit("local3"); + case (LOG_LOCAL4): + return StringRef::from_lit("local4"); + case (LOG_LOCAL5): + return StringRef::from_lit("local5"); + case (LOG_LOCAL6): + return StringRef::from_lit("local6"); + case (LOG_LOCAL7): + return StringRef::from_lit("local7"); + case (LOG_LPR): + return StringRef::from_lit("lpr"); + case (LOG_MAIL): + return StringRef::from_lit("mail"); + case (LOG_SYSLOG): + return StringRef::from_lit("syslog"); + case (LOG_USER): + return StringRef::from_lit("user"); + case (LOG_UUCP): + return StringRef::from_lit("uucp"); + default: + return StringRef::from_lit("(unknown)"); + } +} + +int int_syslog_facility(const StringRef &strfacility) { + if (util::strieq_l("auth", strfacility)) { + return LOG_AUTH; + } + +#ifdef LOG_AUTHPRIV + if (util::strieq_l("authpriv", strfacility)) { + return LOG_AUTHPRIV; + } +#endif // LOG_AUTHPRIV + + if (util::strieq_l("cron", strfacility)) { + return LOG_CRON; + } + + if (util::strieq_l("daemon", strfacility)) { + return LOG_DAEMON; + } + +#ifdef LOG_FTP + if (util::strieq_l("ftp", strfacility)) { + return LOG_FTP; + } +#endif // LOG_FTP + + if (util::strieq_l("kern", strfacility)) { + return LOG_KERN; + } + + if (util::strieq_l("local0", strfacility)) { + return LOG_LOCAL0; + } + + if (util::strieq_l("local1", strfacility)) { + return LOG_LOCAL1; + } + + if (util::strieq_l("local2", strfacility)) { + return LOG_LOCAL2; + } + + if (util::strieq_l("local3", strfacility)) { + return LOG_LOCAL3; + } + + if (util::strieq_l("local4", strfacility)) { + return LOG_LOCAL4; + } + + if (util::strieq_l("local5", strfacility)) { + return LOG_LOCAL5; + } + + if (util::strieq_l("local6", strfacility)) { + return LOG_LOCAL6; + } + + if (util::strieq_l("local7", strfacility)) { + return LOG_LOCAL7; + } + + if (util::strieq_l("lpr", strfacility)) { + return LOG_LPR; + } + + if (util::strieq_l("mail", strfacility)) { + return LOG_MAIL; + } + + if (util::strieq_l("news", strfacility)) { + return LOG_NEWS; + } + + if (util::strieq_l("syslog", strfacility)) { + return LOG_SYSLOG; + } + + if (util::strieq_l("user", strfacility)) { + return LOG_USER; + } + + if (util::strieq_l("uucp", strfacility)) { + return LOG_UUCP; + } + + return -1; +} + +StringRef strproto(Proto proto) { + switch (proto) { + case Proto::NONE: + return StringRef::from_lit("none"); + case Proto::HTTP1: + return StringRef::from_lit("http/1.1"); + case Proto::HTTP2: + return StringRef::from_lit("h2"); + case Proto::HTTP3: + return StringRef::from_lit("h3"); + case Proto::MEMCACHED: + return StringRef::from_lit("memcached"); + } + + // gcc needs this. + assert(0); + abort(); +} + +namespace { +// Consistent hashing method described in +// https://github.com/RJ/ketama. Generate 160 32-bit hashes per |s|, +// which is usually backend address. The each hash is associated to +// index of backend address. When all hashes for every backend +// address are calculated, sort it in ascending order of hash. To +// choose the index, compute 32-bit hash based on client IP address, +// and do lower bound search in the array. The returned index is the +// backend to use. +int compute_affinity_hash(std::vector<AffinityHash> &res, size_t idx, + const StringRef &s) { + int rv; + std::array<uint8_t, 32> buf; + + for (auto i = 0; i < 20; ++i) { + auto t = s.str(); + t += i; + + rv = util::sha256(buf.data(), StringRef{t}); + if (rv != 0) { + return -1; + } + + for (int i = 0; i < 8; ++i) { + auto h = (static_cast<uint32_t>(buf[4 * i]) << 24) | + (static_cast<uint32_t>(buf[4 * i + 1]) << 16) | + (static_cast<uint32_t>(buf[4 * i + 2]) << 8) | + static_cast<uint32_t>(buf[4 * i + 3]); + + res.emplace_back(idx, h); + } + } + + return 0; +} +} // namespace + +// Configures the following member in |config|: +// conn.downstream_router, conn.downstream.addr_groups, +// conn.downstream.addr_group_catch_all. +int configure_downstream_group(Config *config, bool http2_proxy, + bool numeric_addr_only, + const TLSConfig &tlsconf) { + int rv; + + auto &downstreamconf = *config->conn.downstream; + auto &addr_groups = downstreamconf.addr_groups; + auto &routerconf = downstreamconf.router; + auto &router = routerconf.router; + + if (addr_groups.empty()) { + DownstreamAddrConfig addr{}; + addr.host = StringRef::from_lit(DEFAULT_DOWNSTREAM_HOST); + addr.port = DEFAULT_DOWNSTREAM_PORT; + addr.proto = Proto::HTTP1; + addr.weight = 1; + addr.group_weight = 1; + + DownstreamAddrGroupConfig g(StringRef::from_lit("/")); + g.addrs.push_back(std::move(addr)); + router.add_route(g.pattern, addr_groups.size()); + addr_groups.push_back(std::move(g)); + } + + // backward compatibility: override all SNI fields with the option + // value --backend-tls-sni-field + if (!tlsconf.backend_sni_name.empty()) { + auto &sni = tlsconf.backend_sni_name; + for (auto &addr_group : addr_groups) { + for (auto &addr : addr_group.addrs) { + addr.sni = sni; + } + } + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Resolving backend address"; + } + + ssize_t catch_all_group = -1; + for (size_t i = 0; i < addr_groups.size(); ++i) { + auto &g = addr_groups[i]; + if (g.pattern == StringRef::from_lit("/")) { + catch_all_group = i; + } + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Host-path pattern: group " << i << ": '" << g.pattern + << "'"; + for (auto &addr : g.addrs) { + LOG(INFO) << "group " << i << " -> " << addr.host.c_str() + << (addr.host_unix ? "" : ":" + util::utos(addr.port)) + << ", proto=" << strproto(addr.proto) + << (addr.tls ? ", tls" : ""); + } + } +#ifdef HAVE_MRUBY + // Try compile mruby script and catch compile error early. + if (!g.mruby_file.empty()) { + if (mruby::create_mruby_context(g.mruby_file) == nullptr) { + LOG(config->ignore_per_pattern_mruby_error ? ERROR : FATAL) + << "backend: Could not compile mruby file for pattern " + << g.pattern; + if (!config->ignore_per_pattern_mruby_error) { + return -1; + } + g.mruby_file = StringRef{}; + } + } +#endif // HAVE_MRUBY + } + +#ifdef HAVE_MRUBY + // Try compile mruby script (--mruby-file) here to catch compile + // error early. + if (!config->mruby_file.empty()) { + if (mruby::create_mruby_context(config->mruby_file) == nullptr) { + LOG(FATAL) << "mruby-file: Could not compile mruby file"; + return -1; + } + } +#endif // HAVE_MRUBY + + if (catch_all_group == -1) { + LOG(FATAL) << "backend: No catch-all backend address is configured"; + return -1; + } + + downstreamconf.addr_group_catch_all = catch_all_group; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Catch-all pattern is group " << catch_all_group; + } + + auto resolve_flags = numeric_addr_only ? AI_NUMERICHOST | AI_NUMERICSERV : 0; + + std::array<char, util::max_hostport> hostport_buf; + + for (auto &g : addr_groups) { + std::unordered_map<StringRef, uint32_t> wgchk; + for (auto &addr : g.addrs) { + if (addr.group_weight) { + auto it = wgchk.find(addr.group); + if (it == std::end(wgchk)) { + wgchk.emplace(addr.group, addr.group_weight); + } else if ((*it).second != addr.group_weight) { + LOG(FATAL) << "backend: inconsistent group-weight for a single group"; + return -1; + } + } + + if (addr.host_unix) { + // for AF_UNIX socket, we use "localhost" as host for backend + // hostport. This is used as Host header field to backend and + // not going to be passed to any syscalls. + addr.hostport = StringRef::from_lit("localhost"); + + auto path = addr.host.c_str(); + auto pathlen = addr.host.size(); + + if (pathlen + 1 > sizeof(addr.addr.su.un.sun_path)) { + LOG(FATAL) << "UNIX domain socket path " << path << " is too long > " + << sizeof(addr.addr.su.un.sun_path); + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Use UNIX domain socket path " << path + << " for backend connection"; + } + + addr.addr.su.un.sun_family = AF_UNIX; + // copy path including terminal NULL + std::copy_n(path, pathlen + 1, addr.addr.su.un.sun_path); + addr.addr.len = sizeof(addr.addr.su.un); + + continue; + } + + addr.hostport = + util::make_http_hostport(downstreamconf.balloc, addr.host, addr.port); + + auto hostport = + util::make_hostport(std::begin(hostport_buf), addr.host, addr.port); + + if (!addr.dns) { + if (resolve_hostname(&addr.addr, addr.host.c_str(), addr.port, + downstreamconf.family, resolve_flags) == -1) { + LOG(FATAL) << "Resolving backend address failed: " << hostport; + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Resolved backend address: " << hostport << " -> " + << util::to_numeric_addr(&addr.addr); + } + } else { + LOG(INFO) << "Resolving backend address " << hostport + << " takes place dynamically"; + } + } + + for (auto &addr : g.addrs) { + if (addr.group_weight == 0) { + auto it = wgchk.find(addr.group); + if (it == std::end(wgchk)) { + addr.group_weight = 1; + } else { + addr.group_weight = (*it).second; + } + } + } + + if (g.affinity.type != SessionAffinity::NONE) { + size_t idx = 0; + for (auto &addr : g.addrs) { + StringRef key; + if (addr.dns) { + if (addr.host_unix) { + key = addr.host; + } else { + key = addr.hostport; + } + } else { + auto p = reinterpret_cast<uint8_t *>(&addr.addr.su); + key = StringRef{p, addr.addr.len}; + } + rv = compute_affinity_hash(g.affinity_hash, idx, key); + if (rv != 0) { + return -1; + } + + if (g.affinity.cookie.stickiness == + SessionAffinityCookieStickiness::STRICT) { + addr.affinity_hash = util::hash32(key); + g.affinity_hash_map.emplace(addr.affinity_hash, idx); + } + + ++idx; + } + + std::sort(std::begin(g.affinity_hash), std::end(g.affinity_hash), + [](const AffinityHash &lhs, const AffinityHash &rhs) { + return lhs.hash < rhs.hash; + }); + } + + auto &timeout = g.timeout; + if (timeout.read < 1e-9) { + timeout.read = downstreamconf.timeout.read; + } + if (timeout.write < 1e-9) { + timeout.write = downstreamconf.timeout.write; + } + } + + return 0; +} + +int resolve_hostname(Address *addr, const char *hostname, uint16_t port, + int family, int additional_flags) { + int rv; + + auto service = util::utos(port); + + addrinfo hints{}; + hints.ai_family = family; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags |= additional_flags; +#ifdef AI_ADDRCONFIG + hints.ai_flags |= AI_ADDRCONFIG; +#endif // AI_ADDRCONFIG + addrinfo *res; + + rv = getaddrinfo(hostname, service.c_str(), &hints, &res); +#ifdef AI_ADDRCONFIG + if (rv != 0) { + // Retry without AI_ADDRCONFIG + hints.ai_flags &= ~AI_ADDRCONFIG; + rv = getaddrinfo(hostname, service.c_str(), &hints, &res); + } +#endif // AI_ADDRCONFIG + if (rv != 0) { + LOG(FATAL) << "Unable to resolve address for " << hostname << ": " + << gai_strerror(rv); + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + char host[NI_MAXHOST]; + rv = getnameinfo(res->ai_addr, res->ai_addrlen, host, sizeof(host), nullptr, + 0, NI_NUMERICHOST); + if (rv != 0) { + LOG(FATAL) << "Address resolution for " << hostname + << " failed: " << gai_strerror(rv); + + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Address resolution for " << hostname + << " succeeded: " << host; + } + + memcpy(&addr->su, res->ai_addr, res->ai_addrlen); + addr->len = res->ai_addrlen; + + return 0; +} + +} // namespace shrpx diff --git a/src/shrpx_config.h b/src/shrpx_config.h new file mode 100644 index 0000000..7f316eb --- /dev/null +++ b/src/shrpx_config.h @@ -0,0 +1,1450 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_CONFIG_H +#define SHRPX_CONFIG_H + +#include "shrpx.h" + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#include <sys/un.h> +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif // HAVE_ARPA_INET_H +#include <cinttypes> +#include <cstdio> +#include <vector> +#include <memory> +#include <set> +#include <unordered_map> + +#include <openssl/ssl.h> + +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#include "shrpx_router.h" +#if ENABLE_HTTP3 +# include "shrpx_quic.h" +#endif // ENABLE_HTTP3 +#include "template.h" +#include "http2.h" +#include "network.h" +#include "allocator.h" + +using namespace nghttp2; + +namespace shrpx { + +struct LogFragment; +class ConnectBlocker; +class Http2Session; + +namespace tls { + +class CertLookupTree; + +} // namespace tls + +constexpr auto SHRPX_OPT_PRIVATE_KEY_FILE = + StringRef::from_lit("private-key-file"); +constexpr auto SHRPX_OPT_PRIVATE_KEY_PASSWD_FILE = + StringRef::from_lit("private-key-passwd-file"); +constexpr auto SHRPX_OPT_CERTIFICATE_FILE = + StringRef::from_lit("certificate-file"); +constexpr auto SHRPX_OPT_DH_PARAM_FILE = StringRef::from_lit("dh-param-file"); +constexpr auto SHRPX_OPT_SUBCERT = StringRef::from_lit("subcert"); +constexpr auto SHRPX_OPT_BACKEND = StringRef::from_lit("backend"); +constexpr auto SHRPX_OPT_FRONTEND = StringRef::from_lit("frontend"); +constexpr auto SHRPX_OPT_WORKERS = StringRef::from_lit("workers"); +constexpr auto SHRPX_OPT_HTTP2_MAX_CONCURRENT_STREAMS = + StringRef::from_lit("http2-max-concurrent-streams"); +constexpr auto SHRPX_OPT_LOG_LEVEL = StringRef::from_lit("log-level"); +constexpr auto SHRPX_OPT_DAEMON = StringRef::from_lit("daemon"); +constexpr auto SHRPX_OPT_HTTP2_PROXY = StringRef::from_lit("http2-proxy"); +constexpr auto SHRPX_OPT_HTTP2_BRIDGE = StringRef::from_lit("http2-bridge"); +constexpr auto SHRPX_OPT_CLIENT_PROXY = StringRef::from_lit("client-proxy"); +constexpr auto SHRPX_OPT_ADD_X_FORWARDED_FOR = + StringRef::from_lit("add-x-forwarded-for"); +constexpr auto SHRPX_OPT_STRIP_INCOMING_X_FORWARDED_FOR = + StringRef::from_lit("strip-incoming-x-forwarded-for"); +constexpr auto SHRPX_OPT_NO_VIA = StringRef::from_lit("no-via"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_READ_TIMEOUT = + StringRef::from_lit("frontend-http2-read-timeout"); +constexpr auto SHRPX_OPT_FRONTEND_READ_TIMEOUT = + StringRef::from_lit("frontend-read-timeout"); +constexpr auto SHRPX_OPT_FRONTEND_WRITE_TIMEOUT = + StringRef::from_lit("frontend-write-timeout"); +constexpr auto SHRPX_OPT_BACKEND_READ_TIMEOUT = + StringRef::from_lit("backend-read-timeout"); +constexpr auto SHRPX_OPT_BACKEND_WRITE_TIMEOUT = + StringRef::from_lit("backend-write-timeout"); +constexpr auto SHRPX_OPT_STREAM_READ_TIMEOUT = + StringRef::from_lit("stream-read-timeout"); +constexpr auto SHRPX_OPT_STREAM_WRITE_TIMEOUT = + StringRef::from_lit("stream-write-timeout"); +constexpr auto SHRPX_OPT_ACCESSLOG_FILE = StringRef::from_lit("accesslog-file"); +constexpr auto SHRPX_OPT_ACCESSLOG_SYSLOG = + StringRef::from_lit("accesslog-syslog"); +constexpr auto SHRPX_OPT_ACCESSLOG_FORMAT = + StringRef::from_lit("accesslog-format"); +constexpr auto SHRPX_OPT_ERRORLOG_FILE = StringRef::from_lit("errorlog-file"); +constexpr auto SHRPX_OPT_ERRORLOG_SYSLOG = + StringRef::from_lit("errorlog-syslog"); +constexpr auto SHRPX_OPT_BACKEND_KEEP_ALIVE_TIMEOUT = + StringRef::from_lit("backend-keep-alive-timeout"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_WINDOW_BITS = + StringRef::from_lit("frontend-http2-window-bits"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_WINDOW_BITS = + StringRef::from_lit("backend-http2-window-bits"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS = + StringRef::from_lit("frontend-http2-connection-window-bits"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_CONNECTION_WINDOW_BITS = + StringRef::from_lit("backend-http2-connection-window-bits"); +constexpr auto SHRPX_OPT_FRONTEND_NO_TLS = + StringRef::from_lit("frontend-no-tls"); +constexpr auto SHRPX_OPT_BACKEND_NO_TLS = StringRef::from_lit("backend-no-tls"); +constexpr auto SHRPX_OPT_BACKEND_TLS_SNI_FIELD = + StringRef::from_lit("backend-tls-sni-field"); +constexpr auto SHRPX_OPT_PID_FILE = StringRef::from_lit("pid-file"); +constexpr auto SHRPX_OPT_USER = StringRef::from_lit("user"); +constexpr auto SHRPX_OPT_SYSLOG_FACILITY = + StringRef::from_lit("syslog-facility"); +constexpr auto SHRPX_OPT_BACKLOG = StringRef::from_lit("backlog"); +constexpr auto SHRPX_OPT_CIPHERS = StringRef::from_lit("ciphers"); +constexpr auto SHRPX_OPT_CLIENT = StringRef::from_lit("client"); +constexpr auto SHRPX_OPT_INSECURE = StringRef::from_lit("insecure"); +constexpr auto SHRPX_OPT_CACERT = StringRef::from_lit("cacert"); +constexpr auto SHRPX_OPT_BACKEND_IPV4 = StringRef::from_lit("backend-ipv4"); +constexpr auto SHRPX_OPT_BACKEND_IPV6 = StringRef::from_lit("backend-ipv6"); +constexpr auto SHRPX_OPT_BACKEND_HTTP_PROXY_URI = + StringRef::from_lit("backend-http-proxy-uri"); +constexpr auto SHRPX_OPT_READ_RATE = StringRef::from_lit("read-rate"); +constexpr auto SHRPX_OPT_READ_BURST = StringRef::from_lit("read-burst"); +constexpr auto SHRPX_OPT_WRITE_RATE = StringRef::from_lit("write-rate"); +constexpr auto SHRPX_OPT_WRITE_BURST = StringRef::from_lit("write-burst"); +constexpr auto SHRPX_OPT_WORKER_READ_RATE = + StringRef::from_lit("worker-read-rate"); +constexpr auto SHRPX_OPT_WORKER_READ_BURST = + StringRef::from_lit("worker-read-burst"); +constexpr auto SHRPX_OPT_WORKER_WRITE_RATE = + StringRef::from_lit("worker-write-rate"); +constexpr auto SHRPX_OPT_WORKER_WRITE_BURST = + StringRef::from_lit("worker-write-burst"); +constexpr auto SHRPX_OPT_NPN_LIST = StringRef::from_lit("npn-list"); +constexpr auto SHRPX_OPT_TLS_PROTO_LIST = StringRef::from_lit("tls-proto-list"); +constexpr auto SHRPX_OPT_VERIFY_CLIENT = StringRef::from_lit("verify-client"); +constexpr auto SHRPX_OPT_VERIFY_CLIENT_CACERT = + StringRef::from_lit("verify-client-cacert"); +constexpr auto SHRPX_OPT_CLIENT_PRIVATE_KEY_FILE = + StringRef::from_lit("client-private-key-file"); +constexpr auto SHRPX_OPT_CLIENT_CERT_FILE = + StringRef::from_lit("client-cert-file"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_DUMP_REQUEST_HEADER = + StringRef::from_lit("frontend-http2-dump-request-header"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER = + StringRef::from_lit("frontend-http2-dump-response-header"); +constexpr auto SHRPX_OPT_HTTP2_NO_COOKIE_CRUMBLING = + StringRef::from_lit("http2-no-cookie-crumbling"); +constexpr auto SHRPX_OPT_FRONTEND_FRAME_DEBUG = + StringRef::from_lit("frontend-frame-debug"); +constexpr auto SHRPX_OPT_PADDING = StringRef::from_lit("padding"); +constexpr auto SHRPX_OPT_ALTSVC = StringRef::from_lit("altsvc"); +constexpr auto SHRPX_OPT_ADD_REQUEST_HEADER = + StringRef::from_lit("add-request-header"); +constexpr auto SHRPX_OPT_ADD_RESPONSE_HEADER = + StringRef::from_lit("add-response-header"); +constexpr auto SHRPX_OPT_WORKER_FRONTEND_CONNECTIONS = + StringRef::from_lit("worker-frontend-connections"); +constexpr auto SHRPX_OPT_NO_LOCATION_REWRITE = + StringRef::from_lit("no-location-rewrite"); +constexpr auto SHRPX_OPT_NO_HOST_REWRITE = + StringRef::from_lit("no-host-rewrite"); +constexpr auto SHRPX_OPT_BACKEND_HTTP1_CONNECTIONS_PER_HOST = + StringRef::from_lit("backend-http1-connections-per-host"); +constexpr auto SHRPX_OPT_BACKEND_HTTP1_CONNECTIONS_PER_FRONTEND = + StringRef::from_lit("backend-http1-connections-per-frontend"); +constexpr auto SHRPX_OPT_LISTENER_DISABLE_TIMEOUT = + StringRef::from_lit("listener-disable-timeout"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_FILE = + StringRef::from_lit("tls-ticket-key-file"); +constexpr auto SHRPX_OPT_RLIMIT_NOFILE = StringRef::from_lit("rlimit-nofile"); +constexpr auto SHRPX_OPT_BACKEND_REQUEST_BUFFER = + StringRef::from_lit("backend-request-buffer"); +constexpr auto SHRPX_OPT_BACKEND_RESPONSE_BUFFER = + StringRef::from_lit("backend-response-buffer"); +constexpr auto SHRPX_OPT_NO_SERVER_PUSH = StringRef::from_lit("no-server-push"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER = + StringRef::from_lit("backend-http2-connections-per-worker"); +constexpr auto SHRPX_OPT_FETCH_OCSP_RESPONSE_FILE = + StringRef::from_lit("fetch-ocsp-response-file"); +constexpr auto SHRPX_OPT_OCSP_UPDATE_INTERVAL = + StringRef::from_lit("ocsp-update-interval"); +constexpr auto SHRPX_OPT_NO_OCSP = StringRef::from_lit("no-ocsp"); +constexpr auto SHRPX_OPT_HEADER_FIELD_BUFFER = + StringRef::from_lit("header-field-buffer"); +constexpr auto SHRPX_OPT_MAX_HEADER_FIELDS = + StringRef::from_lit("max-header-fields"); +constexpr auto SHRPX_OPT_INCLUDE = StringRef::from_lit("include"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_CIPHER = + StringRef::from_lit("tls-ticket-key-cipher"); +constexpr auto SHRPX_OPT_HOST_REWRITE = StringRef::from_lit("host-rewrite"); +constexpr auto SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED = + StringRef::from_lit("tls-session-cache-memcached"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED = + StringRef::from_lit("tls-ticket-key-memcached"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_INTERVAL = + StringRef::from_lit("tls-ticket-key-memcached-interval"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_RETRY = + StringRef::from_lit("tls-ticket-key-memcached-max-retry"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL = + StringRef::from_lit("tls-ticket-key-memcached-max-fail"); +constexpr auto SHRPX_OPT_MRUBY_FILE = StringRef::from_lit("mruby-file"); +constexpr auto SHRPX_OPT_ACCEPT_PROXY_PROTOCOL = + StringRef::from_lit("accept-proxy-protocol"); +constexpr auto SHRPX_OPT_FASTOPEN = StringRef::from_lit("fastopen"); +constexpr auto SHRPX_OPT_TLS_DYN_REC_WARMUP_THRESHOLD = + StringRef::from_lit("tls-dyn-rec-warmup-threshold"); +constexpr auto SHRPX_OPT_TLS_DYN_REC_IDLE_TIMEOUT = + StringRef::from_lit("tls-dyn-rec-idle-timeout"); +constexpr auto SHRPX_OPT_ADD_FORWARDED = StringRef::from_lit("add-forwarded"); +constexpr auto SHRPX_OPT_STRIP_INCOMING_FORWARDED = + StringRef::from_lit("strip-incoming-forwarded"); +constexpr auto SHRPX_OPT_FORWARDED_BY = StringRef::from_lit("forwarded-by"); +constexpr auto SHRPX_OPT_FORWARDED_FOR = StringRef::from_lit("forwarded-for"); +constexpr auto SHRPX_OPT_REQUEST_HEADER_FIELD_BUFFER = + StringRef::from_lit("request-header-field-buffer"); +constexpr auto SHRPX_OPT_MAX_REQUEST_HEADER_FIELDS = + StringRef::from_lit("max-request-header-fields"); +constexpr auto SHRPX_OPT_RESPONSE_HEADER_FIELD_BUFFER = + StringRef::from_lit("response-header-field-buffer"); +constexpr auto SHRPX_OPT_MAX_RESPONSE_HEADER_FIELDS = + StringRef::from_lit("max-response-header-fields"); +constexpr auto SHRPX_OPT_NO_HTTP2_CIPHER_BLOCK_LIST = + StringRef::from_lit("no-http2-cipher-block-list"); +constexpr auto SHRPX_OPT_NO_HTTP2_CIPHER_BLACK_LIST = + StringRef::from_lit("no-http2-cipher-black-list"); +constexpr auto SHRPX_OPT_BACKEND_HTTP1_TLS = + StringRef::from_lit("backend-http1-tls"); +constexpr auto SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_TLS = + StringRef::from_lit("tls-session-cache-memcached-tls"); +constexpr auto SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_CERT_FILE = + StringRef::from_lit("tls-session-cache-memcached-cert-file"); +constexpr auto SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_PRIVATE_KEY_FILE = + StringRef::from_lit("tls-session-cache-memcached-private-key-file"); +constexpr auto SHRPX_OPT_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY = + StringRef::from_lit("tls-session-cache-memcached-address-family"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_TLS = + StringRef::from_lit("tls-ticket-key-memcached-tls"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_CERT_FILE = + StringRef::from_lit("tls-ticket-key-memcached-cert-file"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_PRIVATE_KEY_FILE = + StringRef::from_lit("tls-ticket-key-memcached-private-key-file"); +constexpr auto SHRPX_OPT_TLS_TICKET_KEY_MEMCACHED_ADDRESS_FAMILY = + StringRef::from_lit("tls-ticket-key-memcached-address-family"); +constexpr auto SHRPX_OPT_BACKEND_ADDRESS_FAMILY = + StringRef::from_lit("backend-address-family"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_MAX_CONCURRENT_STREAMS = + StringRef::from_lit("frontend-http2-max-concurrent-streams"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS = + StringRef::from_lit("backend-http2-max-concurrent-streams"); +constexpr auto SHRPX_OPT_BACKEND_CONNECTIONS_PER_FRONTEND = + StringRef::from_lit("backend-connections-per-frontend"); +constexpr auto SHRPX_OPT_BACKEND_TLS = StringRef::from_lit("backend-tls"); +constexpr auto SHRPX_OPT_BACKEND_CONNECTIONS_PER_HOST = + StringRef::from_lit("backend-connections-per-host"); +constexpr auto SHRPX_OPT_ERROR_PAGE = StringRef::from_lit("error-page"); +constexpr auto SHRPX_OPT_NO_KQUEUE = StringRef::from_lit("no-kqueue"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_SETTINGS_TIMEOUT = + StringRef::from_lit("frontend-http2-settings-timeout"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_SETTINGS_TIMEOUT = + StringRef::from_lit("backend-http2-settings-timeout"); +constexpr auto SHRPX_OPT_API_MAX_REQUEST_BODY = + StringRef::from_lit("api-max-request-body"); +constexpr auto SHRPX_OPT_BACKEND_MAX_BACKOFF = + StringRef::from_lit("backend-max-backoff"); +constexpr auto SHRPX_OPT_SERVER_NAME = StringRef::from_lit("server-name"); +constexpr auto SHRPX_OPT_NO_SERVER_REWRITE = + StringRef::from_lit("no-server-rewrite"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_OPTIMIZE_WRITE_BUFFER_SIZE = + StringRef::from_lit("frontend-http2-optimize-write-buffer-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_OPTIMIZE_WINDOW_SIZE = + StringRef::from_lit("frontend-http2-optimize-window-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_WINDOW_SIZE = + StringRef::from_lit("frontend-http2-window-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE = + StringRef::from_lit("frontend-http2-connection-window-size"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_WINDOW_SIZE = + StringRef::from_lit("backend-http2-window-size"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_CONNECTION_WINDOW_SIZE = + StringRef::from_lit("backend-http2-connection-window-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE = + StringRef::from_lit("frontend-http2-encoder-dynamic-table-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE = + StringRef::from_lit("frontend-http2-decoder-dynamic-table-size"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE = + StringRef::from_lit("backend-http2-encoder-dynamic-table-size"); +constexpr auto SHRPX_OPT_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE = + StringRef::from_lit("backend-http2-decoder-dynamic-table-size"); +constexpr auto SHRPX_OPT_ECDH_CURVES = StringRef::from_lit("ecdh-curves"); +constexpr auto SHRPX_OPT_TLS_SCT_DIR = StringRef::from_lit("tls-sct-dir"); +constexpr auto SHRPX_OPT_BACKEND_CONNECT_TIMEOUT = + StringRef::from_lit("backend-connect-timeout"); +constexpr auto SHRPX_OPT_DNS_CACHE_TIMEOUT = + StringRef::from_lit("dns-cache-timeout"); +constexpr auto SHRPX_OPT_DNS_LOOKUP_TIMEOUT = + StringRef::from_lit("dns-lookup-timeout"); +constexpr auto SHRPX_OPT_DNS_MAX_TRY = StringRef::from_lit("dns-max-try"); +constexpr auto SHRPX_OPT_FRONTEND_KEEP_ALIVE_TIMEOUT = + StringRef::from_lit("frontend-keep-alive-timeout"); +constexpr auto SHRPX_OPT_PSK_SECRETS = StringRef::from_lit("psk-secrets"); +constexpr auto SHRPX_OPT_CLIENT_PSK_SECRETS = + StringRef::from_lit("client-psk-secrets"); +constexpr auto SHRPX_OPT_CLIENT_NO_HTTP2_CIPHER_BLOCK_LIST = + StringRef::from_lit("client-no-http2-cipher-block-list"); +constexpr auto SHRPX_OPT_CLIENT_NO_HTTP2_CIPHER_BLACK_LIST = + StringRef::from_lit("client-no-http2-cipher-black-list"); +constexpr auto SHRPX_OPT_CLIENT_CIPHERS = StringRef::from_lit("client-ciphers"); +constexpr auto SHRPX_OPT_ACCESSLOG_WRITE_EARLY = + StringRef::from_lit("accesslog-write-early"); +constexpr auto SHRPX_OPT_TLS_MIN_PROTO_VERSION = + StringRef::from_lit("tls-min-proto-version"); +constexpr auto SHRPX_OPT_TLS_MAX_PROTO_VERSION = + StringRef::from_lit("tls-max-proto-version"); +constexpr auto SHRPX_OPT_REDIRECT_HTTPS_PORT = + StringRef::from_lit("redirect-https-port"); +constexpr auto SHRPX_OPT_FRONTEND_MAX_REQUESTS = + StringRef::from_lit("frontend-max-requests"); +constexpr auto SHRPX_OPT_SINGLE_THREAD = StringRef::from_lit("single-thread"); +constexpr auto SHRPX_OPT_SINGLE_PROCESS = StringRef::from_lit("single-process"); +constexpr auto SHRPX_OPT_NO_ADD_X_FORWARDED_PROTO = + StringRef::from_lit("no-add-x-forwarded-proto"); +constexpr auto SHRPX_OPT_NO_STRIP_INCOMING_X_FORWARDED_PROTO = + StringRef::from_lit("no-strip-incoming-x-forwarded-proto"); +constexpr auto SHRPX_OPT_OCSP_STARTUP = StringRef::from_lit("ocsp-startup"); +constexpr auto SHRPX_OPT_NO_VERIFY_OCSP = StringRef::from_lit("no-verify-ocsp"); +constexpr auto SHRPX_OPT_VERIFY_CLIENT_TOLERATE_EXPIRED = + StringRef::from_lit("verify-client-tolerate-expired"); +constexpr auto SHRPX_OPT_IGNORE_PER_PATTERN_MRUBY_ERROR = + StringRef::from_lit("ignore-per-pattern-mruby-error"); +constexpr auto SHRPX_OPT_TLS_NO_POSTPONE_EARLY_DATA = + StringRef::from_lit("tls-no-postpone-early-data"); +constexpr auto SHRPX_OPT_TLS_MAX_EARLY_DATA = + StringRef::from_lit("tls-max-early-data"); +constexpr auto SHRPX_OPT_TLS13_CIPHERS = StringRef::from_lit("tls13-ciphers"); +constexpr auto SHRPX_OPT_TLS13_CLIENT_CIPHERS = + StringRef::from_lit("tls13-client-ciphers"); +constexpr auto SHRPX_OPT_NO_STRIP_INCOMING_EARLY_DATA = + StringRef::from_lit("no-strip-incoming-early-data"); +constexpr auto SHRPX_OPT_QUIC_BPF_PROGRAM_FILE = + StringRef::from_lit("quic-bpf-program-file"); +constexpr auto SHRPX_OPT_NO_QUIC_BPF = StringRef::from_lit("no-quic-bpf"); +constexpr auto SHRPX_OPT_HTTP2_ALTSVC = StringRef::from_lit("http2-altsvc"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP3_READ_TIMEOUT = + StringRef::from_lit("frontend-http3-read-timeout"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_IDLE_TIMEOUT = + StringRef::from_lit("frontend-quic-idle-timeout"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_DEBUG_LOG = + StringRef::from_lit("frontend-quic-debug-log"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP3_WINDOW_SIZE = + StringRef::from_lit("frontend-http3-window-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP3_CONNECTION_WINDOW_SIZE = + StringRef::from_lit("frontend-http3-connection-window-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP3_MAX_WINDOW_SIZE = + StringRef::from_lit("frontend-http3-max-window-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP3_MAX_CONNECTION_WINDOW_SIZE = + StringRef::from_lit("frontend-http3-max-connection-window-size"); +constexpr auto SHRPX_OPT_FRONTEND_HTTP3_MAX_CONCURRENT_STREAMS = + StringRef::from_lit("frontend-http3-max-concurrent-streams"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_EARLY_DATA = + StringRef::from_lit("frontend-quic-early-data"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_QLOG_DIR = + StringRef::from_lit("frontend-quic-qlog-dir"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_REQUIRE_TOKEN = + StringRef::from_lit("frontend-quic-require-token"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER = + StringRef::from_lit("frontend-quic-congestion-controller"); +constexpr auto SHRPX_OPT_QUIC_SERVER_ID = StringRef::from_lit("quic-server-id"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_SECRET_FILE = + StringRef::from_lit("frontend-quic-secret-file"); +constexpr auto SHRPX_OPT_RLIMIT_MEMLOCK = StringRef::from_lit("rlimit-memlock"); +constexpr auto SHRPX_OPT_MAX_WORKER_PROCESSES = + StringRef::from_lit("max-worker-processes"); +constexpr auto SHRPX_OPT_WORKER_PROCESS_GRACE_SHUTDOWN_PERIOD = + StringRef::from_lit("worker-process-grace-shutdown-period"); +constexpr auto SHRPX_OPT_FRONTEND_QUIC_INITIAL_RTT = + StringRef::from_lit("frontend-quic-initial-rtt"); +constexpr auto SHRPX_OPT_REQUIRE_HTTP_SCHEME = + StringRef::from_lit("require-http-scheme"); +constexpr auto SHRPX_OPT_TLS_KTLS = StringRef::from_lit("tls-ktls"); +constexpr auto SHRPX_OPT_ALPN_LIST = StringRef::from_lit("alpn-list"); + +constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8; + +constexpr char DEFAULT_DOWNSTREAM_HOST[] = "127.0.0.1"; +constexpr int16_t DEFAULT_DOWNSTREAM_PORT = 80; + +enum class Proto { + NONE, + HTTP1, + HTTP2, + HTTP3, + MEMCACHED, +}; + +enum class SessionAffinity { + // No session affinity + NONE, + // Client IP affinity + IP, + // Cookie based affinity + COOKIE, +}; + +enum class SessionAffinityCookieSecure { + // Secure attribute of session affinity cookie is determined by the + // request scheme. + AUTO, + // Secure attribute of session affinity cookie is always set. + YES, + // Secure attribute of session affinity cookie is always unset. + NO, +}; + +enum class SessionAffinityCookieStickiness { + // Backend server might be changed when an existing backend server + // is removed, or new backend server is added. + LOOSE, + // Backend server might be changed when a designated backend server + // is removed, but adding new backend server does not cause + // breakage. + STRICT, +}; + +struct AffinityConfig { + // Type of session affinity. + SessionAffinity type; + struct { + // Name of a cookie to use. + StringRef name; + // Path which a cookie is applied to. + StringRef path; + // Secure attribute + SessionAffinityCookieSecure secure; + // Affinity Stickiness + SessionAffinityCookieStickiness stickiness; + } cookie; +}; + +enum shrpx_forwarded_param { + FORWARDED_NONE = 0, + FORWARDED_BY = 0x1, + FORWARDED_FOR = 0x2, + FORWARDED_HOST = 0x4, + FORWARDED_PROTO = 0x8, +}; + +enum class ForwardedNode { + OBFUSCATED, + IP, +}; + +struct AltSvc { + StringRef protocol_id, host, origin, service, params; + + uint16_t port; +}; + +enum class UpstreamAltMode { + // No alternative mode + NONE, + // API processing mode + API, + // Health monitor mode + HEALTHMON, +}; + +struct UpstreamAddr { + // The unique index of this address. + size_t index; + // The frontend address (e.g., FQDN, hostname, IP address). If + // |host_unix| is true, this is UNIX domain socket path. This must + // be NULL terminated string. + StringRef host; + // For TCP socket, this is <IP address>:<PORT>. For IPv6 address, + // address is surrounded by square brackets. If socket is UNIX + // domain socket, this is "localhost". + StringRef hostport; + // frontend port. 0 if |host_unix| is true. + uint16_t port; + // For TCP socket, this is either AF_INET or AF_INET6. For UNIX + // domain socket, this is 0. + int family; + // Alternate mode + UpstreamAltMode alt_mode; + // true if |host| contains UNIX domain socket path. + bool host_unix; + // true if TLS is enabled. + bool tls; + // true if SNI host should be used as a host when selecting backend + // server. + bool sni_fwd; + // true if client is supposed to send PROXY protocol v1 header. + bool accept_proxy_protocol; + bool quic; + int fd; +}; + +struct DownstreamAddrConfig { + // Resolved address if |dns| is false + Address addr; + // backend address. If |host_unix| is true, this is UNIX domain + // socket path. This must be NULL terminated string. + StringRef host; + // <HOST>:<PORT>. This does not treat 80 and 443 specially. If + // |host_unix| is true, this is "localhost". + StringRef hostport; + // hostname sent as SNI field + StringRef sni; + // name of group which this address belongs to. + StringRef group; + size_t fall; + size_t rise; + // weight of this address inside a weight group. Its range is [1, + // 256], inclusive. + uint32_t weight; + // weight of the weight group. Its range is [1, 256], inclusive. + uint32_t group_weight; + // affinity hash for this address. It is assigned when strict + // stickiness is enabled. + uint32_t affinity_hash; + // Application protocol used in this group + Proto proto; + // backend port. 0 if |host_unix| is true. + uint16_t port; + // true if |host| contains UNIX domain socket path. + bool host_unix; + bool tls; + // true if dynamic DNS is enabled + bool dns; + // true if :scheme pseudo header field should be upgraded to secure + // variant (e.g., "https") when forwarding request to a backend + // connected by TLS connection. + bool upgrade_scheme; + // true if a request should not be forwarded to a backend. + bool dnf; +}; + +// Mapping hash to idx which is an index into +// DownstreamAddrGroupConfig::addrs. +struct AffinityHash { + AffinityHash(size_t idx, uint32_t hash) : idx(idx), hash(hash) {} + + size_t idx; + uint32_t hash; +}; + +struct DownstreamAddrGroupConfig { + DownstreamAddrGroupConfig(const StringRef &pattern) + : pattern(pattern), + affinity{SessionAffinity::NONE}, + redirect_if_not_tls(false), + dnf{false}, + timeout{} {} + + StringRef pattern; + StringRef mruby_file; + std::vector<DownstreamAddrConfig> addrs; + // Bunch of session affinity hash. Only used if affinity == + // SessionAffinity::IP. + std::vector<AffinityHash> affinity_hash; + // Maps affinity hash of each DownstreamAddrConfig to its index in + // addrs. It is only assigned when strict stickiness is enabled. + std::unordered_map<uint32_t, size_t> affinity_hash_map; + // Cookie based session affinity configuration. + AffinityConfig affinity; + // true if this group requires that client connection must be TLS, + // and the request must be redirected to https URI. + bool redirect_if_not_tls; + // true if a request should not be forwarded to a backend. + bool dnf; + // Timeouts for backend connection. + struct { + ev_tstamp read; + ev_tstamp write; + } timeout; +}; + +struct TicketKey { + const EVP_CIPHER *cipher; + const EVP_MD *hmac; + size_t hmac_keylen; + struct { + // name of this ticket configuration + std::array<uint8_t, 16> name; + // encryption key for |cipher| + std::array<uint8_t, 32> enc_key; + // hmac key for |hmac| + std::array<uint8_t, 32> hmac_key; + } data; +}; + +struct TicketKeys { + ~TicketKeys(); + std::vector<TicketKey> keys; +}; + +struct TLSCertificate { + TLSCertificate(StringRef private_key_file, StringRef cert_file, + std::vector<uint8_t> sct_data) + : private_key_file(std::move(private_key_file)), + cert_file(std::move(cert_file)), + sct_data(std::move(sct_data)) {} + + StringRef private_key_file; + StringRef cert_file; + std::vector<uint8_t> sct_data; +}; + +#ifdef ENABLE_HTTP3 +struct QUICKeyingMaterial { + std::array<uint8_t, SHRPX_QUIC_SECRET_RESERVEDLEN> reserved; + std::array<uint8_t, SHRPX_QUIC_SECRETLEN> secret; + std::array<uint8_t, SHRPX_QUIC_SALTLEN> salt; + std::array<uint8_t, SHRPX_QUIC_CID_ENCRYPTION_KEYLEN> cid_encryption_key; + // Identifier of this keying material. Only the first 2 bits are + // used. + uint8_t id; +}; + +struct QUICKeyingMaterials { + std::vector<QUICKeyingMaterial> keying_materials; +}; +#endif // ENABLE_HTTP3 + +struct HttpProxy { + Address addr; + // host in http proxy URI + StringRef host; + // userinfo in http proxy URI, not percent-encoded form + StringRef userinfo; + // port in http proxy URI + uint16_t port; +}; + +struct TLSConfig { + // RFC 5077 Session ticket related configurations + struct { + struct { + Address addr; + uint16_t port; + // Hostname of memcached server. This is also used as SNI field + // if TLS is enabled. + StringRef host; + // Client private key and certificate for authentication + StringRef private_key_file; + StringRef cert_file; + ev_tstamp interval; + // Maximum number of retries when getting TLS ticket key from + // mamcached, due to network error. + size_t max_retry; + // Maximum number of consecutive error from memcached, when this + // limit reached, TLS ticket is disabled. + size_t max_fail; + // Address family of memcached connection. One of either + // AF_INET, AF_INET6 or AF_UNSPEC. + int family; + bool tls; + } memcached; + std::vector<StringRef> files; + const EVP_CIPHER *cipher; + // true if --tls-ticket-key-cipher is used + bool cipher_given; + } ticket; + + // Session cache related configurations + struct { + struct { + Address addr; + uint16_t port; + // Hostname of memcached server. This is also used as SNI field + // if TLS is enabled. + StringRef host; + // Client private key and certificate for authentication + StringRef private_key_file; + StringRef cert_file; + // Address family of memcached connection. One of either + // AF_INET, AF_INET6 or AF_UNSPEC. + int family; + bool tls; + } memcached; + } session_cache; + + // Dynamic record sizing configurations + struct { + size_t warmup_threshold; + ev_tstamp idle_timeout; + } dyn_rec; + + // OCSP related configurations + struct { + ev_tstamp update_interval; + StringRef fetch_ocsp_response_file; + bool disabled; + bool startup; + bool no_verify; + } ocsp; + + // Client verification configurations + struct { + // Path to file containing CA certificate solely used for client + // certificate validation + StringRef cacert; + bool enabled; + // true if we accept an expired client certificate. + bool tolerate_expired; + } client_verify; + + // Client (backend connection) TLS configuration. + struct { + // Client PSK configuration + struct { + // identity must be NULL terminated string. + StringRef identity; + StringRef secret; + } psk; + StringRef private_key_file; + StringRef cert_file; + StringRef ciphers; + StringRef tls13_ciphers; + bool no_http2_cipher_block_list; + } client; + + // PSK secrets. The key is identity, and the associated value is + // its secret. + std::map<StringRef, StringRef> psk_secrets; + // The list of additional TLS certificate pair + std::vector<TLSCertificate> subcerts; + std::vector<unsigned char> alpn_prefs; + // list of supported ALPN protocol strings in the order of + // preference. + std::vector<StringRef> alpn_list; + // list of supported SSL/TLS protocol strings. + std::vector<StringRef> tls_proto_list; + std::vector<uint8_t> sct_data; + BIO_METHOD *bio_method; + // Bit mask to disable SSL/TLS protocol versions. This will be + // passed to SSL_CTX_set_options(). + long int tls_proto_mask; + StringRef backend_sni_name; + std::chrono::seconds session_timeout; + StringRef private_key_file; + StringRef private_key_passwd; + StringRef cert_file; + StringRef dh_param_file; + StringRef ciphers; + StringRef tls13_ciphers; + StringRef ecdh_curves; + StringRef cacert; + // The maximum amount of 0-RTT data that server accepts. + uint32_t max_early_data; + // The minimum and maximum TLS version. These values are defined in + // OpenSSL header file. + int min_proto_version; + int max_proto_version; + bool insecure; + bool no_http2_cipher_block_list; + // true if forwarding requests included in TLS early data should not + // be postponed until TLS handshake finishes. + bool no_postpone_early_data; + bool ktls; +}; + +#ifdef ENABLE_HTTP3 +struct QUICConfig { + struct { + struct { + ev_tstamp idle; + } timeout; + struct { + bool log; + } debug; + struct { + StringRef dir; + } qlog; + ngtcp2_cc_algo congestion_controller; + bool early_data; + bool require_token; + StringRef secret_file; + ev_tstamp initial_rtt; + } upstream; + struct { + StringRef prog_file; + bool disabled; + } bpf; + std::array<uint8_t, SHRPX_QUIC_SERVER_IDLEN> server_id; +}; + +struct Http3Config { + struct { + size_t max_concurrent_streams; + int32_t window_size; + int32_t connection_window_size; + int32_t max_window_size; + int32_t max_connection_window_size; + } upstream; +}; +#endif // ENABLE_HTTP3 + +// custom error page +struct ErrorPage { + // not NULL-terminated + std::vector<uint8_t> content; + // 0 is special value, and it matches all HTTP status code. + unsigned int http_status; +}; + +struct HttpConfig { + struct { + // obfuscated value used in "by" parameter of Forwarded header + // field. This is only used when user defined static obfuscated + // string is provided. + StringRef by_obfuscated; + // bitwise-OR of one or more of shrpx_forwarded_param values. + uint32_t params; + // type of value recorded in "by" parameter of Forwarded header + // field. + ForwardedNode by_node_type; + // type of value recorded in "for" parameter of Forwarded header + // field. + ForwardedNode for_node_type; + bool strip_incoming; + } forwarded; + struct { + bool add; + bool strip_incoming; + } xff; + struct { + bool add; + bool strip_incoming; + } xfp; + struct { + bool strip_incoming; + } early_data; + std::vector<AltSvc> altsvcs; + // altsvcs serialized in a wire format. + StringRef altsvc_header_value; + std::vector<AltSvc> http2_altsvcs; + // http2_altsvcs serialized in a wire format. + StringRef http2_altsvc_header_value; + std::vector<ErrorPage> error_pages; + HeaderRefs add_request_headers; + HeaderRefs add_response_headers; + StringRef server_name; + // Port number which appears in Location header field when https + // redirect is made. + StringRef redirect_https_port; + size_t request_header_field_buffer; + size_t max_request_header_fields; + size_t response_header_field_buffer; + size_t max_response_header_fields; + size_t max_requests; + bool no_via; + bool no_location_rewrite; + bool no_host_rewrite; + bool no_server_rewrite; + bool require_http_scheme; +}; + +struct Http2Config { + struct { + struct { + struct { + StringRef request_header_file; + StringRef response_header_file; + FILE *request_header; + FILE *response_header; + } dump; + bool frame_debug; + } debug; + struct { + ev_tstamp settings; + } timeout; + nghttp2_option *option; + nghttp2_option *alt_mode_option; + nghttp2_session_callbacks *callbacks; + size_t max_concurrent_streams; + size_t encoder_dynamic_table_size; + size_t decoder_dynamic_table_size; + int32_t window_size; + int32_t connection_window_size; + bool optimize_write_buffer_size; + bool optimize_window_size; + } upstream; + struct { + struct { + ev_tstamp settings; + } timeout; + nghttp2_option *option; + nghttp2_session_callbacks *callbacks; + size_t encoder_dynamic_table_size; + size_t decoder_dynamic_table_size; + int32_t window_size; + int32_t connection_window_size; + size_t max_concurrent_streams; + } downstream; + struct { + ev_tstamp stream_read; + ev_tstamp stream_write; + } timeout; + bool no_cookie_crumbling; + bool no_server_push; +}; + +struct LoggingConfig { + struct { + std::vector<LogFragment> format; + StringRef file; + // Send accesslog to syslog, ignoring accesslog_file. + bool syslog; + // Write accesslog when response headers are received from + // backend, rather than response body is received and sent. + bool write_early; + } access; + struct { + StringRef file; + // Send errorlog to syslog, ignoring errorlog_file. + bool syslog; + } error; + int syslog_facility; + int severity; +}; + +struct RateLimitConfig { + size_t rate; + size_t burst; +}; + +// Wildcard host pattern routing. We strips left most '*' from host +// field. router includes all path patterns sharing the same wildcard +// host. +struct WildcardPattern { + WildcardPattern(const StringRef &host) : host(host) {} + + // This might not be NULL terminated. Currently it is only used for + // comparison. + StringRef host; + Router router; +}; + +// Configuration to select backend to forward request +struct RouterConfig { + Router router; + // Router for reversed wildcard hosts. Since this router has + // wildcard hosts reversed without '*', one should call match() + // function with reversed host stripping last character. This is + // because we require at least one character must match for '*'. + // The index stored in this router is index of wildcard_patterns. + Router rev_wildcard_router; + std::vector<WildcardPattern> wildcard_patterns; +}; + +struct DownstreamConfig { + DownstreamConfig() + : balloc(1024, 1024), + timeout{}, + addr_group_catch_all{0}, + connections_per_host{0}, + connections_per_frontend{0}, + request_buffer_size{0}, + response_buffer_size{0}, + family{0} {} + + DownstreamConfig(const DownstreamConfig &) = delete; + DownstreamConfig(DownstreamConfig &&) = delete; + DownstreamConfig &operator=(const DownstreamConfig &) = delete; + DownstreamConfig &operator=(DownstreamConfig &&) = delete; + + // Allocator to allocate memory for Downstream configuration. Since + // we may swap around DownstreamConfig in arbitrary times with API + // calls, we should use their own allocator instead of per Config + // allocator. + BlockAllocator balloc; + struct { + ev_tstamp read; + ev_tstamp write; + ev_tstamp idle_read; + ev_tstamp connect; + // The maximum backoff while checking health check for offline + // backend or while detaching failed backend from load balancing + // group temporarily. + ev_tstamp max_backoff; + } timeout; + RouterConfig router; + std::vector<DownstreamAddrGroupConfig> addr_groups; + // The index of catch-all group in downstream_addr_groups. + size_t addr_group_catch_all; + size_t connections_per_host; + size_t connections_per_frontend; + size_t request_buffer_size; + size_t response_buffer_size; + // Address family of backend connection. One of either AF_INET, + // AF_INET6 or AF_UNSPEC. This is ignored if backend connection + // is made via Unix domain socket. + int family; +}; + +struct ConnectionConfig { + struct { + struct { + ev_tstamp sleep; + } timeout; + // address of frontend acceptors + std::vector<UpstreamAddr> addrs; + int backlog; + // TCP fastopen. If this is positive, it is passed to + // setsockopt() along with TCP_FASTOPEN. + int fastopen; + } listener; + +#ifdef ENABLE_HTTP3 + struct { + std::vector<UpstreamAddr> addrs; + } quic_listener; +#endif // ENABLE_HTTP3 + + struct { + struct { + ev_tstamp http2_read; + ev_tstamp http3_read; + ev_tstamp read; + ev_tstamp write; + ev_tstamp idle_read; + } timeout; + struct { + RateLimitConfig read; + RateLimitConfig write; + } ratelimit; + size_t worker_connections; + // Deprecated. See UpstreamAddr.accept_proxy_protocol. + bool accept_proxy_protocol; + } upstream; + + std::shared_ptr<DownstreamConfig> downstream; +}; + +struct APIConfig { + // Maximum request body size for one API request + size_t max_request_body; + // true if at least one of UpstreamAddr has api enabled + bool enabled; +}; + +struct DNSConfig { + struct { + ev_tstamp cache; + ev_tstamp lookup; + } timeout; + // The number of tries name resolver makes before abandoning + // request. + size_t max_try; +}; + +struct Config { + Config() + : balloc(4096, 4096), + downstream_http_proxy{}, + http{}, + http2{}, + tls{}, +#ifdef ENABLE_HTTP3 + quic{}, +#endif // ENABLE_HTTP3 + logging{}, + conn{}, + api{}, + dns{}, + config_revision{0}, + num_worker{0}, + padding{0}, + rlimit_nofile{0}, + rlimit_memlock{0}, + uid{0}, + gid{0}, + pid{0}, + verbose{false}, + daemon{false}, + http2_proxy{false}, + single_process{false}, + single_thread{false}, + ignore_per_pattern_mruby_error{false}, + ev_loop_flags{0}, + max_worker_processes{0}, + worker_process_grace_shutdown_period{0.} { + } + ~Config(); + + Config(Config &&) = delete; + Config(const Config &&) = delete; + Config &operator=(Config &&) = delete; + Config &operator=(const Config &&) = delete; + + // Allocator to allocate memory for this object except for + // DownstreamConfig. Currently, it is used to allocate memory for + // strings. + BlockAllocator balloc; + HttpProxy downstream_http_proxy; + HttpConfig http; + Http2Config http2; + TLSConfig tls; +#ifdef ENABLE_HTTP3 + QUICConfig quic; + Http3Config http3; +#endif // ENABLE_HTTP3 + LoggingConfig logging; + ConnectionConfig conn; + APIConfig api; + DNSConfig dns; + StringRef pid_file; + StringRef conf_path; + StringRef user; + StringRef mruby_file; + // The revision of configuration which is opaque string, and changes + // on each configuration reloading. This does not change on + // backendconfig API call. This value is returned in health check + // as "nghttpx-conf-rev" response header field. The external + // program can check this value to know whether reloading has + // completed or not. + uint64_t config_revision; + size_t num_worker; + size_t padding; + size_t rlimit_nofile; + size_t rlimit_memlock; + uid_t uid; + gid_t gid; + pid_t pid; + bool verbose; + bool daemon; + bool http2_proxy; + // Run nghttpx in single process mode. With this mode, signal + // handling is omitted. + bool single_process; + bool single_thread; + // Ignore mruby compile error for per-pattern mruby script. + bool ignore_per_pattern_mruby_error; + // flags passed to ev_default_loop() and ev_loop_new() + int ev_loop_flags; + size_t max_worker_processes; + ev_tstamp worker_process_grace_shutdown_period; +}; + +const Config *get_config(); +Config *mod_config(); +// Replaces the current config with given |new_config|. The old config is +// returned. +std::unique_ptr<Config> replace_config(std::unique_ptr<Config> new_config); +void create_config(); + +// generated by gennghttpxfun.py +enum { + SHRPX_OPTID_ACCEPT_PROXY_PROTOCOL, + SHRPX_OPTID_ACCESSLOG_FILE, + SHRPX_OPTID_ACCESSLOG_FORMAT, + SHRPX_OPTID_ACCESSLOG_SYSLOG, + SHRPX_OPTID_ACCESSLOG_WRITE_EARLY, + SHRPX_OPTID_ADD_FORWARDED, + SHRPX_OPTID_ADD_REQUEST_HEADER, + SHRPX_OPTID_ADD_RESPONSE_HEADER, + SHRPX_OPTID_ADD_X_FORWARDED_FOR, + SHRPX_OPTID_ALPN_LIST, + SHRPX_OPTID_ALTSVC, + SHRPX_OPTID_API_MAX_REQUEST_BODY, + SHRPX_OPTID_BACKEND, + SHRPX_OPTID_BACKEND_ADDRESS_FAMILY, + SHRPX_OPTID_BACKEND_CONNECT_TIMEOUT, + SHRPX_OPTID_BACKEND_CONNECTIONS_PER_FRONTEND, + SHRPX_OPTID_BACKEND_CONNECTIONS_PER_HOST, + SHRPX_OPTID_BACKEND_HTTP_PROXY_URI, + SHRPX_OPTID_BACKEND_HTTP1_CONNECTIONS_PER_FRONTEND, + SHRPX_OPTID_BACKEND_HTTP1_CONNECTIONS_PER_HOST, + SHRPX_OPTID_BACKEND_HTTP1_TLS, + SHRPX_OPTID_BACKEND_HTTP2_CONNECTION_WINDOW_BITS, + SHRPX_OPTID_BACKEND_HTTP2_CONNECTION_WINDOW_SIZE, + SHRPX_OPTID_BACKEND_HTTP2_CONNECTIONS_PER_WORKER, + SHRPX_OPTID_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE, + SHRPX_OPTID_BACKEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE, + SHRPX_OPTID_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS, + SHRPX_OPTID_BACKEND_HTTP2_SETTINGS_TIMEOUT, + SHRPX_OPTID_BACKEND_HTTP2_WINDOW_BITS, + SHRPX_OPTID_BACKEND_HTTP2_WINDOW_SIZE, + SHRPX_OPTID_BACKEND_IPV4, + SHRPX_OPTID_BACKEND_IPV6, + SHRPX_OPTID_BACKEND_KEEP_ALIVE_TIMEOUT, + SHRPX_OPTID_BACKEND_MAX_BACKOFF, + SHRPX_OPTID_BACKEND_NO_TLS, + SHRPX_OPTID_BACKEND_READ_TIMEOUT, + SHRPX_OPTID_BACKEND_REQUEST_BUFFER, + SHRPX_OPTID_BACKEND_RESPONSE_BUFFER, + SHRPX_OPTID_BACKEND_TLS, + SHRPX_OPTID_BACKEND_TLS_SNI_FIELD, + SHRPX_OPTID_BACKEND_WRITE_TIMEOUT, + SHRPX_OPTID_BACKLOG, + SHRPX_OPTID_CACERT, + SHRPX_OPTID_CERTIFICATE_FILE, + SHRPX_OPTID_CIPHERS, + SHRPX_OPTID_CLIENT, + SHRPX_OPTID_CLIENT_CERT_FILE, + SHRPX_OPTID_CLIENT_CIPHERS, + SHRPX_OPTID_CLIENT_NO_HTTP2_CIPHER_BLACK_LIST, + SHRPX_OPTID_CLIENT_NO_HTTP2_CIPHER_BLOCK_LIST, + SHRPX_OPTID_CLIENT_PRIVATE_KEY_FILE, + SHRPX_OPTID_CLIENT_PROXY, + SHRPX_OPTID_CLIENT_PSK_SECRETS, + SHRPX_OPTID_CONF, + SHRPX_OPTID_DAEMON, + SHRPX_OPTID_DH_PARAM_FILE, + SHRPX_OPTID_DNS_CACHE_TIMEOUT, + SHRPX_OPTID_DNS_LOOKUP_TIMEOUT, + SHRPX_OPTID_DNS_MAX_TRY, + SHRPX_OPTID_ECDH_CURVES, + SHRPX_OPTID_ERROR_PAGE, + SHRPX_OPTID_ERRORLOG_FILE, + SHRPX_OPTID_ERRORLOG_SYSLOG, + SHRPX_OPTID_FASTOPEN, + SHRPX_OPTID_FETCH_OCSP_RESPONSE_FILE, + SHRPX_OPTID_FORWARDED_BY, + SHRPX_OPTID_FORWARDED_FOR, + SHRPX_OPTID_FRONTEND, + SHRPX_OPTID_FRONTEND_FRAME_DEBUG, + SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_BITS, + SHRPX_OPTID_FRONTEND_HTTP2_CONNECTION_WINDOW_SIZE, + SHRPX_OPTID_FRONTEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE, + SHRPX_OPTID_FRONTEND_HTTP2_DUMP_REQUEST_HEADER, + SHRPX_OPTID_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER, + SHRPX_OPTID_FRONTEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE, + SHRPX_OPTID_FRONTEND_HTTP2_MAX_CONCURRENT_STREAMS, + SHRPX_OPTID_FRONTEND_HTTP2_OPTIMIZE_WINDOW_SIZE, + SHRPX_OPTID_FRONTEND_HTTP2_OPTIMIZE_WRITE_BUFFER_SIZE, + SHRPX_OPTID_FRONTEND_HTTP2_READ_TIMEOUT, + SHRPX_OPTID_FRONTEND_HTTP2_SETTINGS_TIMEOUT, + SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_BITS, + SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_SIZE, + SHRPX_OPTID_FRONTEND_HTTP3_CONNECTION_WINDOW_SIZE, + SHRPX_OPTID_FRONTEND_HTTP3_MAX_CONCURRENT_STREAMS, + SHRPX_OPTID_FRONTEND_HTTP3_MAX_CONNECTION_WINDOW_SIZE, + SHRPX_OPTID_FRONTEND_HTTP3_MAX_WINDOW_SIZE, + SHRPX_OPTID_FRONTEND_HTTP3_READ_TIMEOUT, + SHRPX_OPTID_FRONTEND_HTTP3_WINDOW_SIZE, + SHRPX_OPTID_FRONTEND_KEEP_ALIVE_TIMEOUT, + SHRPX_OPTID_FRONTEND_MAX_REQUESTS, + SHRPX_OPTID_FRONTEND_NO_TLS, + SHRPX_OPTID_FRONTEND_QUIC_CONGESTION_CONTROLLER, + SHRPX_OPTID_FRONTEND_QUIC_DEBUG_LOG, + SHRPX_OPTID_FRONTEND_QUIC_EARLY_DATA, + SHRPX_OPTID_FRONTEND_QUIC_IDLE_TIMEOUT, + SHRPX_OPTID_FRONTEND_QUIC_INITIAL_RTT, + SHRPX_OPTID_FRONTEND_QUIC_QLOG_DIR, + SHRPX_OPTID_FRONTEND_QUIC_REQUIRE_TOKEN, + SHRPX_OPTID_FRONTEND_QUIC_SECRET_FILE, + SHRPX_OPTID_FRONTEND_READ_TIMEOUT, + SHRPX_OPTID_FRONTEND_WRITE_TIMEOUT, + SHRPX_OPTID_HEADER_FIELD_BUFFER, + SHRPX_OPTID_HOST_REWRITE, + SHRPX_OPTID_HTTP2_ALTSVC, + SHRPX_OPTID_HTTP2_BRIDGE, + SHRPX_OPTID_HTTP2_MAX_CONCURRENT_STREAMS, + SHRPX_OPTID_HTTP2_NO_COOKIE_CRUMBLING, + SHRPX_OPTID_HTTP2_PROXY, + SHRPX_OPTID_IGNORE_PER_PATTERN_MRUBY_ERROR, + SHRPX_OPTID_INCLUDE, + SHRPX_OPTID_INSECURE, + SHRPX_OPTID_LISTENER_DISABLE_TIMEOUT, + SHRPX_OPTID_LOG_LEVEL, + SHRPX_OPTID_MAX_HEADER_FIELDS, + SHRPX_OPTID_MAX_REQUEST_HEADER_FIELDS, + SHRPX_OPTID_MAX_RESPONSE_HEADER_FIELDS, + SHRPX_OPTID_MAX_WORKER_PROCESSES, + SHRPX_OPTID_MRUBY_FILE, + SHRPX_OPTID_NO_ADD_X_FORWARDED_PROTO, + SHRPX_OPTID_NO_HOST_REWRITE, + SHRPX_OPTID_NO_HTTP2_CIPHER_BLACK_LIST, + SHRPX_OPTID_NO_HTTP2_CIPHER_BLOCK_LIST, + SHRPX_OPTID_NO_KQUEUE, + SHRPX_OPTID_NO_LOCATION_REWRITE, + SHRPX_OPTID_NO_OCSP, + SHRPX_OPTID_NO_QUIC_BPF, + SHRPX_OPTID_NO_SERVER_PUSH, + SHRPX_OPTID_NO_SERVER_REWRITE, + SHRPX_OPTID_NO_STRIP_INCOMING_EARLY_DATA, + SHRPX_OPTID_NO_STRIP_INCOMING_X_FORWARDED_PROTO, + SHRPX_OPTID_NO_VERIFY_OCSP, + SHRPX_OPTID_NO_VIA, + SHRPX_OPTID_NPN_LIST, + SHRPX_OPTID_OCSP_STARTUP, + SHRPX_OPTID_OCSP_UPDATE_INTERVAL, + SHRPX_OPTID_PADDING, + SHRPX_OPTID_PID_FILE, + SHRPX_OPTID_PRIVATE_KEY_FILE, + SHRPX_OPTID_PRIVATE_KEY_PASSWD_FILE, + SHRPX_OPTID_PSK_SECRETS, + SHRPX_OPTID_QUIC_BPF_PROGRAM_FILE, + SHRPX_OPTID_QUIC_SERVER_ID, + SHRPX_OPTID_READ_BURST, + SHRPX_OPTID_READ_RATE, + SHRPX_OPTID_REDIRECT_HTTPS_PORT, + SHRPX_OPTID_REQUEST_HEADER_FIELD_BUFFER, + SHRPX_OPTID_REQUIRE_HTTP_SCHEME, + SHRPX_OPTID_RESPONSE_HEADER_FIELD_BUFFER, + SHRPX_OPTID_RLIMIT_MEMLOCK, + SHRPX_OPTID_RLIMIT_NOFILE, + SHRPX_OPTID_SERVER_NAME, + SHRPX_OPTID_SINGLE_PROCESS, + SHRPX_OPTID_SINGLE_THREAD, + SHRPX_OPTID_STREAM_READ_TIMEOUT, + SHRPX_OPTID_STREAM_WRITE_TIMEOUT, + SHRPX_OPTID_STRIP_INCOMING_FORWARDED, + SHRPX_OPTID_STRIP_INCOMING_X_FORWARDED_FOR, + SHRPX_OPTID_SUBCERT, + SHRPX_OPTID_SYSLOG_FACILITY, + SHRPX_OPTID_TLS_DYN_REC_IDLE_TIMEOUT, + SHRPX_OPTID_TLS_DYN_REC_WARMUP_THRESHOLD, + SHRPX_OPTID_TLS_KTLS, + SHRPX_OPTID_TLS_MAX_EARLY_DATA, + SHRPX_OPTID_TLS_MAX_PROTO_VERSION, + SHRPX_OPTID_TLS_MIN_PROTO_VERSION, + SHRPX_OPTID_TLS_NO_POSTPONE_EARLY_DATA, + SHRPX_OPTID_TLS_PROTO_LIST, + SHRPX_OPTID_TLS_SCT_DIR, + SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED, + SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY, + SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_CERT_FILE, + SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_PRIVATE_KEY_FILE, + SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_TLS, + SHRPX_OPTID_TLS_TICKET_KEY_CIPHER, + SHRPX_OPTID_TLS_TICKET_KEY_FILE, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_ADDRESS_FAMILY, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_CERT_FILE, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_INTERVAL, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_MAX_FAIL, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_MAX_RETRY, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_PRIVATE_KEY_FILE, + SHRPX_OPTID_TLS_TICKET_KEY_MEMCACHED_TLS, + SHRPX_OPTID_TLS13_CIPHERS, + SHRPX_OPTID_TLS13_CLIENT_CIPHERS, + SHRPX_OPTID_USER, + SHRPX_OPTID_VERIFY_CLIENT, + SHRPX_OPTID_VERIFY_CLIENT_CACERT, + SHRPX_OPTID_VERIFY_CLIENT_TOLERATE_EXPIRED, + SHRPX_OPTID_WORKER_FRONTEND_CONNECTIONS, + SHRPX_OPTID_WORKER_PROCESS_GRACE_SHUTDOWN_PERIOD, + SHRPX_OPTID_WORKER_READ_BURST, + SHRPX_OPTID_WORKER_READ_RATE, + SHRPX_OPTID_WORKER_WRITE_BURST, + SHRPX_OPTID_WORKER_WRITE_RATE, + SHRPX_OPTID_WORKERS, + SHRPX_OPTID_WRITE_BURST, + SHRPX_OPTID_WRITE_RATE, + SHRPX_OPTID_MAXIDX, +}; + +// Looks up token for given option name |name| of length |namelen|. +int option_lookup_token(const char *name, size_t namelen); + +// Parses option name |opt| and value |optarg|. The results are +// stored into the object pointed by |config|. This function returns 0 +// if it succeeds, or -1. The |included_set| contains the all paths +// already included while processing this configuration, to avoid loop +// in --include option. The |pattern_addr_indexer| contains a pair of +// pattern of backend, and its index in DownstreamConfig::addr_groups. +// It is introduced to speed up loading configuration file with lots +// of backends. +int parse_config(Config *config, const StringRef &opt, const StringRef &optarg, + std::set<StringRef> &included_set, + std::map<StringRef, size_t> &pattern_addr_indexer); + +// Similar to parse_config() above, but additional |optid| which +// should be the return value of option_lookup_token(opt). +int parse_config(Config *config, int optid, const StringRef &opt, + const StringRef &optarg, std::set<StringRef> &included_set, + std::map<StringRef, size_t> &pattern_addr_indexer); + +// Loads configurations from |filename| and stores them in |config|. +// This function returns 0 if it succeeds, or -1. See parse_config() +// for |include_set|. +int load_config(Config *config, const char *filename, + std::set<StringRef> &include_set, + std::map<StringRef, size_t> &pattern_addr_indexer); + +// Parses header field in |optarg|. We expect header field is formed +// like "NAME: VALUE". We require that NAME is non empty string. ":" +// is allowed at the start of the NAME, but NAME == ":" is not +// allowed. This function returns pair of NAME and VALUE. +HeaderRefs::value_type parse_header(BlockAllocator &balloc, + const StringRef &optarg); + +std::vector<LogFragment> parse_log_format(BlockAllocator &balloc, + const StringRef &optarg); + +// Returns string for syslog |facility|. +StringRef str_syslog_facility(int facility); + +// Returns integer value of syslog |facility| string. +int int_syslog_facility(const StringRef &strfacility); + +FILE *open_file_for_write(const char *filename); + +// Reads TLS ticket key file in |files| and returns TicketKey which +// stores read key data. The given |cipher| and |hmac| determine the +// expected file size. This function returns TicketKey if it +// succeeds, or nullptr. +std::unique_ptr<TicketKeys> +read_tls_ticket_key_file(const std::vector<StringRef> &files, + const EVP_CIPHER *cipher, const EVP_MD *hmac); + +#ifdef ENABLE_HTTP3 +std::shared_ptr<QUICKeyingMaterials> +read_quic_secret_file(const StringRef &path); +#endif // ENABLE_HTTP3 + +// Returns string representation of |proto|. +StringRef strproto(Proto proto); + +int configure_downstream_group(Config *config, bool http2_proxy, + bool numeric_addr_only, + const TLSConfig &tlsconf); + +int resolve_hostname(Address *addr, const char *hostname, uint16_t port, + int family, int additional_flags = 0); + +} // namespace shrpx + +#endif // SHRPX_CONFIG_H diff --git a/src/shrpx_config_test.cc b/src/shrpx_config_test.cc new file mode 100644 index 0000000..a8f0962 --- /dev/null +++ b/src/shrpx_config_test.cc @@ -0,0 +1,249 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_config_test.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H + +#include <cstdlib> + +#include <CUnit/CUnit.h> + +#include "shrpx_config.h" +#include "shrpx_log.h" + +namespace shrpx { + +void test_shrpx_config_parse_header(void) { + BlockAllocator balloc(4096, 4096); + + auto p = parse_header(balloc, StringRef::from_lit("a: b")); + CU_ASSERT("a" == p.name); + CU_ASSERT("b" == p.value); + + p = parse_header(balloc, StringRef::from_lit("a: b")); + CU_ASSERT("a" == p.name); + CU_ASSERT("b" == p.value); + + p = parse_header(balloc, StringRef::from_lit(":a: b")); + CU_ASSERT(p.name.empty()); + + p = parse_header(balloc, StringRef::from_lit("a: :b")); + CU_ASSERT("a" == p.name); + CU_ASSERT(":b" == p.value); + + p = parse_header(balloc, StringRef::from_lit(": b")); + CU_ASSERT(p.name.empty()); + + p = parse_header(balloc, StringRef::from_lit("alpha: bravo charlie")); + CU_ASSERT("alpha" == p.name); + CU_ASSERT("bravo charlie" == p.value); + + p = parse_header(balloc, StringRef::from_lit("a,: b")); + CU_ASSERT(p.name.empty()); + + p = parse_header(balloc, StringRef::from_lit("a: b\x0a")); + CU_ASSERT(p.name.empty()); +} + +void test_shrpx_config_parse_log_format(void) { + BlockAllocator balloc(4096, 4096); + + auto res = parse_log_format( + balloc, StringRef::from_lit( + R"($remote_addr - $remote_user [$time_local] )" + R"("$request" $status $body_bytes_sent )" + R"("${http_referer}" $http_host "$http_user_agent")")); + CU_ASSERT(16 == res.size()); + + CU_ASSERT(LogFragmentType::REMOTE_ADDR == res[0].type); + + CU_ASSERT(LogFragmentType::LITERAL == res[1].type); + CU_ASSERT(" - $remote_user [" == res[1].value); + + CU_ASSERT(LogFragmentType::TIME_LOCAL == res[2].type); + + CU_ASSERT(LogFragmentType::LITERAL == res[3].type); + CU_ASSERT("] \"" == res[3].value); + + CU_ASSERT(LogFragmentType::REQUEST == res[4].type); + + CU_ASSERT(LogFragmentType::LITERAL == res[5].type); + CU_ASSERT("\" " == res[5].value); + + CU_ASSERT(LogFragmentType::STATUS == res[6].type); + + CU_ASSERT(LogFragmentType::LITERAL == res[7].type); + CU_ASSERT(" " == res[7].value); + + CU_ASSERT(LogFragmentType::BODY_BYTES_SENT == res[8].type); + + CU_ASSERT(LogFragmentType::LITERAL == res[9].type); + CU_ASSERT(" \"" == res[9].value); + + CU_ASSERT(LogFragmentType::HTTP == res[10].type); + CU_ASSERT("referer" == res[10].value); + + CU_ASSERT(LogFragmentType::LITERAL == res[11].type); + CU_ASSERT("\" " == res[11].value); + + CU_ASSERT(LogFragmentType::AUTHORITY == res[12].type); + + CU_ASSERT(LogFragmentType::LITERAL == res[13].type); + CU_ASSERT(" \"" == res[13].value); + + CU_ASSERT(LogFragmentType::HTTP == res[14].type); + CU_ASSERT("user-agent" == res[14].value); + + CU_ASSERT(LogFragmentType::LITERAL == res[15].type); + CU_ASSERT("\"" == res[15].value); + + res = parse_log_format(balloc, StringRef::from_lit("$")); + + CU_ASSERT(1 == res.size()); + + CU_ASSERT(LogFragmentType::LITERAL == res[0].type); + CU_ASSERT("$" == res[0].value); + + res = parse_log_format(balloc, StringRef::from_lit("${")); + + CU_ASSERT(1 == res.size()); + + CU_ASSERT(LogFragmentType::LITERAL == res[0].type); + CU_ASSERT("${" == res[0].value); + + res = parse_log_format(balloc, StringRef::from_lit("${a")); + + CU_ASSERT(1 == res.size()); + + CU_ASSERT(LogFragmentType::LITERAL == res[0].type); + CU_ASSERT("${a" == res[0].value); + + res = parse_log_format(balloc, StringRef::from_lit("${a ")); + + CU_ASSERT(1 == res.size()); + + CU_ASSERT(LogFragmentType::LITERAL == res[0].type); + CU_ASSERT("${a " == res[0].value); + + res = parse_log_format(balloc, StringRef::from_lit("$$remote_addr")); + + CU_ASSERT(2 == res.size()); + + CU_ASSERT(LogFragmentType::LITERAL == res[0].type); + CU_ASSERT("$" == res[0].value); + + CU_ASSERT(LogFragmentType::REMOTE_ADDR == res[1].type); + CU_ASSERT("" == res[1].value); +} + +void test_shrpx_config_read_tls_ticket_key_file(void) { + char file1[] = "/tmp/nghttpx-unittest.XXXXXX"; + auto fd1 = mkstemp(file1); + CU_ASSERT(fd1 != -1); + CU_ASSERT(48 == + write(fd1, "0..............12..............34..............5", 48)); + char file2[] = "/tmp/nghttpx-unittest.XXXXXX"; + auto fd2 = mkstemp(file2); + CU_ASSERT(fd2 != -1); + CU_ASSERT(48 == + write(fd2, "6..............78..............9a..............b", 48)); + + close(fd1); + close(fd2); + auto ticket_keys = read_tls_ticket_key_file( + {StringRef{file1}, StringRef{file2}}, EVP_aes_128_cbc(), EVP_sha256()); + unlink(file1); + unlink(file2); + CU_ASSERT(ticket_keys.get() != nullptr); + CU_ASSERT(2 == ticket_keys->keys.size()); + auto key = &ticket_keys->keys[0]; + CU_ASSERT(std::equal(std::begin(key->data.name), std::end(key->data.name), + "0..............1")); + CU_ASSERT(std::equal(std::begin(key->data.enc_key), + std::begin(key->data.enc_key) + 16, "2..............3")); + CU_ASSERT(std::equal(std::begin(key->data.hmac_key), + std::begin(key->data.hmac_key) + 16, + "4..............5")); + CU_ASSERT(16 == key->hmac_keylen); + + key = &ticket_keys->keys[1]; + CU_ASSERT(std::equal(std::begin(key->data.name), std::end(key->data.name), + "6..............7")); + CU_ASSERT(std::equal(std::begin(key->data.enc_key), + std::begin(key->data.enc_key) + 16, "8..............9")); + CU_ASSERT(std::equal(std::begin(key->data.hmac_key), + std::begin(key->data.hmac_key) + 16, + "a..............b")); + CU_ASSERT(16 == key->hmac_keylen); +} + +void test_shrpx_config_read_tls_ticket_key_file_aes_256(void) { + char file1[] = "/tmp/nghttpx-unittest.XXXXXX"; + auto fd1 = mkstemp(file1); + CU_ASSERT(fd1 != -1); + CU_ASSERT(80 == write(fd1, + "0..............12..............................34..." + "...........................5", + 80)); + char file2[] = "/tmp/nghttpx-unittest.XXXXXX"; + auto fd2 = mkstemp(file2); + CU_ASSERT(fd2 != -1); + CU_ASSERT(80 == write(fd2, + "6..............78..............................9a..." + "...........................b", + 80)); + + close(fd1); + close(fd2); + auto ticket_keys = read_tls_ticket_key_file( + {StringRef{file1}, StringRef{file2}}, EVP_aes_256_cbc(), EVP_sha256()); + unlink(file1); + unlink(file2); + CU_ASSERT(ticket_keys.get() != nullptr); + CU_ASSERT(2 == ticket_keys->keys.size()); + auto key = &ticket_keys->keys[0]; + CU_ASSERT(std::equal(std::begin(key->data.name), std::end(key->data.name), + "0..............1")); + CU_ASSERT(std::equal(std::begin(key->data.enc_key), + std::end(key->data.enc_key), + "2..............................3")); + CU_ASSERT(std::equal(std::begin(key->data.hmac_key), + std::end(key->data.hmac_key), + "4..............................5")); + + key = &ticket_keys->keys[1]; + CU_ASSERT(std::equal(std::begin(key->data.name), std::end(key->data.name), + "6..............7")); + CU_ASSERT(std::equal(std::begin(key->data.enc_key), + std::end(key->data.enc_key), + "8..............................9")); + CU_ASSERT(std::equal(std::begin(key->data.hmac_key), + std::end(key->data.hmac_key), + "a..............................b")); +} + +} // namespace shrpx diff --git a/src/shrpx_config_test.h b/src/shrpx_config_test.h new file mode 100644 index 0000000..a30de41 --- /dev/null +++ b/src/shrpx_config_test.h @@ -0,0 +1,42 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_CONFIG_TEST_H +#define SHRPX_CONFIG_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_shrpx_config_parse_header(void); +void test_shrpx_config_parse_log_format(void); +void test_shrpx_config_read_tls_ticket_key_file(void); +void test_shrpx_config_read_tls_ticket_key_file_aes_256(void); +void test_shrpx_config_match_downstream_addr_group(void); + +} // namespace shrpx + +#endif // SHRPX_CONFIG_TEST_H diff --git a/src/shrpx_connect_blocker.cc b/src/shrpx_connect_blocker.cc new file mode 100644 index 0000000..ff767b0 --- /dev/null +++ b/src/shrpx_connect_blocker.cc @@ -0,0 +1,143 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_connect_blocker.h" +#include "shrpx_config.h" +#include "shrpx_log.h" + +namespace shrpx { + +namespace { +void connect_blocker_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto connect_blocker = static_cast<ConnectBlocker *>(w->data); + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Unblock"; + } + + connect_blocker->call_unblock_func(); +} +} // namespace + +ConnectBlocker::ConnectBlocker(std::mt19937 &gen, struct ev_loop *loop, + std::function<void()> block_func, + std::function<void()> unblock_func) + : gen_(gen), + block_func_(std::move(block_func)), + unblock_func_(std::move(unblock_func)), + loop_(loop), + fail_count_(0), + offline_(false) { + ev_timer_init(&timer_, connect_blocker_cb, 0., 0.); + timer_.data = this; +} + +ConnectBlocker::~ConnectBlocker() { ev_timer_stop(loop_, &timer_); } + +bool ConnectBlocker::blocked() const { return ev_is_active(&timer_); } + +void ConnectBlocker::on_success() { + if (ev_is_active(&timer_)) { + return; + } + + fail_count_ = 0; +} + +// Use the similar backoff algorithm described in +// https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md +namespace { +constexpr size_t MAX_BACKOFF_EXP = 10; +constexpr auto MULTIPLIER = 1.6; +constexpr auto JITTER = 0.2; +} // namespace + +void ConnectBlocker::on_failure() { + if (ev_is_active(&timer_)) { + return; + } + + call_block_func(); + + ++fail_count_; + + auto base_backoff = + util::int_pow(MULTIPLIER, std::min(MAX_BACKOFF_EXP, fail_count_)); + auto dist = std::uniform_real_distribution<>(-JITTER * base_backoff, + JITTER * base_backoff); + + auto &downstreamconf = *get_config()->conn.downstream; + + auto backoff = + std::min(downstreamconf.timeout.max_backoff, base_backoff + dist(gen_)); + + LOG(WARN) << "Could not connect " << fail_count_ + << " times in a row; sleep for " << backoff << " seconds"; + + ev_timer_set(&timer_, backoff, 0.); + ev_timer_start(loop_, &timer_); +} + +size_t ConnectBlocker::get_fail_count() const { return fail_count_; } + +void ConnectBlocker::offline() { + if (offline_) { + return; + } + + if (!ev_is_active(&timer_)) { + call_block_func(); + } + + offline_ = true; + + ev_timer_stop(loop_, &timer_); + ev_timer_set(&timer_, std::numeric_limits<double>::max(), 0.); + ev_timer_start(loop_, &timer_); +} + +void ConnectBlocker::online() { + ev_timer_stop(loop_, &timer_); + + call_unblock_func(); + + fail_count_ = 0; + + offline_ = false; +} + +bool ConnectBlocker::in_offline() const { return offline_; } + +void ConnectBlocker::call_block_func() { + if (block_func_) { + block_func_(); + } +} + +void ConnectBlocker::call_unblock_func() { + if (unblock_func_) { + unblock_func_(); + } +} + +} // namespace shrpx diff --git a/src/shrpx_connect_blocker.h b/src/shrpx_connect_blocker.h new file mode 100644 index 0000000..1ebe789 --- /dev/null +++ b/src/shrpx_connect_blocker.h @@ -0,0 +1,86 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_CONNECT_BLOCKER_H +#define SHRPX_CONNECT_BLOCKER_H + +#include "shrpx.h" + +#include <random> +#include <functional> + +#include <ev.h> + +namespace shrpx { + +class ConnectBlocker { +public: + ConnectBlocker(std::mt19937 &gen, struct ev_loop *loop, + std::function<void()> block_func, + std::function<void()> unblock_func); + ~ConnectBlocker(); + + // Returns true if making connection is not allowed. + bool blocked() const; + // Call this function if connect operation succeeded. This will + // reset sleep_ to minimum value. + void on_success(); + // Call this function if connect operations failed. This will start + // timer and blocks connection establishment with exponential + // backoff. + void on_failure(); + + size_t get_fail_count() const; + + // Peer is now considered offline. This effectively means that the + // connection is blocked until online() is called. + void offline(); + + // Peer is now considered online + void online(); + + // Returns true if peer is considered offline. + bool in_offline() const; + + void call_block_func(); + void call_unblock_func(); + +private: + std::mt19937 &gen_; + // Called when blocking is started + std::function<void()> block_func_; + // Called when unblocked + std::function<void()> unblock_func_; + ev_timer timer_; + struct ev_loop *loop_; + // The number of consecutive connection failure. Reset to 0 on + // success. + size_t fail_count_; + // true if peer is considered offline. + bool offline_; +}; + +} // namespace shrpx + +#endif // SHRPX_CONNECT_BLOCKER_H diff --git a/src/shrpx_connection.cc b/src/shrpx_connection.cc new file mode 100644 index 0000000..a5ab390 --- /dev/null +++ b/src/shrpx_connection.cc @@ -0,0 +1,1275 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_connection.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <netinet/tcp.h> + +#include <limits> + +#include <openssl/err.h> + +#include "shrpx_tls.h" +#include "shrpx_memcached_request.h" +#include "shrpx_log.h" +#include "memchunk.h" +#include "util.h" +#include "ssl_compat.h" + +using namespace nghttp2; +using namespace std::chrono_literals; + +namespace shrpx { + +Connection::Connection(struct ev_loop *loop, int fd, SSL *ssl, + MemchunkPool *mcpool, ev_tstamp write_timeout, + ev_tstamp read_timeout, + const RateLimitConfig &write_limit, + const RateLimitConfig &read_limit, IOCb writecb, + IOCb readcb, TimerCb timeoutcb, void *data, + size_t tls_dyn_rec_warmup_threshold, + ev_tstamp tls_dyn_rec_idle_timeout, Proto proto) + : +#ifdef ENABLE_HTTP3 + conn_ref{nullptr, this}, +#endif // ENABLE_HTTP3 + tls{DefaultMemchunks(mcpool), DefaultPeekMemchunks(mcpool), + DefaultMemchunks(mcpool)}, + wlimit(loop, &wev, write_limit.rate, write_limit.burst), + rlimit(loop, &rev, read_limit.rate, read_limit.burst, this), + loop(loop), + data(data), + fd(fd), + tls_dyn_rec_warmup_threshold(tls_dyn_rec_warmup_threshold), + tls_dyn_rec_idle_timeout(util::duration_from(tls_dyn_rec_idle_timeout)), + proto(proto), + read_timeout(read_timeout) { + + ev_io_init(&wev, writecb, fd, EV_WRITE); + ev_io_init(&rev, readcb, proto == Proto::HTTP3 ? 0 : fd, EV_READ); + + wev.data = this; + rev.data = this; + + ev_timer_init(&wt, timeoutcb, 0., write_timeout); + ev_timer_init(&rt, timeoutcb, 0., read_timeout); + + wt.data = this; + rt.data = this; + + if (ssl) { + set_ssl(ssl); + } +} + +Connection::~Connection() { disconnect(); } + +void Connection::disconnect() { + if (tls.ssl) { + if (proto != Proto::HTTP3) { + SSL_set_shutdown(tls.ssl, + SSL_get_shutdown(tls.ssl) | SSL_RECEIVED_SHUTDOWN); + ERR_clear_error(); + + if (tls.cached_session) { + SSL_SESSION_free(tls.cached_session); + tls.cached_session = nullptr; + } + + if (tls.cached_session_lookup_req) { + tls.cached_session_lookup_req->canceled = true; + tls.cached_session_lookup_req = nullptr; + } + + SSL_shutdown(tls.ssl); + } + + SSL_free(tls.ssl); + tls.ssl = nullptr; + + tls.wbuf.reset(); + tls.rbuf.reset(); + tls.last_write_idle = {}; + tls.warmup_writelen = 0; + tls.last_writelen = 0; + tls.last_readlen = 0; + tls.handshake_state = TLSHandshakeState::NORMAL; + tls.initial_handshake_done = false; + tls.reneg_started = false; + tls.sct_requested = false; + tls.early_data_finish = false; + } + + if (proto != Proto::HTTP3 && fd != -1) { + shutdown(fd, SHUT_WR); + close(fd); + fd = -1; + } + + // Stop watchers here because they could be activated in + // SSL_shutdown(). + ev_timer_stop(loop, &rt); + ev_timer_stop(loop, &wt); + + rlimit.stopw(); + wlimit.stopw(); +} + +void Connection::prepare_client_handshake() { + SSL_set_connect_state(tls.ssl); + // This prevents SSL_read_early_data from being called. + tls.early_data_finish = true; +} + +void Connection::prepare_server_handshake() { + auto &tlsconf = get_config()->tls; + if (proto != Proto::HTTP3 && !tlsconf.session_cache.memcached.host.empty()) { + auto bio = BIO_new(tlsconf.bio_method); + BIO_set_data(bio, this); + SSL_set_bio(tls.ssl, bio, bio); + } + + SSL_set_accept_state(tls.ssl); + tls.server_handshake = true; +} + +// BIO implementation is inspired by openldap implementation: +// http://www.openldap.org/devel/cvsweb.cgi/~checkout~/libraries/libldap/tls_o.c +namespace { +int shrpx_bio_write(BIO *b, const char *buf, int len) { + if (buf == nullptr || len <= 0) { + return 0; + } + + auto conn = static_cast<Connection *>(BIO_get_data(b)); + auto &wbuf = conn->tls.wbuf; + + BIO_clear_retry_flags(b); + + if (conn->tls.initial_handshake_done) { + // After handshake finished, send |buf| of length |len| to the + // socket directly. + + // Only when TLS session was prematurely ended before server sent + // all handshake message, this condition is true. This could be + // alert from SSL_shutdown(). Since connection is already down, + // just return error. + if (wbuf.rleft()) { + return -1; + } + auto nwrite = conn->write_clear(buf, len); + if (nwrite < 0) { + return -1; + } + + if (nwrite == 0) { + BIO_set_retry_write(b); + return -1; + } + + return nwrite; + } + + wbuf.append(buf, len); + + return len; +} +} // namespace + +namespace { +int shrpx_bio_read(BIO *b, char *buf, int len) { + if (buf == nullptr || len <= 0) { + return 0; + } + + auto conn = static_cast<Connection *>(BIO_get_data(b)); + auto &rbuf = conn->tls.rbuf; + + BIO_clear_retry_flags(b); + + if (conn->tls.initial_handshake_done && rbuf.rleft() == 0) { + auto nread = conn->read_clear(buf, len); + if (nread < 0) { + return -1; + } + if (nread == 0) { + BIO_set_retry_read(b); + return -1; + } + return nread; + } + + if (rbuf.rleft() == 0) { + BIO_set_retry_read(b); + return -1; + } + + return rbuf.remove(buf, len); +} +} // namespace + +namespace { +int shrpx_bio_puts(BIO *b, const char *str) { + return shrpx_bio_write(b, str, strlen(str)); +} +} // namespace + +namespace { +int shrpx_bio_gets(BIO *b, char *buf, int len) { return -1; } +} // namespace + +namespace { +long shrpx_bio_ctrl(BIO *b, int cmd, long num, void *ptr) { + switch (cmd) { + case BIO_CTRL_FLUSH: + return 1; + } + + return 0; +} +} // namespace + +namespace { +int shrpx_bio_create(BIO *b) { + BIO_set_init(b, 1); + + return 1; +} +} // namespace + +namespace { +int shrpx_bio_destroy(BIO *b) { + if (b == nullptr) { + return 0; + } + + return 1; +} +} // namespace + +BIO_METHOD *create_bio_method() { + auto meth = BIO_meth_new(BIO_TYPE_FD, "nghttpx-bio"); + BIO_meth_set_write(meth, shrpx_bio_write); + BIO_meth_set_read(meth, shrpx_bio_read); + BIO_meth_set_puts(meth, shrpx_bio_puts); + BIO_meth_set_gets(meth, shrpx_bio_gets); + BIO_meth_set_ctrl(meth, shrpx_bio_ctrl); + BIO_meth_set_create(meth, shrpx_bio_create); + BIO_meth_set_destroy(meth, shrpx_bio_destroy); + + return meth; +} + +void Connection::set_ssl(SSL *ssl) { + tls.ssl = ssl; + + SSL_set_app_data(tls.ssl, this); +} + +namespace { +// We should buffer at least full encrypted TLS record here. +// Theoretically, peer can send client hello in several TLS records, +// which could exceed this limit, but it is not portable, and we don't +// have to handle such exotic behaviour. +bool read_buffer_full(DefaultPeekMemchunks &rbuf) { + return rbuf.rleft_buffered() >= 20_k; +} +} // namespace + +int Connection::tls_handshake() { + wlimit.stopw(); + ev_timer_stop(loop, &wt); + + auto &tlsconf = get_config()->tls; + + if (!tls.server_handshake || tlsconf.session_cache.memcached.host.empty()) { + return tls_handshake_simple(); + } + + std::array<uint8_t, 16_k> buf; + + if (ev_is_active(&rev)) { + auto nread = read_clear(buf.data(), buf.size()); + if (nread < 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake read error"; + } + return -1; + } + tls.rbuf.append(buf.data(), nread); + if (read_buffer_full(tls.rbuf)) { + rlimit.stopw(); + } + } + + if (tls.initial_handshake_done) { + return write_tls_pending_handshake(); + } + + switch (tls.handshake_state) { + case TLSHandshakeState::WAIT_FOR_SESSION_CACHE: + return SHRPX_ERR_INPROGRESS; + case TLSHandshakeState::GOT_SESSION_CACHE: { + // Use the same trick invented by @kazuho in h2o project. + + // Discard all outgoing data. + tls.wbuf.reset(); + // Rewind buffered incoming data to replay client hello. + tls.rbuf.disable_peek(false); + + auto ssl_ctx = SSL_get_SSL_CTX(tls.ssl); + auto ssl_opts = SSL_get_options(tls.ssl); + SSL_free(tls.ssl); + + auto ssl = tls::create_ssl(ssl_ctx); + if (!ssl) { + return -1; + } + if (ssl_opts & SSL_OP_NO_TICKET) { + SSL_set_options(ssl, SSL_OP_NO_TICKET); + } + + set_ssl(ssl); + + prepare_server_handshake(); + + tls.handshake_state = TLSHandshakeState::NORMAL; + break; + } + case TLSHandshakeState::CANCEL_SESSION_CACHE: + tls.handshake_state = TLSHandshakeState::NORMAL; + break; + default: + break; + } + + int rv; + + ERR_clear_error(); + +#ifdef NGHTTP2_GENUINE_OPENSSL + if (!tls.server_handshake || tls.early_data_finish) { + rv = SSL_do_handshake(tls.ssl); + } else { + for (;;) { + size_t nread; + + rv = SSL_read_early_data(tls.ssl, buf.data(), buf.size(), &nread); + if (rv == SSL_READ_EARLY_DATA_ERROR) { + // If we have early data, and server sends ServerHello, assume + // that handshake is completed in server side, and start + // processing request. If we don't exit handshake code here, + // server waits for EndOfEarlyData and Finished message from + // client, which voids the purpose of 0-RTT data. The left + // over of handshake is done through write_tls or read_tls. + if (tlsconf.no_postpone_early_data && + (tls.handshake_state == TLSHandshakeState::WRITE_STARTED || + tls.wbuf.rleft()) && + tls.earlybuf.rleft()) { + rv = 1; + } + + break; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: read early data " << nread << " bytes"; + } + + tls.earlybuf.append(buf.data(), nread); + + if (rv == SSL_READ_EARLY_DATA_FINISH) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: read all early data; total " + << tls.earlybuf.rleft() << " bytes"; + } + tls.early_data_finish = true; + // The same reason stated above. + if (tlsconf.no_postpone_early_data && + (tls.handshake_state == TLSHandshakeState::WRITE_STARTED || + tls.wbuf.rleft()) && + tls.earlybuf.rleft()) { + rv = 1; + } else { + ERR_clear_error(); + rv = SSL_do_handshake(tls.ssl); + } + break; + } + } + } +#else // !NGHTTP2_GENUINE_OPENSSL + rv = SSL_do_handshake(tls.ssl); +#endif // !NGHTTP2_GENUINE_OPENSSL + + if (rv <= 0) { + auto err = SSL_get_error(tls.ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + if (read_buffer_full(tls.rbuf)) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake message is too large"; + } + return -1; + } + break; + case SSL_ERROR_WANT_WRITE: + break; + case SSL_ERROR_SSL: { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake libssl error: " + << ERR_error_string(ERR_get_error(), nullptr); + } + + struct iovec iov[1]; + auto iovcnt = tls.wbuf.riovec(iov, 1); + auto nwrite = writev_clear(iov, iovcnt); + if (nwrite > 0) { + tls.wbuf.drain(nwrite); + } + + return SHRPX_ERR_NETWORK; + } + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake libssl error " << err; + } + return SHRPX_ERR_NETWORK; + } + } + + if (tls.handshake_state == TLSHandshakeState::WAIT_FOR_SESSION_CACHE) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake is still in progress"; + } + return SHRPX_ERR_INPROGRESS; + } + + // Don't send handshake data if handshake was completed in OpenSSL + // routine. We have to check HTTP/2 requirement if HTTP/2 was + // negotiated before sending finished message to the peer. + if ((rv != 1 +#ifdef NGHTTP2_OPENSSL_IS_BORINGSSL + || SSL_in_init(tls.ssl) +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + ) && + tls.wbuf.rleft()) { + // First write indicates that resumption stuff has done. + if (tls.handshake_state != TLSHandshakeState::WRITE_STARTED) { + tls.handshake_state = TLSHandshakeState::WRITE_STARTED; + // If peek has already disabled, this is noop. + tls.rbuf.disable_peek(true); + } + std::array<struct iovec, 4> iov; + auto iovcnt = tls.wbuf.riovec(iov.data(), iov.size()); + auto nwrite = writev_clear(iov.data(), iovcnt); + if (nwrite < 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake write error"; + } + return -1; + } + tls.wbuf.drain(nwrite); + + if (tls.wbuf.rleft()) { + wlimit.startw(); + ev_timer_again(loop, &wt); + } + } + + if (!read_buffer_full(tls.rbuf)) { + // We may have stopped reading + rlimit.startw(); + } + + if (rv != 1) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake is still in progress"; + } + return SHRPX_ERR_INPROGRESS; + } + +#ifdef NGHTTP2_OPENSSL_IS_BORINGSSL + if (!tlsconf.no_postpone_early_data && SSL_in_early_data(tls.ssl) && + SSL_in_init(tls.ssl)) { + auto nread = SSL_read(tls.ssl, buf.data(), buf.size()); + if (nread <= 0) { + auto err = SSL_get_error(tls.ssl, nread); + switch (err) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + break; + case SSL_ERROR_ZERO_RETURN: + return SHRPX_ERR_EOF; + case SSL_ERROR_SSL: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: " + << ERR_error_string(ERR_get_error(), nullptr); + } + return SHRPX_ERR_NETWORK; + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: SSL_get_error returned " << err; + } + return SHRPX_ERR_NETWORK; + } + } else { + tls.earlybuf.append(buf.data(), nread); + } + + if (SSL_in_init(tls.ssl)) { + return SHRPX_ERR_INPROGRESS; + } + } +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + + // Handshake was done + + rv = check_http2_requirement(); + if (rv != 0) { + return -1; + } + + // Just in case + tls.rbuf.disable_peek(true); + + tls.initial_handshake_done = true; + + return write_tls_pending_handshake(); +} + +int Connection::tls_handshake_simple() { + wlimit.stopw(); + ev_timer_stop(loop, &wt); + + if (tls.initial_handshake_done) { + return write_tls_pending_handshake(); + } + + if (SSL_get_fd(tls.ssl) == -1) { + SSL_set_fd(tls.ssl, fd); + } + + int rv; +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + auto &tlsconf = get_config()->tls; + std::array<uint8_t, 16_k> buf; +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_BORINGSSL + + ERR_clear_error(); + +#ifdef NGHTTP2_GENUINE_OPENSSL + if (!tls.server_handshake || tls.early_data_finish) { + rv = SSL_do_handshake(tls.ssl); + } else { + for (;;) { + size_t nread; + + rv = SSL_read_early_data(tls.ssl, buf.data(), buf.size(), &nread); + if (rv == SSL_READ_EARLY_DATA_ERROR) { + // If we have early data, and server sends ServerHello, assume + // that handshake is completed in server side, and start + // processing request. If we don't exit handshake code here, + // server waits for EndOfEarlyData and Finished message from + // client, which voids the purpose of 0-RTT data. The left + // over of handshake is done through write_tls or read_tls. + if (tlsconf.no_postpone_early_data && tls.earlybuf.rleft()) { + rv = 1; + } + + break; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: read early data " << nread << " bytes"; + } + + tls.earlybuf.append(buf.data(), nread); + + if (rv == SSL_READ_EARLY_DATA_FINISH) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: read all early data; total " + << tls.earlybuf.rleft() << " bytes"; + } + tls.early_data_finish = true; + // The same reason stated above. + if (tlsconf.no_postpone_early_data && tls.earlybuf.rleft()) { + rv = 1; + } else { + ERR_clear_error(); + rv = SSL_do_handshake(tls.ssl); + } + break; + } + } + } +#else // !NGHTTP2_GENUINE_OPENSSL + rv = SSL_do_handshake(tls.ssl); +#endif // !NGHTTP2_GENUINE_OPENSSL + + if (rv <= 0) { + auto err = SSL_get_error(tls.ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + if (read_buffer_full(tls.rbuf)) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake message is too large"; + } + return -1; + } + break; + case SSL_ERROR_WANT_WRITE: + wlimit.startw(); + ev_timer_again(loop, &wt); + break; + case SSL_ERROR_SSL: { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake libssl error: " + << ERR_error_string(ERR_get_error(), nullptr); + } + return SHRPX_ERR_NETWORK; + } + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake libssl error " << err; + } + return SHRPX_ERR_NETWORK; + } + } + + if (rv != 1) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake is still in progress"; + } + return SHRPX_ERR_INPROGRESS; + } + +#ifdef NGHTTP2_OPENSSL_IS_BORINGSSL + if (!tlsconf.no_postpone_early_data && SSL_in_early_data(tls.ssl) && + SSL_in_init(tls.ssl)) { + auto nread = SSL_read(tls.ssl, buf.data(), buf.size()); + if (nread <= 0) { + auto err = SSL_get_error(tls.ssl, nread); + switch (err) { + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + break; + case SSL_ERROR_ZERO_RETURN: + return SHRPX_ERR_EOF; + case SSL_ERROR_SSL: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: " + << ERR_error_string(ERR_get_error(), nullptr); + } + return SHRPX_ERR_NETWORK; + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: SSL_get_error returned " << err; + } + return SHRPX_ERR_NETWORK; + } + } else { + tls.earlybuf.append(buf.data(), nread); + } + + if (SSL_in_init(tls.ssl)) { + return SHRPX_ERR_INPROGRESS; + } + } +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + + // Handshake was done + + rv = check_http2_requirement(); + if (rv != 0) { + return -1; + } + + tls.initial_handshake_done = true; + + return write_tls_pending_handshake(); +} + +int Connection::write_tls_pending_handshake() { + // Send handshake data left in the buffer + while (tls.wbuf.rleft()) { + std::array<struct iovec, 4> iov; + auto iovcnt = tls.wbuf.riovec(iov.data(), iov.size()); + auto nwrite = writev_clear(iov.data(), iovcnt); + if (nwrite < 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: handshake write error"; + } + return -1; + } + if (nwrite == 0) { + wlimit.startw(); + ev_timer_again(loop, &wt); + + return SHRPX_ERR_INPROGRESS; + } + tls.wbuf.drain(nwrite); + } + +#ifdef NGHTTP2_OPENSSL_IS_BORINGSSL + if (!SSL_in_init(tls.ssl)) { + // This will send a session ticket. + auto nwrite = SSL_write(tls.ssl, "", 0); + if (nwrite < 0) { + auto err = SSL_get_error(tls.ssl, nwrite); + switch (err) { + case SSL_ERROR_WANT_READ: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Close connection due to TLS renegotiation"; + } + return SHRPX_ERR_NETWORK; + case SSL_ERROR_WANT_WRITE: + break; + case SSL_ERROR_SSL: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_write: " + << ERR_error_string(ERR_get_error(), nullptr); + } + return SHRPX_ERR_NETWORK; + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_write: SSL_get_error returned " << err; + } + return SHRPX_ERR_NETWORK; + } + } + } +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + + // We have to start read watcher, since later stage of code expects + // this. + rlimit.startw(); + + // We may have whole request in tls.rbuf. This means that we don't + // get notified further read event. This is especially true for + // HTTP/1.1. + handle_tls_pending_read(); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL/TLS handshake completed"; + nghttp2::tls::TLSSessionInfo tls_info{}; + if (nghttp2::tls::get_tls_session_info(&tls_info, tls.ssl)) { + LOG(INFO) << "cipher=" << tls_info.cipher + << " protocol=" << tls_info.protocol + << " resumption=" << (tls_info.session_reused ? "yes" : "no") + << " session_id=" + << util::format_hex(tls_info.session_id, + tls_info.session_id_length); + } + } + + return 0; +} + +int Connection::check_http2_requirement() { + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len; + + SSL_get0_alpn_selected(tls.ssl, &next_proto, &next_proto_len); + if (next_proto == nullptr || + !util::check_h2_is_selected(StringRef{next_proto, next_proto_len})) { + return 0; + } + if (!nghttp2::tls::check_http2_tls_version(tls.ssl)) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "TLSv1.2 was not negotiated. HTTP/2 must not be used."; + } + return -1; + } + + auto check_block_list = false; + if (tls.server_handshake) { + check_block_list = !get_config()->tls.no_http2_cipher_block_list; + } else { + check_block_list = !get_config()->tls.client.no_http2_cipher_block_list; + } + + if (check_block_list && + nghttp2::tls::check_http2_cipher_block_list(tls.ssl)) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "The negotiated cipher suite is in HTTP/2 cipher suite " + "block list. HTTP/2 must not be used."; + } + return -1; + } + + return 0; +} + +namespace { +constexpr size_t SHRPX_SMALL_WRITE_LIMIT = 1300; +} // namespace + +size_t Connection::get_tls_write_limit() { + + if (tls_dyn_rec_warmup_threshold == 0) { + return std::numeric_limits<ssize_t>::max(); + } + + auto t = std::chrono::steady_clock::now(); + + if (tls.last_write_idle.time_since_epoch().count() >= 0 && + t - tls.last_write_idle > tls_dyn_rec_idle_timeout) { + // Time out, use small record size + tls.warmup_writelen = 0; + return SHRPX_SMALL_WRITE_LIMIT; + } + + if (tls.warmup_writelen >= tls_dyn_rec_warmup_threshold) { + return std::numeric_limits<ssize_t>::max(); + } + + return SHRPX_SMALL_WRITE_LIMIT; +} + +void Connection::update_tls_warmup_writelen(size_t n) { + if (tls.warmup_writelen < tls_dyn_rec_warmup_threshold) { + tls.warmup_writelen += n; + } +} + +void Connection::start_tls_write_idle() { + if (tls.last_write_idle.time_since_epoch().count() < 0) { + tls.last_write_idle = std::chrono::steady_clock::now(); + } +} + +ssize_t Connection::write_tls(const void *data, size_t len) { + // SSL_write requires the same arguments (buf pointer and its + // length) on SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE. + // get_write_limit() may return smaller length than previously + // passed to SSL_write, which violates OpenSSL assumption. To avoid + // this, we keep last length passed to SSL_write to + // tls.last_writelen if SSL_write indicated I/O blocking. + if (tls.last_writelen == 0) { + len = std::min(len, wlimit.avail()); + len = std::min(len, get_tls_write_limit()); + if (len == 0) { + return 0; + } + } else { + len = tls.last_writelen; + tls.last_writelen = 0; + } + + tls.last_write_idle = std::chrono::steady_clock::time_point(-1s); + + auto &tlsconf = get_config()->tls; + auto via_bio = + tls.server_handshake && !tlsconf.session_cache.memcached.host.empty(); + + ERR_clear_error(); + +#ifdef NGHTTP2_GENUINE_OPENSSL + int rv; + if (SSL_is_init_finished(tls.ssl)) { + rv = SSL_write(tls.ssl, data, len); + } else { + size_t nwrite; + rv = SSL_write_early_data(tls.ssl, data, len, &nwrite); + // Use the same semantics with SSL_write. + if (rv == 1) { + rv = nwrite; + } + } +#else // !NGHTTP2_GENUINE_OPENSSL + auto rv = SSL_write(tls.ssl, data, len); +#endif // !NGHTTP2_GENUINE_OPENSSL + + if (rv <= 0) { + auto err = SSL_get_error(tls.ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Close connection due to TLS renegotiation"; + } + return SHRPX_ERR_NETWORK; + case SSL_ERROR_WANT_WRITE: + tls.last_writelen = len; + // starting write watcher and timer is done in write_clear via + // bio otherwise. + if (!via_bio) { + wlimit.startw(); + ev_timer_again(loop, &wt); + } + + return 0; + case SSL_ERROR_SSL: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_write: " + << ERR_error_string(ERR_get_error(), nullptr); + } + return SHRPX_ERR_NETWORK; + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_write: SSL_get_error returned " << err; + } + return SHRPX_ERR_NETWORK; + } + } + + if (!via_bio) { + wlimit.drain(rv); + + if (ev_is_active(&wt)) { + ev_timer_again(loop, &wt); + } + } + + update_tls_warmup_writelen(rv); + + return rv; +} + +ssize_t Connection::read_tls(void *data, size_t len) { + ERR_clear_error(); + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + if (tls.earlybuf.rleft()) { + return tls.earlybuf.remove(data, len); + } +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_BORINGSSL + + // SSL_read requires the same arguments (buf pointer and its + // length) on SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE. + // rlimit_.avail() or rlimit_.avail() may return different length + // than the length previously passed to SSL_read, which violates + // OpenSSL assumption. To avoid this, we keep last length passed + // to SSL_read to tls_last_readlen_ if SSL_read indicated I/O + // blocking. + if (tls.last_readlen == 0) { + len = std::min(len, rlimit.avail()); + if (len == 0) { + return 0; + } + } else { + len = tls.last_readlen; + tls.last_readlen = 0; + } + + auto &tlsconf = get_config()->tls; + auto via_bio = + tls.server_handshake && !tlsconf.session_cache.memcached.host.empty(); + +#ifdef NGHTTP2_GENUINE_OPENSSL + if (!tls.early_data_finish) { + // TLSv1.3 handshake is still going on. + size_t nread; + auto rv = SSL_read_early_data(tls.ssl, data, len, &nread); + if (rv == SSL_READ_EARLY_DATA_ERROR) { + auto err = SSL_get_error(tls.ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + tls.last_readlen = len; + return 0; + case SSL_ERROR_SSL: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: " + << ERR_error_string(ERR_get_error(), nullptr); + } + return SHRPX_ERR_NETWORK; + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: SSL_get_error returned " << err; + } + return SHRPX_ERR_NETWORK; + } + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: read early data " << nread << " bytes"; + } + + if (rv == SSL_READ_EARLY_DATA_FINISH) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "tls: read all early data"; + } + tls.early_data_finish = true; + // We may have stopped write watcher in write_tls. + wlimit.startw(); + } + + if (!via_bio) { + rlimit.drain(nread); + } + + return nread; + } +#endif // NGHTTP2_GENUINE_OPENSSL + + auto rv = SSL_read(tls.ssl, data, len); + + if (rv <= 0) { + auto err = SSL_get_error(tls.ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + tls.last_readlen = len; + return 0; + case SSL_ERROR_WANT_WRITE: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Close connection due to TLS renegotiation"; + } + return SHRPX_ERR_NETWORK; + case SSL_ERROR_ZERO_RETURN: + return SHRPX_ERR_EOF; + case SSL_ERROR_SSL: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: " << ERR_error_string(ERR_get_error(), nullptr); + } + return SHRPX_ERR_NETWORK; + default: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL_read: SSL_get_error returned " << err; + } + return SHRPX_ERR_NETWORK; + } + } + + if (!via_bio) { + rlimit.drain(rv); + } + + return rv; +} + +ssize_t Connection::write_clear(const void *data, size_t len) { + len = std::min(len, wlimit.avail()); + if (len == 0) { + return 0; + } + + ssize_t nwrite; + while ((nwrite = write(fd, data, len)) == -1 && errno == EINTR) + ; + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + wlimit.startw(); + ev_timer_again(loop, &wt); + return 0; + } + return SHRPX_ERR_NETWORK; + } + + wlimit.drain(nwrite); + + if (ev_is_active(&wt)) { + ev_timer_again(loop, &wt); + } + + return nwrite; +} + +ssize_t Connection::writev_clear(struct iovec *iov, int iovcnt) { + iovcnt = limit_iovec(iov, iovcnt, wlimit.avail()); + if (iovcnt == 0) { + return 0; + } + + ssize_t nwrite; + while ((nwrite = writev(fd, iov, iovcnt)) == -1 && errno == EINTR) + ; + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + wlimit.startw(); + ev_timer_again(loop, &wt); + return 0; + } + return SHRPX_ERR_NETWORK; + } + + wlimit.drain(nwrite); + + if (ev_is_active(&wt)) { + ev_timer_again(loop, &wt); + } + + return nwrite; +} + +ssize_t Connection::read_clear(void *data, size_t len) { + len = std::min(len, rlimit.avail()); + if (len == 0) { + return 0; + } + + ssize_t nread; + while ((nread = read(fd, data, len)) == -1 && errno == EINTR) + ; + if (nread == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return SHRPX_ERR_NETWORK; + } + + if (nread == 0) { + return SHRPX_ERR_EOF; + } + + rlimit.drain(nread); + + return nread; +} + +ssize_t Connection::read_nolim_clear(void *data, size_t len) { + ssize_t nread; + while ((nread = read(fd, data, len)) == -1 && errno == EINTR) + ; + if (nread == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return SHRPX_ERR_NETWORK; + } + + if (nread == 0) { + return SHRPX_ERR_EOF; + } + + return nread; +} + +ssize_t Connection::peek_clear(void *data, size_t len) { + ssize_t nread; + while ((nread = recv(fd, data, len, MSG_PEEK)) == -1 && errno == EINTR) + ; + if (nread == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return SHRPX_ERR_NETWORK; + } + + if (nread == 0) { + return SHRPX_ERR_EOF; + } + + return nread; +} + +void Connection::handle_tls_pending_read() { + if (!ev_is_active(&rev)) { + return; + } + rlimit.handle_tls_pending_read(); +} + +int Connection::get_tcp_hint(TCPHint *hint) const { +#if defined(TCP_INFO) && defined(TCP_NOTSENT_LOWAT) + struct tcp_info tcp_info; + socklen_t tcp_info_len = sizeof(tcp_info); + int rv; + + rv = getsockopt(fd, IPPROTO_TCP, TCP_INFO, &tcp_info, &tcp_info_len); + + if (rv != 0) { + return -1; + } + + auto avail_packets = tcp_info.tcpi_snd_cwnd > tcp_info.tcpi_unacked + ? tcp_info.tcpi_snd_cwnd - tcp_info.tcpi_unacked + : 0; + + // http://www.slideshare.net/kazuho/programming-tcp-for-responsiveness + + // TODO 29 (5 (header) + 8 (explicit nonce) + 16 (tag)) is TLS + // overhead for AES-GCM. For CHACHA20_POLY1305, it is 21 since it + // does not need 8 bytes explicit nonce. + // + // For TLSv1.3, AES-GCM and CHACHA20_POLY1305 overhead are now 22 + // bytes (5 (header) + 1 (ContentType) + 16 (tag)). + size_t tls_overhead; +# ifdef TLS1_3_VERSION + if (SSL_version(tls.ssl) == TLS1_3_VERSION) { + tls_overhead = 22; + } else +# endif // TLS1_3_VERSION + { + tls_overhead = 29; + } + + auto writable_size = + (avail_packets + 2) * (tcp_info.tcpi_snd_mss - tls_overhead); + if (writable_size > 16_k) { + writable_size = writable_size & ~(16_k - 1); + } else { + if (writable_size < 536) { + LOG(INFO) << "writable_size is too small: " << writable_size; + } + // TODO is this required? + writable_size = std::max(writable_size, static_cast<size_t>(536 * 2)); + } + + // if (LOG_ENABLED(INFO)) { + // LOG(INFO) << "snd_cwnd=" << tcp_info.tcpi_snd_cwnd + // << ", unacked=" << tcp_info.tcpi_unacked + // << ", snd_mss=" << tcp_info.tcpi_snd_mss + // << ", rtt=" << tcp_info.tcpi_rtt << "us" + // << ", rcv_space=" << tcp_info.tcpi_rcv_space + // << ", writable=" << writable_size; + // } + + hint->write_buffer_size = writable_size; + // TODO tcpi_rcv_space is considered as rwin, is that correct? + hint->rwin = tcp_info.tcpi_rcv_space; + + return 0; +#else // !defined(TCP_INFO) || !defined(TCP_NOTSENT_LOWAT) + return -1; +#endif // !defined(TCP_INFO) || !defined(TCP_NOTSENT_LOWAT) +} + +void Connection::again_rt(ev_tstamp t) { + read_timeout = t; + rt.repeat = t; + ev_timer_again(loop, &rt); + last_read = std::chrono::steady_clock::now(); +} + +void Connection::again_rt() { + rt.repeat = read_timeout; + ev_timer_again(loop, &rt); + last_read = std::chrono::steady_clock::now(); +} + +bool Connection::expired_rt() { + auto delta = read_timeout - util::ev_tstamp_from( + std::chrono::steady_clock::now() - last_read); + if (delta < 1e-9) { + return true; + } + rt.repeat = delta; + ev_timer_again(loop, &rt); + return false; +} + +} // namespace shrpx diff --git a/src/shrpx_connection.h b/src/shrpx_connection.h new file mode 100644 index 0000000..10526f7 --- /dev/null +++ b/src/shrpx_connection.h @@ -0,0 +1,203 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_CONNECTION_H +#define SHRPX_CONNECTION_H + +#include "shrpx_config.h" + +#include <sys/uio.h> + +#include <ev.h> + +#include <openssl/ssl.h> + +#ifdef ENABLE_HTTP3 +# include <ngtcp2/ngtcp2_crypto.h> +#endif // ENABLE_HTTP3 + +#include "shrpx_rate_limit.h" +#include "shrpx_error.h" +#include "memchunk.h" + +namespace shrpx { + +struct MemcachedRequest; + +namespace tls { +struct TLSSessionCache; +} // namespace tls + +enum class TLSHandshakeState { + NORMAL, + WAIT_FOR_SESSION_CACHE, + GOT_SESSION_CACHE, + CANCEL_SESSION_CACHE, + WRITE_STARTED, +}; + +struct TLSConnection { + DefaultMemchunks wbuf; + DefaultPeekMemchunks rbuf; + // Stores TLSv1.3 early data. + DefaultMemchunks earlybuf; + SSL *ssl; + SSL_SESSION *cached_session; + MemcachedRequest *cached_session_lookup_req; + tls::TLSSessionCache *client_session_cache; + std::chrono::steady_clock::time_point last_write_idle; + size_t warmup_writelen; + // length passed to SSL_write and SSL_read last time. This is + // required since these functions require the exact same parameters + // on non-blocking I/O. + size_t last_writelen, last_readlen; + TLSHandshakeState handshake_state; + bool initial_handshake_done; + bool reneg_started; + // true if ssl is prepared to do handshake as server. + bool server_handshake; + // true if ssl is initialized as server, and client requested + // signed_certificate_timestamp extension. + bool sct_requested; + // true if TLSv1.3 early data has been completely received. Since + // SSL_read_early_data acts like SSL_do_handshake, this field may be + // true even if the negotiated TLS version is TLSv1.2 or earlier. + // This value is also true if this is client side connection for + // convenience. + bool early_data_finish; +}; + +struct TCPHint { + size_t write_buffer_size; + uint32_t rwin; +}; + +template <typename T> using EVCb = void (*)(struct ev_loop *, T *, int); + +using IOCb = EVCb<ev_io>; +using TimerCb = EVCb<ev_timer>; + +struct Connection { + Connection(struct ev_loop *loop, int fd, SSL *ssl, MemchunkPool *mcpool, + ev_tstamp write_timeout, ev_tstamp read_timeout, + const RateLimitConfig &write_limit, + const RateLimitConfig &read_limit, IOCb writecb, IOCb readcb, + TimerCb timeoutcb, void *data, size_t tls_dyn_rec_warmup_threshold, + ev_tstamp tls_dyn_rec_idle_timeout, Proto proto); + ~Connection(); + + void disconnect(); + + void prepare_client_handshake(); + void prepare_server_handshake(); + + int tls_handshake(); + int tls_handshake_simple(); + int write_tls_pending_handshake(); + + int check_http2_requirement(); + + // All write_* and writev_clear functions return number of bytes + // written. If nothing cannot be written (e.g., there is no + // allowance in RateLimit or underlying connection blocks), return + // 0. SHRPX_ERR_NETWORK is returned in case of error. + // + // All read_* functions return number of bytes read. If nothing + // cannot be read (e.g., there is no allowance in Ratelimit or + // underlying connection blocks), return 0. SHRPX_ERR_EOF is + // returned in case of EOF and no data was read. Otherwise + // SHRPX_ERR_NETWORK is return in case of error. + ssize_t write_tls(const void *data, size_t len); + ssize_t read_tls(void *data, size_t len); + + size_t get_tls_write_limit(); + // Updates the number of bytes written in warm up period. + void update_tls_warmup_writelen(size_t n); + // Tells there is no immediate write now. This triggers timer to + // determine fallback to short record size mode. + void start_tls_write_idle(); + + ssize_t write_clear(const void *data, size_t len); + ssize_t writev_clear(struct iovec *iov, int iovcnt); + ssize_t read_clear(void *data, size_t len); + // Read at most |len| bytes of data from socket without rate limit. + ssize_t read_nolim_clear(void *data, size_t len); + // Peek at most |len| bytes of data from socket without rate limit. + ssize_t peek_clear(void *data, size_t len); + + void handle_tls_pending_read(); + + void set_ssl(SSL *ssl); + + int get_tcp_hint(TCPHint *hint) const; + + // These functions are provided for read timer which is frequently + // restarted. We do a trick to make a bit more efficient than just + // calling ev_timer_again(). + + // Restarts read timer with timeout value |t|. + void again_rt(ev_tstamp t); + // Restarts read timer without changing timeout. + void again_rt(); + // Returns true if read timer expired. + bool expired_rt(); + +#ifdef ENABLE_HTTP3 + // This must be the first member of Connection. + ngtcp2_crypto_conn_ref conn_ref; +#endif // ENABLE_HTTP3 + TLSConnection tls; + ev_io wev; + ev_io rev; + ev_timer wt; + ev_timer rt; + RateLimit wlimit; + RateLimit rlimit; + struct ev_loop *loop; + void *data; + int fd; + size_t tls_dyn_rec_warmup_threshold; + std::chrono::steady_clock::duration tls_dyn_rec_idle_timeout; + // Application protocol used over the connection. This field is not + // used in this object at the moment. The rest of the program may + // use this value when it is useful. + Proto proto; + // The point of time when last read is observed. Note: since we use + // |rt| as idle timer, the activity is not limited to read. + std::chrono::steady_clock::time_point last_read; + // Timeout for read timer |rt|. + ev_tstamp read_timeout; +}; + +#ifdef ENABLE_HTTP3 +static_assert(std::is_standard_layout<Connection>::value, + "Connection is not standard layout"); +#endif // ENABLE_HTTP3 + +// Creates BIO_method shared by all SSL objects. +BIO_METHOD *create_bio_method(); + +} // namespace shrpx + +#endif // SHRPX_CONNECTION_H diff --git a/src/shrpx_connection_handler.cc b/src/shrpx_connection_handler.cc new file mode 100644 index 0000000..330e832 --- /dev/null +++ b/src/shrpx_connection_handler.cc @@ -0,0 +1,1319 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_connection_handler.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <sys/types.h> +#include <sys/wait.h> + +#include <cerrno> +#include <thread> +#include <random> + +#include "shrpx_client_handler.h" +#include "shrpx_tls.h" +#include "shrpx_worker.h" +#include "shrpx_config.h" +#include "shrpx_http2_session.h" +#include "shrpx_connect_blocker.h" +#include "shrpx_downstream_connection.h" +#include "shrpx_accept_handler.h" +#include "shrpx_memcached_dispatcher.h" +#include "shrpx_signal.h" +#include "shrpx_log.h" +#include "xsi_strerror.h" +#include "util.h" +#include "template.h" +#include "ssl_compat.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +void acceptor_disable_cb(struct ev_loop *loop, ev_timer *w, int revent) { + auto h = static_cast<ConnectionHandler *>(w->data); + + // If we are in graceful shutdown period, we must not enable + // acceptors again. + if (h->get_graceful_shutdown()) { + return; + } + + h->enable_acceptor(); +} +} // namespace + +namespace { +void ocsp_cb(struct ev_loop *loop, ev_timer *w, int revent) { + auto h = static_cast<ConnectionHandler *>(w->data); + + // If we are in graceful shutdown period, we won't do ocsp query. + if (h->get_graceful_shutdown()) { + return; + } + + LOG(NOTICE) << "Start ocsp update"; + + h->proceed_next_cert_ocsp(); +} +} // namespace + +namespace { +void ocsp_read_cb(struct ev_loop *loop, ev_io *w, int revent) { + auto h = static_cast<ConnectionHandler *>(w->data); + + h->read_ocsp_chunk(); +} +} // namespace + +namespace { +void ocsp_chld_cb(struct ev_loop *loop, ev_child *w, int revent) { + auto h = static_cast<ConnectionHandler *>(w->data); + + h->handle_ocsp_complete(); +} +} // namespace + +namespace { +void thread_join_async_cb(struct ev_loop *loop, ev_async *w, int revent) { + ev_break(loop); +} +} // namespace + +namespace { +void serial_event_async_cb(struct ev_loop *loop, ev_async *w, int revent) { + auto h = static_cast<ConnectionHandler *>(w->data); + + h->handle_serial_event(); +} +} // namespace + +ConnectionHandler::ConnectionHandler(struct ev_loop *loop, std::mt19937 &gen) + : +#ifdef ENABLE_HTTP3 + quic_ipc_fd_(-1), +#endif // ENABLE_HTTP3 + gen_(gen), + single_worker_(nullptr), + loop_(loop), +#ifdef HAVE_NEVERBLEED + nb_(nullptr), +#endif // HAVE_NEVERBLEED + tls_ticket_key_memcached_get_retry_count_(0), + tls_ticket_key_memcached_fail_count_(0), + worker_round_robin_cnt_(get_config()->api.enabled ? 1 : 0), + graceful_shutdown_(false), + enable_acceptor_on_ocsp_completion_(false) { + ev_timer_init(&disable_acceptor_timer_, acceptor_disable_cb, 0., 0.); + disable_acceptor_timer_.data = this; + + ev_timer_init(&ocsp_timer_, ocsp_cb, 0., 0.); + ocsp_timer_.data = this; + + ev_io_init(&ocsp_.rev, ocsp_read_cb, -1, EV_READ); + ocsp_.rev.data = this; + + ev_async_init(&thread_join_asyncev_, thread_join_async_cb); + + ev_async_init(&serial_event_asyncev_, serial_event_async_cb); + serial_event_asyncev_.data = this; + + ev_async_start(loop_, &serial_event_asyncev_); + + ev_child_init(&ocsp_.chldev, ocsp_chld_cb, 0, 0); + ocsp_.chldev.data = this; + + ocsp_.next = 0; + ocsp_.proc.rfd = -1; + + reset_ocsp(); +} + +ConnectionHandler::~ConnectionHandler() { + ev_child_stop(loop_, &ocsp_.chldev); + ev_async_stop(loop_, &serial_event_asyncev_); + ev_async_stop(loop_, &thread_join_asyncev_); + ev_io_stop(loop_, &ocsp_.rev); + ev_timer_stop(loop_, &ocsp_timer_); + ev_timer_stop(loop_, &disable_acceptor_timer_); + +#ifdef ENABLE_HTTP3 + for (auto ssl_ctx : quic_all_ssl_ctx_) { + if (ssl_ctx == nullptr) { + continue; + } + + auto tls_ctx_data = + static_cast<tls::TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx)); + delete tls_ctx_data; + SSL_CTX_free(ssl_ctx); + } +#endif // ENABLE_HTTP3 + + for (auto ssl_ctx : all_ssl_ctx_) { + auto tls_ctx_data = + static_cast<tls::TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx)); + delete tls_ctx_data; + SSL_CTX_free(ssl_ctx); + } + + // Free workers before destroying ev_loop + workers_.clear(); + + for (auto loop : worker_loops_) { + ev_loop_destroy(loop); + } +} + +void ConnectionHandler::set_ticket_keys_to_worker( + const std::shared_ptr<TicketKeys> &ticket_keys) { + for (auto &worker : workers_) { + worker->set_ticket_keys(ticket_keys); + } +} + +void ConnectionHandler::worker_reopen_log_files() { + for (auto &worker : workers_) { + WorkerEvent wev{}; + + wev.type = WorkerEventType::REOPEN_LOG; + + worker->send(std::move(wev)); + } +} + +void ConnectionHandler::worker_replace_downstream( + std::shared_ptr<DownstreamConfig> downstreamconf) { + for (auto &worker : workers_) { + WorkerEvent wev{}; + + wev.type = WorkerEventType::REPLACE_DOWNSTREAM; + wev.downstreamconf = downstreamconf; + + worker->send(std::move(wev)); + } +} + +int ConnectionHandler::create_single_worker() { + cert_tree_ = tls::create_cert_lookup_tree(); + auto sv_ssl_ctx = tls::setup_server_ssl_context( + all_ssl_ctx_, indexed_ssl_ctx_, cert_tree_.get() +#ifdef HAVE_NEVERBLEED + , + nb_ +#endif // HAVE_NEVERBLEED + ); + +#ifdef ENABLE_HTTP3 + quic_cert_tree_ = tls::create_cert_lookup_tree(); + auto quic_sv_ssl_ctx = tls::setup_quic_server_ssl_context( + quic_all_ssl_ctx_, quic_indexed_ssl_ctx_, quic_cert_tree_.get() +# ifdef HAVE_NEVERBLEED + , + nb_ +# endif // HAVE_NEVERBLEED + ); +#endif // ENABLE_HTTP3 + + auto cl_ssl_ctx = tls::setup_downstream_client_ssl_context( +#ifdef HAVE_NEVERBLEED + nb_ +#endif // HAVE_NEVERBLEED + ); + + if (cl_ssl_ctx) { + all_ssl_ctx_.push_back(cl_ssl_ctx); +#ifdef ENABLE_HTTP3 + quic_all_ssl_ctx_.push_back(nullptr); +#endif // ENABLE_HTTP3 + } + + auto config = get_config(); + auto &tlsconf = config->tls; + + SSL_CTX *session_cache_ssl_ctx = nullptr; + { + auto &memcachedconf = config->tls.session_cache.memcached; + if (memcachedconf.tls) { + session_cache_ssl_ctx = tls::create_ssl_client_context( +#ifdef HAVE_NEVERBLEED + nb_, +#endif // HAVE_NEVERBLEED + tlsconf.cacert, memcachedconf.cert_file, + memcachedconf.private_key_file); + all_ssl_ctx_.push_back(session_cache_ssl_ctx); +#ifdef ENABLE_HTTP3 + quic_all_ssl_ctx_.push_back(nullptr); +#endif // ENABLE_HTTP3 + } + } + +#if defined(ENABLE_HTTP3) && defined(HAVE_LIBBPF) + quic_bpf_refs_.resize(config->conn.quic_listener.addrs.size()); +#endif // ENABLE_HTTP3 && HAVE_LIBBPF + +#ifdef ENABLE_HTTP3 + assert(cid_prefixes_.size() == 1); + const auto &cid_prefix = cid_prefixes_[0]; +#endif // ENABLE_HTTP3 + + single_worker_ = std::make_unique<Worker>( + loop_, sv_ssl_ctx, cl_ssl_ctx, session_cache_ssl_ctx, cert_tree_.get(), +#ifdef ENABLE_HTTP3 + quic_sv_ssl_ctx, quic_cert_tree_.get(), cid_prefix.data(), + cid_prefix.size(), +# ifdef HAVE_LIBBPF + /* index = */ 0, +# endif // HAVE_LIBBPF +#endif // ENABLE_HTTP3 + ticket_keys_, this, config->conn.downstream); +#ifdef HAVE_MRUBY + if (single_worker_->create_mruby_context() != 0) { + return -1; + } +#endif // HAVE_MRUBY + +#ifdef ENABLE_HTTP3 + if (single_worker_->setup_quic_server_socket() != 0) { + return -1; + } +#endif // ENABLE_HTTP3 + + return 0; +} + +int ConnectionHandler::create_worker_thread(size_t num) { +#ifndef NOTHREADS + assert(workers_.size() == 0); + + cert_tree_ = tls::create_cert_lookup_tree(); + auto sv_ssl_ctx = tls::setup_server_ssl_context( + all_ssl_ctx_, indexed_ssl_ctx_, cert_tree_.get() +# ifdef HAVE_NEVERBLEED + , + nb_ +# endif // HAVE_NEVERBLEED + ); + +# ifdef ENABLE_HTTP3 + quic_cert_tree_ = tls::create_cert_lookup_tree(); + auto quic_sv_ssl_ctx = tls::setup_quic_server_ssl_context( + quic_all_ssl_ctx_, quic_indexed_ssl_ctx_, quic_cert_tree_.get() +# ifdef HAVE_NEVERBLEED + , + nb_ +# endif // HAVE_NEVERBLEED + ); +# endif // ENABLE_HTTP3 + + auto cl_ssl_ctx = tls::setup_downstream_client_ssl_context( +# ifdef HAVE_NEVERBLEED + nb_ +# endif // HAVE_NEVERBLEED + ); + + if (cl_ssl_ctx) { + all_ssl_ctx_.push_back(cl_ssl_ctx); +# ifdef ENABLE_HTTP3 + quic_all_ssl_ctx_.push_back(nullptr); +# endif // ENABLE_HTTP3 + } + + auto config = get_config(); + auto &tlsconf = config->tls; + auto &apiconf = config->api; + +# if defined(ENABLE_HTTP3) && defined(HAVE_LIBBPF) + quic_bpf_refs_.resize(config->conn.quic_listener.addrs.size()); +# endif // ENABLE_HTTP3 && HAVE_LIBBPF + + // We have dedicated worker for API request processing. + if (apiconf.enabled) { + ++num; + } + + SSL_CTX *session_cache_ssl_ctx = nullptr; + { + auto &memcachedconf = config->tls.session_cache.memcached; + + if (memcachedconf.tls) { + session_cache_ssl_ctx = tls::create_ssl_client_context( +# ifdef HAVE_NEVERBLEED + nb_, +# endif // HAVE_NEVERBLEED + tlsconf.cacert, memcachedconf.cert_file, + memcachedconf.private_key_file); + all_ssl_ctx_.push_back(session_cache_ssl_ctx); +# ifdef ENABLE_HTTP3 + quic_all_ssl_ctx_.push_back(nullptr); +# endif // ENABLE_HTTP3 + } + } + +# ifdef ENABLE_HTTP3 + assert(cid_prefixes_.size() == num); +# endif // ENABLE_HTTP3 + + for (size_t i = 0; i < num; ++i) { + auto loop = ev_loop_new(config->ev_loop_flags); + +# ifdef ENABLE_HTTP3 + const auto &cid_prefix = cid_prefixes_[i]; +# endif // ENABLE_HTTP3 + + auto worker = std::make_unique<Worker>( + loop, sv_ssl_ctx, cl_ssl_ctx, session_cache_ssl_ctx, cert_tree_.get(), +# ifdef ENABLE_HTTP3 + quic_sv_ssl_ctx, quic_cert_tree_.get(), cid_prefix.data(), + cid_prefix.size(), +# ifdef HAVE_LIBBPF + i, +# endif // HAVE_LIBBPF +# endif // ENABLE_HTTP3 + ticket_keys_, this, config->conn.downstream); +# ifdef HAVE_MRUBY + if (worker->create_mruby_context() != 0) { + return -1; + } +# endif // HAVE_MRUBY + +# ifdef ENABLE_HTTP3 + if ((!apiconf.enabled || i != 0) && + worker->setup_quic_server_socket() != 0) { + return -1; + } +# endif // ENABLE_HTTP3 + + workers_.push_back(std::move(worker)); + worker_loops_.push_back(loop); + + LLOG(NOTICE, this) << "Created worker thread #" << workers_.size() - 1; + } + + for (auto &worker : workers_) { + worker->run_async(); + } + +#endif // NOTHREADS + + return 0; +} + +void ConnectionHandler::join_worker() { +#ifndef NOTHREADS + int n = 0; + + if (LOG_ENABLED(INFO)) { + LLOG(INFO, this) << "Waiting for worker thread to join: n=" + << workers_.size(); + } + + for (auto &worker : workers_) { + worker->wait(); + if (LOG_ENABLED(INFO)) { + LLOG(INFO, this) << "Thread #" << n << " joined"; + } + ++n; + } +#endif // NOTHREADS +} + +void ConnectionHandler::graceful_shutdown_worker() { + if (single_worker_) { + return; + } + + if (LOG_ENABLED(INFO)) { + LLOG(INFO, this) << "Sending graceful shutdown signal to worker"; + } + + for (auto &worker : workers_) { + WorkerEvent wev{}; + wev.type = WorkerEventType::GRACEFUL_SHUTDOWN; + + worker->send(std::move(wev)); + } + +#ifndef NOTHREADS + ev_async_start(loop_, &thread_join_asyncev_); + + thread_join_fut_ = std::async(std::launch::async, [this]() { + (void)reopen_log_files(get_config()->logging); + join_worker(); + ev_async_send(get_loop(), &thread_join_asyncev_); + delete_log_config(); + }); +#endif // NOTHREADS +} + +int ConnectionHandler::handle_connection(int fd, sockaddr *addr, int addrlen, + const UpstreamAddr *faddr) { + if (LOG_ENABLED(INFO)) { + LLOG(INFO, this) << "Accepted connection from " + << util::numeric_name(addr, addrlen) << ", fd=" << fd; + } + + auto config = get_config(); + + if (single_worker_) { + auto &upstreamconf = config->conn.upstream; + if (single_worker_->get_worker_stat()->num_connections >= + upstreamconf.worker_connections) { + + if (LOG_ENABLED(INFO)) { + LLOG(INFO, this) << "Too many connections >=" + << upstreamconf.worker_connections; + } + + close(fd); + return -1; + } + + auto client = + tls::accept_connection(single_worker_.get(), fd, addr, addrlen, faddr); + if (!client) { + LLOG(ERROR, this) << "ClientHandler creation failed"; + + close(fd); + return -1; + } + + return 0; + } + + Worker *worker; + + if (faddr->alt_mode == UpstreamAltMode::API) { + worker = workers_[0].get(); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Dispatch connection to API worker #0"; + } + } else { + worker = workers_[worker_round_robin_cnt_].get(); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Dispatch connection to worker #" << worker_round_robin_cnt_; + } + + if (++worker_round_robin_cnt_ == workers_.size()) { + auto &apiconf = config->api; + + if (apiconf.enabled) { + worker_round_robin_cnt_ = 1; + } else { + worker_round_robin_cnt_ = 0; + } + } + } + + WorkerEvent wev{}; + wev.type = WorkerEventType::NEW_CONNECTION; + wev.client_fd = fd; + memcpy(&wev.client_addr, addr, addrlen); + wev.client_addrlen = addrlen; + wev.faddr = faddr; + + worker->send(std::move(wev)); + + return 0; +} + +struct ev_loop *ConnectionHandler::get_loop() const { return loop_; } + +Worker *ConnectionHandler::get_single_worker() const { + return single_worker_.get(); +} + +void ConnectionHandler::add_acceptor(std::unique_ptr<AcceptHandler> h) { + acceptors_.push_back(std::move(h)); +} + +void ConnectionHandler::delete_acceptor() { acceptors_.clear(); } + +void ConnectionHandler::enable_acceptor() { + for (auto &a : acceptors_) { + a->enable(); + } +} + +void ConnectionHandler::disable_acceptor() { + for (auto &a : acceptors_) { + a->disable(); + } +} + +void ConnectionHandler::sleep_acceptor(ev_tstamp t) { + if (t == 0. || ev_is_active(&disable_acceptor_timer_)) { + return; + } + + disable_acceptor(); + + ev_timer_set(&disable_acceptor_timer_, t, 0.); + ev_timer_start(loop_, &disable_acceptor_timer_); +} + +void ConnectionHandler::accept_pending_connection() { + for (auto &a : acceptors_) { + a->accept_connection(); + } +} + +void ConnectionHandler::set_ticket_keys( + std::shared_ptr<TicketKeys> ticket_keys) { + ticket_keys_ = std::move(ticket_keys); + if (single_worker_) { + single_worker_->set_ticket_keys(ticket_keys_); + } +} + +const std::shared_ptr<TicketKeys> &ConnectionHandler::get_ticket_keys() const { + return ticket_keys_; +} + +void ConnectionHandler::set_graceful_shutdown(bool f) { + graceful_shutdown_ = f; + if (single_worker_) { + single_worker_->set_graceful_shutdown(f); + } +} + +bool ConnectionHandler::get_graceful_shutdown() const { + return graceful_shutdown_; +} + +void ConnectionHandler::cancel_ocsp_update() { + enable_acceptor_on_ocsp_completion_ = false; + ev_timer_stop(loop_, &ocsp_timer_); + + if (ocsp_.proc.pid == 0) { + return; + } + + int rv; + + rv = kill(ocsp_.proc.pid, SIGTERM); + if (rv != 0) { + auto error = errno; + LOG(ERROR) << "Could not send signal to OCSP query process: errno=" + << error; + } + + while ((rv = waitpid(ocsp_.proc.pid, nullptr, 0)) == -1 && errno == EINTR) + ; + if (rv == -1) { + auto error = errno; + LOG(ERROR) << "Error occurred while we were waiting for the completion of " + "OCSP query process: errno=" + << error; + } +} + +// inspired by h2o_read_command function from h2o project: +// https://github.com/h2o/h2o +int ConnectionHandler::start_ocsp_update(const char *cert_file) { + int rv; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Start ocsp update for " << cert_file; + } + + assert(!ev_is_active(&ocsp_.rev)); + assert(!ev_is_active(&ocsp_.chldev)); + + char *const argv[] = { + const_cast<char *>( + get_config()->tls.ocsp.fetch_ocsp_response_file.c_str()), + const_cast<char *>(cert_file), nullptr}; + + Process proc; + rv = exec_read_command(proc, argv); + if (rv != 0) { + return -1; + } + + ocsp_.proc = proc; + + ev_io_set(&ocsp_.rev, ocsp_.proc.rfd, EV_READ); + ev_io_start(loop_, &ocsp_.rev); + + ev_child_set(&ocsp_.chldev, ocsp_.proc.pid, 0); + ev_child_start(loop_, &ocsp_.chldev); + + return 0; +} + +void ConnectionHandler::read_ocsp_chunk() { + std::array<uint8_t, 4_k> buf; + for (;;) { + ssize_t n; + while ((n = read(ocsp_.proc.rfd, buf.data(), buf.size())) == -1 && + errno == EINTR) + ; + + if (n == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return; + } + auto error = errno; + LOG(WARN) << "Reading from ocsp query command failed: errno=" << error; + ocsp_.error = error; + + break; + } + + if (n == 0) { + break; + } + + std::copy_n(std::begin(buf), n, std::back_inserter(ocsp_.resp)); + } + + ev_io_stop(loop_, &ocsp_.rev); +} + +void ConnectionHandler::handle_ocsp_complete() { + ev_io_stop(loop_, &ocsp_.rev); + ev_child_stop(loop_, &ocsp_.chldev); + + assert(ocsp_.next < all_ssl_ctx_.size()); +#ifdef ENABLE_HTTP3 + assert(all_ssl_ctx_.size() == quic_all_ssl_ctx_.size()); +#endif // ENABLE_HTTP3 + + auto ssl_ctx = all_ssl_ctx_[ocsp_.next]; + auto tls_ctx_data = + static_cast<tls::TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx)); + + auto rstatus = ocsp_.chldev.rstatus; + auto status = WEXITSTATUS(rstatus); + if (ocsp_.error || !WIFEXITED(rstatus) || status != 0) { + LOG(WARN) << "ocsp query command for " << tls_ctx_data->cert_file + << " failed: error=" << ocsp_.error << ", rstatus=" << log::hex + << rstatus << log::dec << ", status=" << status; + ++ocsp_.next; + proceed_next_cert_ocsp(); + return; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "ocsp update for " << tls_ctx_data->cert_file + << " finished successfully"; + } + + auto config = get_config(); + auto &tlsconf = config->tls; + + if (tlsconf.ocsp.no_verify || + tls::verify_ocsp_response(ssl_ctx, ocsp_.resp.data(), + ocsp_.resp.size()) == 0) { +#ifdef ENABLE_HTTP3 + // We have list of SSL_CTX with the same certificate in + // quic_all_ssl_ctx_ as well. Some SSL_CTXs are missing there in + // that case we get nullptr. + auto quic_ssl_ctx = quic_all_ssl_ctx_[ocsp_.next]; + if (quic_ssl_ctx) { +# ifndef NGHTTP2_OPENSSL_IS_BORINGSSL + auto quic_tls_ctx_data = static_cast<tls::TLSContextData *>( + SSL_CTX_get_app_data(quic_ssl_ctx)); +# ifdef HAVE_ATOMIC_STD_SHARED_PTR + std::atomic_store_explicit( + &quic_tls_ctx_data->ocsp_data, + std::make_shared<std::vector<uint8_t>>(ocsp_.resp), + std::memory_order_release); +# else // !HAVE_ATOMIC_STD_SHARED_PTR + std::lock_guard<std::mutex> g(quic_tls_ctx_data->mu); + quic_tls_ctx_data->ocsp_data = + std::make_shared<std::vector<uint8_t>>(ocsp_.resp); +# endif // !HAVE_ATOMIC_STD_SHARED_PTR +# else // NGHTTP2_OPENSSL_IS_BORINGSSL + SSL_CTX_set_ocsp_response(quic_ssl_ctx, ocsp_.resp.data(), + ocsp_.resp.size()); +# endif // NGHTTP2_OPENSSL_IS_BORINGSSL + } +#endif // ENABLE_HTTP3 + +#ifndef NGHTTP2_OPENSSL_IS_BORINGSSL +# ifdef HAVE_ATOMIC_STD_SHARED_PTR + std::atomic_store_explicit( + &tls_ctx_data->ocsp_data, + std::make_shared<std::vector<uint8_t>>(std::move(ocsp_.resp)), + std::memory_order_release); +# else // !HAVE_ATOMIC_STD_SHARED_PTR + std::lock_guard<std::mutex> g(tls_ctx_data->mu); + tls_ctx_data->ocsp_data = + std::make_shared<std::vector<uint8_t>>(std::move(ocsp_.resp)); +# endif // !HAVE_ATOMIC_STD_SHARED_PTR +#else // NGHTTP2_OPENSSL_IS_BORINGSSL + SSL_CTX_set_ocsp_response(ssl_ctx, ocsp_.resp.data(), ocsp_.resp.size()); +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + } + + ++ocsp_.next; + proceed_next_cert_ocsp(); +} + +void ConnectionHandler::reset_ocsp() { + if (ocsp_.proc.rfd != -1) { + close(ocsp_.proc.rfd); + } + + ocsp_.proc.rfd = -1; + ocsp_.proc.pid = 0; + ocsp_.error = 0; + ocsp_.resp = std::vector<uint8_t>(); +} + +void ConnectionHandler::proceed_next_cert_ocsp() { + for (;;) { + reset_ocsp(); + if (ocsp_.next == all_ssl_ctx_.size()) { + ocsp_.next = 0; + // We have updated all ocsp response, and schedule next update. + ev_timer_set(&ocsp_timer_, get_config()->tls.ocsp.update_interval, 0.); + ev_timer_start(loop_, &ocsp_timer_); + + if (enable_acceptor_on_ocsp_completion_) { + enable_acceptor_on_ocsp_completion_ = false; + enable_acceptor(); + } + + return; + } + + auto ssl_ctx = all_ssl_ctx_[ocsp_.next]; + auto tls_ctx_data = + static_cast<tls::TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx)); + + // client SSL_CTX is also included in all_ssl_ctx_, but has no + // tls_ctx_data. + if (!tls_ctx_data) { + ++ocsp_.next; + continue; + } + + auto cert_file = tls_ctx_data->cert_file; + + if (start_ocsp_update(cert_file) != 0) { + ++ocsp_.next; + continue; + } + + break; + } +} + +void ConnectionHandler::set_tls_ticket_key_memcached_dispatcher( + std::unique_ptr<MemcachedDispatcher> dispatcher) { + tls_ticket_key_memcached_dispatcher_ = std::move(dispatcher); +} + +MemcachedDispatcher * +ConnectionHandler::get_tls_ticket_key_memcached_dispatcher() const { + return tls_ticket_key_memcached_dispatcher_.get(); +} + +// Use the similar backoff algorithm described in +// https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md +namespace { +constexpr size_t MAX_BACKOFF_EXP = 10; +constexpr auto MULTIPLIER = 3.2; +constexpr auto JITTER = 0.2; +} // namespace + +void ConnectionHandler::on_tls_ticket_key_network_error(ev_timer *w) { + if (++tls_ticket_key_memcached_get_retry_count_ >= + get_config()->tls.ticket.memcached.max_retry) { + LOG(WARN) << "Memcached: tls ticket get retry all failed " + << tls_ticket_key_memcached_get_retry_count_ << " times."; + + on_tls_ticket_key_not_found(w); + return; + } + + auto base_backoff = util::int_pow( + MULTIPLIER, + std::min(MAX_BACKOFF_EXP, tls_ticket_key_memcached_get_retry_count_)); + auto dist = std::uniform_real_distribution<>(-JITTER * base_backoff, + JITTER * base_backoff); + + auto backoff = base_backoff + dist(gen_); + + LOG(WARN) + << "Memcached: tls ticket get failed due to network error, retrying in " + << backoff << " seconds"; + + ev_timer_set(w, backoff, 0.); + ev_timer_start(loop_, w); +} + +void ConnectionHandler::on_tls_ticket_key_not_found(ev_timer *w) { + tls_ticket_key_memcached_get_retry_count_ = 0; + + if (++tls_ticket_key_memcached_fail_count_ >= + get_config()->tls.ticket.memcached.max_fail) { + LOG(WARN) << "Memcached: could not get tls ticket; disable tls ticket"; + + tls_ticket_key_memcached_fail_count_ = 0; + + set_ticket_keys(nullptr); + set_ticket_keys_to_worker(nullptr); + } + + LOG(WARN) << "Memcached: tls ticket get failed, schedule next"; + schedule_next_tls_ticket_key_memcached_get(w); +} + +void ConnectionHandler::on_tls_ticket_key_get_success( + const std::shared_ptr<TicketKeys> &ticket_keys, ev_timer *w) { + LOG(NOTICE) << "Memcached: tls ticket get success"; + + tls_ticket_key_memcached_get_retry_count_ = 0; + tls_ticket_key_memcached_fail_count_ = 0; + + schedule_next_tls_ticket_key_memcached_get(w); + + if (!ticket_keys || ticket_keys->keys.empty()) { + LOG(WARN) << "Memcached: tls ticket keys are empty; tls ticket disabled"; + set_ticket_keys(nullptr); + set_ticket_keys_to_worker(nullptr); + return; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "ticket keys get done"; + LOG(INFO) << 0 << " enc+dec: " + << util::format_hex(ticket_keys->keys[0].data.name); + for (size_t i = 1; i < ticket_keys->keys.size(); ++i) { + auto &key = ticket_keys->keys[i]; + LOG(INFO) << i << " dec: " << util::format_hex(key.data.name); + } + } + + set_ticket_keys(ticket_keys); + set_ticket_keys_to_worker(ticket_keys); +} + +void ConnectionHandler::schedule_next_tls_ticket_key_memcached_get( + ev_timer *w) { + ev_timer_set(w, get_config()->tls.ticket.memcached.interval, 0.); + ev_timer_start(loop_, w); +} + +SSL_CTX *ConnectionHandler::create_tls_ticket_key_memcached_ssl_ctx() { + auto config = get_config(); + auto &tlsconf = config->tls; + auto &memcachedconf = config->tls.ticket.memcached; + + auto ssl_ctx = tls::create_ssl_client_context( +#ifdef HAVE_NEVERBLEED + nb_, +#endif // HAVE_NEVERBLEED + tlsconf.cacert, memcachedconf.cert_file, memcachedconf.private_key_file); + + all_ssl_ctx_.push_back(ssl_ctx); +#ifdef ENABLE_HTTP3 + quic_all_ssl_ctx_.push_back(nullptr); +#endif // ENABLE_HTTP3 + + return ssl_ctx; +} + +#ifdef HAVE_NEVERBLEED +void ConnectionHandler::set_neverbleed(neverbleed_t *nb) { nb_ = nb; } +#endif // HAVE_NEVERBLEED + +void ConnectionHandler::handle_serial_event() { + std::vector<SerialEvent> q; + { + std::lock_guard<std::mutex> g(serial_event_mu_); + q.swap(serial_events_); + } + + for (auto &sev : q) { + switch (sev.type) { + case SerialEventType::REPLACE_DOWNSTREAM: + // Mmake sure that none of worker uses + // get_config()->conn.downstream + mod_config()->conn.downstream = sev.downstreamconf; + + if (single_worker_) { + single_worker_->replace_downstream_config(sev.downstreamconf); + + break; + } + + worker_replace_downstream(sev.downstreamconf); + + break; + default: + break; + } + } +} + +void ConnectionHandler::send_replace_downstream( + const std::shared_ptr<DownstreamConfig> &downstreamconf) { + send_serial_event( + SerialEvent(SerialEventType::REPLACE_DOWNSTREAM, downstreamconf)); +} + +void ConnectionHandler::send_serial_event(SerialEvent ev) { + { + std::lock_guard<std::mutex> g(serial_event_mu_); + + serial_events_.push_back(std::move(ev)); + } + + ev_async_send(loop_, &serial_event_asyncev_); +} + +SSL_CTX *ConnectionHandler::get_ssl_ctx(size_t idx) const { + return all_ssl_ctx_[idx]; +} + +const std::vector<SSL_CTX *> & +ConnectionHandler::get_indexed_ssl_ctx(size_t idx) const { + return indexed_ssl_ctx_[idx]; +} + +#ifdef ENABLE_HTTP3 +const std::vector<SSL_CTX *> & +ConnectionHandler::get_quic_indexed_ssl_ctx(size_t idx) const { + return quic_indexed_ssl_ctx_[idx]; +} +#endif // ENABLE_HTTP3 + +void ConnectionHandler::set_enable_acceptor_on_ocsp_completion(bool f) { + enable_acceptor_on_ocsp_completion_ = f; +} + +#ifdef ENABLE_HTTP3 +int ConnectionHandler::forward_quic_packet( + const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *cid_prefix, const uint8_t *data, size_t datalen) { + assert(!get_config()->single_thread); + + for (auto &worker : workers_) { + if (!std::equal(cid_prefix, cid_prefix + SHRPX_QUIC_CID_PREFIXLEN, + worker->get_cid_prefix())) { + continue; + } + + WorkerEvent wev{}; + wev.type = WorkerEventType::QUIC_PKT_FORWARD; + wev.quic_pkt = std::make_unique<QUICPacket>(faddr->index, remote_addr, + local_addr, pi, data, datalen); + + worker->send(std::move(wev)); + + return 0; + } + + return -1; +} + +void ConnectionHandler::set_quic_keying_materials( + std::shared_ptr<QUICKeyingMaterials> qkms) { + quic_keying_materials_ = std::move(qkms); +} + +const std::shared_ptr<QUICKeyingMaterials> & +ConnectionHandler::get_quic_keying_materials() const { + return quic_keying_materials_; +} + +void ConnectionHandler::set_cid_prefixes( + const std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> + &cid_prefixes) { + cid_prefixes_ = cid_prefixes; +} + +QUICLingeringWorkerProcess * +ConnectionHandler::match_quic_lingering_worker_process_cid_prefix( + const uint8_t *dcid, size_t dcidlen) { + assert(dcidlen >= SHRPX_QUIC_CID_PREFIXLEN); + + for (auto &lwps : quic_lingering_worker_processes_) { + for (auto &cid_prefix : lwps.cid_prefixes) { + if (std::equal(std::begin(cid_prefix), std::end(cid_prefix), dcid)) { + return &lwps; + } + } + } + + return nullptr; +} + +# ifdef HAVE_LIBBPF +std::vector<BPFRef> &ConnectionHandler::get_quic_bpf_refs() { + return quic_bpf_refs_; +} + +void ConnectionHandler::unload_bpf_objects() { + LOG(NOTICE) << "Unloading BPF objects"; + + for (auto &ref : quic_bpf_refs_) { + if (ref.obj == nullptr) { + continue; + } + + bpf_object__close(ref.obj); + + ref.obj = nullptr; + } +} +# endif // HAVE_LIBBPF + +void ConnectionHandler::set_quic_ipc_fd(int fd) { quic_ipc_fd_ = fd; } + +void ConnectionHandler::set_quic_lingering_worker_processes( + const std::vector<QUICLingeringWorkerProcess> &quic_lwps) { + quic_lingering_worker_processes_ = quic_lwps; +} + +int ConnectionHandler::forward_quic_packet_to_lingering_worker_process( + QUICLingeringWorkerProcess *quic_lwp, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, const uint8_t *data, + size_t datalen) { + std::array<uint8_t, 512> header; + + assert(header.size() >= 1 + 1 + 1 + 1 + sizeof(sockaddr_storage) * 2); + assert(remote_addr.len > 0); + assert(local_addr.len > 0); + + auto p = header.data(); + + *p++ = static_cast<uint8_t>(QUICIPCType::DGRAM_FORWARD); + *p++ = static_cast<uint8_t>(remote_addr.len - 1); + p = std::copy_n(reinterpret_cast<const uint8_t *>(&remote_addr.su), + remote_addr.len, p); + *p++ = static_cast<uint8_t>(local_addr.len - 1); + p = std::copy_n(reinterpret_cast<const uint8_t *>(&local_addr.su), + local_addr.len, p); + *p++ = pi.ecn; + + iovec msg_iov[] = { + { + .iov_base = header.data(), + .iov_len = static_cast<size_t>(p - header.data()), + }, + { + .iov_base = const_cast<uint8_t *>(data), + .iov_len = datalen, + }, + }; + + msghdr msg{}; + msg.msg_iov = msg_iov; + msg.msg_iovlen = array_size(msg_iov); + + ssize_t nwrite; + + while ((nwrite = sendmsg(quic_lwp->quic_ipc_fd, &msg, 0)) == -1 && + errno == EINTR) + ; + + if (nwrite == -1) { + std::array<char, STRERROR_BUFSIZE> errbuf; + + auto error = errno; + LOG(ERROR) << "Failed to send QUIC IPC message: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + return -1; + } + + return 0; +} + +int ConnectionHandler::quic_ipc_read() { + std::array<uint8_t, 65536> buf; + + ssize_t nread; + + while ((nread = recv(quic_ipc_fd_, buf.data(), buf.size(), 0)) == -1 && + errno == EINTR) + ; + + if (nread == -1) { + std::array<char, STRERROR_BUFSIZE> errbuf; + + auto error = errno; + LOG(ERROR) << "Failed to read data from QUIC IPC channel: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + return -1; + } + + if (nread == 0) { + return 0; + } + + size_t len = 1 + 1 + 1 + 1; + + // Wire format: + // TYPE(1) REMOTE_ADDRLEN(1) REMOTE_ADDR(N) LOCAL_ADDRLEN(1) LOCAL_ADDR(N) + // ECN(1) DGRAM_PAYLOAD(N) + // + // When encoding, REMOTE_ADDRLEN and LOCAL_ADDRLEN are decremented + // by 1. + if (static_cast<size_t>(nread) < len) { + return 0; + } + + auto p = buf.data(); + if (*p != static_cast<uint8_t>(QUICIPCType::DGRAM_FORWARD)) { + LOG(ERROR) << "Unknown QUICIPCType: " << static_cast<uint32_t>(*p); + + return -1; + } + + ++p; + + auto pkt = std::make_unique<QUICPacket>(); + + auto remote_addrlen = static_cast<size_t>(*p++) + 1; + if (remote_addrlen > sizeof(sockaddr_storage)) { + LOG(ERROR) << "The length of remote address is too large: " + << remote_addrlen; + + return -1; + } + + len += remote_addrlen; + + if (static_cast<size_t>(nread) < len) { + LOG(ERROR) << "Insufficient QUIC IPC message length"; + + return -1; + } + + pkt->remote_addr.len = remote_addrlen; + memcpy(&pkt->remote_addr.su, p, remote_addrlen); + + p += remote_addrlen; + + auto local_addrlen = static_cast<size_t>(*p++) + 1; + if (local_addrlen > sizeof(sockaddr_storage)) { + LOG(ERROR) << "The length of local address is too large: " << local_addrlen; + + return -1; + } + + len += local_addrlen; + + if (static_cast<size_t>(nread) < len) { + LOG(ERROR) << "Insufficient QUIC IPC message length"; + + return -1; + } + + pkt->local_addr.len = local_addrlen; + memcpy(&pkt->local_addr.su, p, local_addrlen); + + p += local_addrlen; + + pkt->pi.ecn = *p++; + + auto datalen = nread - (p - buf.data()); + + pkt->data.assign(p, p + datalen); + + // At the moment, UpstreamAddr index is unknown. + pkt->upstream_addr_index = static_cast<size_t>(-1); + + ngtcp2_version_cid vc; + + auto rv = ngtcp2_pkt_decode_version_cid(&vc, p, datalen, SHRPX_QUIC_SCIDLEN); + if (rv < 0) { + LOG(ERROR) << "ngtcp2_pkt_decode_version_cid: " << ngtcp2_strerror(rv); + + return -1; + } + + if (vc.dcidlen != SHRPX_QUIC_SCIDLEN) { + LOG(ERROR) << "DCID length is invalid"; + return -1; + } + + if (single_worker_) { + auto faddr = single_worker_->find_quic_upstream_addr(pkt->local_addr); + if (faddr == nullptr) { + LOG(ERROR) << "No suitable upstream address found"; + + return 0; + } + + auto quic_conn_handler = single_worker_->get_quic_connection_handler(); + + // Ignore return value + quic_conn_handler->handle_packet(faddr, pkt->remote_addr, pkt->local_addr, + pkt->pi, pkt->data.data(), + pkt->data.size()); + + return 0; + } + + auto &qkm = quic_keying_materials_->keying_materials.front(); + + std::array<uint8_t, SHRPX_QUIC_DECRYPTED_DCIDLEN> decrypted_dcid; + + if (decrypt_quic_connection_id(decrypted_dcid.data(), + vc.dcid + SHRPX_QUIC_CID_PREFIX_OFFSET, + qkm.cid_encryption_key.data()) != 0) { + return -1; + } + + for (auto &worker : workers_) { + if (!std::equal(std::begin(decrypted_dcid), + std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN, + worker->get_cid_prefix())) { + continue; + } + + WorkerEvent wev{ + .type = WorkerEventType::QUIC_PKT_FORWARD, + .quic_pkt = std::move(pkt), + }; + worker->send(std::move(wev)); + + return 0; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "No worker to match CID prefix"; + } + + return 0; +} +#endif // ENABLE_HTTP3 + +} // namespace shrpx diff --git a/src/shrpx_connection_handler.h b/src/shrpx_connection_handler.h new file mode 100644 index 0000000..f3748ab --- /dev/null +++ b/src/shrpx_connection_handler.h @@ -0,0 +1,322 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_CONNECTION_HANDLER_H +#define SHRPX_CONNECTION_HANDLER_H + +#include "shrpx.h" + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H + +#include <mutex> +#include <memory> +#include <vector> +#include <random> +#ifndef NOTHREADS +# include <future> +#endif // NOTHREADS + +#ifdef HAVE_LIBBPF +# include <bpf/libbpf.h> +#endif // HAVE_LIBBPF + +#include <openssl/ssl.h> + +#include <ev.h> + +#ifdef HAVE_NEVERBLEED +# include <neverbleed.h> +#endif // HAVE_NEVERBLEED + +#include "shrpx_downstream_connection_pool.h" +#include "shrpx_config.h" +#include "shrpx_exec.h" + +namespace shrpx { + +class Http2Session; +class ConnectBlocker; +class AcceptHandler; +class Worker; +struct WorkerStat; +struct TicketKeys; +class MemcachedDispatcher; +struct UpstreamAddr; + +namespace tls { + +class CertLookupTree; + +} // namespace tls + +struct OCSPUpdateContext { + // ocsp response buffer + std::vector<uint8_t> resp; + // Process running fetch-ocsp-response script + Process proc; + // index to ConnectionHandler::all_ssl_ctx_, which points to next + // SSL_CTX to update ocsp response cache. + size_t next; + ev_child chldev; + ev_io rev; + // errno encountered while processing response + int error; +}; + +// SerialEvent is an event sent from Worker thread. +enum class SerialEventType { + NONE, + REPLACE_DOWNSTREAM, +}; + +struct SerialEvent { + // ctor for event uses DownstreamConfig + SerialEvent(SerialEventType type, + const std::shared_ptr<DownstreamConfig> &downstreamconf) + : type(type), downstreamconf(downstreamconf) {} + + SerialEventType type; + std::shared_ptr<DownstreamConfig> downstreamconf; +}; + +#ifdef ENABLE_HTTP3 +# ifdef HAVE_LIBBPF +struct BPFRef { + bpf_object *obj; + bpf_map *reuseport_array; + bpf_map *cid_prefix_map; +}; +# endif // HAVE_LIBBPF + +// QUIC IPC message type. +enum class QUICIPCType { + NONE, + // Send forwarded QUIC UDP datagram and its metadata. + DGRAM_FORWARD, +}; + +// WorkerProcesses which are in graceful shutdown period. +struct QUICLingeringWorkerProcess { + QUICLingeringWorkerProcess( + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes, + int quic_ipc_fd) + : cid_prefixes{std::move(cid_prefixes)}, quic_ipc_fd{quic_ipc_fd} {} + + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes; + // Socket to send QUIC IPC message to this worker process. + int quic_ipc_fd; +}; +#endif // ENABLE_HTTP3 + +class ConnectionHandler { +public: + ConnectionHandler(struct ev_loop *loop, std::mt19937 &gen); + ~ConnectionHandler(); + int handle_connection(int fd, sockaddr *addr, int addrlen, + const UpstreamAddr *faddr); + // Creates Worker object for single threaded configuration. + int create_single_worker(); + // Creates |num| Worker objects for multi threaded configuration. + // The |num| must be strictly more than 1. + int create_worker_thread(size_t num); + void + set_ticket_keys_to_worker(const std::shared_ptr<TicketKeys> &ticket_keys); + void worker_reopen_log_files(); + void set_ticket_keys(std::shared_ptr<TicketKeys> ticket_keys); + const std::shared_ptr<TicketKeys> &get_ticket_keys() const; + struct ev_loop *get_loop() const; + Worker *get_single_worker() const; + void add_acceptor(std::unique_ptr<AcceptHandler> h); + void delete_acceptor(); + void enable_acceptor(); + void disable_acceptor(); + void sleep_acceptor(ev_tstamp t); + void accept_pending_connection(); + void graceful_shutdown_worker(); + void set_graceful_shutdown(bool f); + bool get_graceful_shutdown() const; + void join_worker(); + + // Cancels ocsp update process + void cancel_ocsp_update(); + // Starts ocsp update for certificate |cert_file|. + int start_ocsp_update(const char *cert_file); + // Reads incoming data from ocsp update process + void read_ocsp_chunk(); + // Handles the completion of one ocsp update + void handle_ocsp_complete(); + // Resets ocsp_; + void reset_ocsp(); + // Proceeds to the next certificate's ocsp update. If all + // certificates' ocsp update has been done, schedule next ocsp + // update. + void proceed_next_cert_ocsp(); + + void set_tls_ticket_key_memcached_dispatcher( + std::unique_ptr<MemcachedDispatcher> dispatcher); + + MemcachedDispatcher *get_tls_ticket_key_memcached_dispatcher() const; + void on_tls_ticket_key_network_error(ev_timer *w); + void on_tls_ticket_key_not_found(ev_timer *w); + void + on_tls_ticket_key_get_success(const std::shared_ptr<TicketKeys> &ticket_keys, + ev_timer *w); + void schedule_next_tls_ticket_key_memcached_get(ev_timer *w); + SSL_CTX *create_tls_ticket_key_memcached_ssl_ctx(); + // Returns the SSL_CTX at all_ssl_ctx_[idx]. This does not perform + // array bound checking. + SSL_CTX *get_ssl_ctx(size_t idx) const; + + const std::vector<SSL_CTX *> &get_indexed_ssl_ctx(size_t idx) const; +#ifdef ENABLE_HTTP3 + const std::vector<SSL_CTX *> &get_quic_indexed_ssl_ctx(size_t idx) const; + + int forward_quic_packet(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *cid_prefix, const uint8_t *data, + size_t datalen); + + void set_quic_keying_materials(std::shared_ptr<QUICKeyingMaterials> qkms); + const std::shared_ptr<QUICKeyingMaterials> &get_quic_keying_materials() const; + + void set_cid_prefixes( + const std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> + &cid_prefixes); + + void set_quic_lingering_worker_processes( + const std::vector<QUICLingeringWorkerProcess> &quic_lwps); + + // Return matching QUICLingeringWorkerProcess which has a CID prefix + // such that |dcid| starts with it. If no such + // QUICLingeringWorkerProcess, it returns nullptr. + QUICLingeringWorkerProcess * + match_quic_lingering_worker_process_cid_prefix(const uint8_t *dcid, + size_t dcidlen); + + int forward_quic_packet_to_lingering_worker_process( + QUICLingeringWorkerProcess *quic_lwp, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, const uint8_t *data, + size_t datalen); + + void set_quic_ipc_fd(int fd); + + int quic_ipc_read(); + +# ifdef HAVE_LIBBPF + std::vector<BPFRef> &get_quic_bpf_refs(); + void unload_bpf_objects(); +# endif // HAVE_LIBBPF +#endif // ENABLE_HTTP3 + +#ifdef HAVE_NEVERBLEED + void set_neverbleed(neverbleed_t *nb); +#endif // HAVE_NEVERBLEED + + // Send SerialEvent SerialEventType::REPLACE_DOWNSTREAM to this + // object. + void send_replace_downstream( + const std::shared_ptr<DownstreamConfig> &downstreamconf); + // Internal function to send |ev| to this object. + void send_serial_event(SerialEvent ev); + // Handles SerialEvents received. + void handle_serial_event(); + // Sends WorkerEvent to make them replace downstream. + void + worker_replace_downstream(std::shared_ptr<DownstreamConfig> downstreamconf); + + void set_enable_acceptor_on_ocsp_completion(bool f); + +private: + // Stores all SSL_CTX objects. + std::vector<SSL_CTX *> all_ssl_ctx_; + // Stores all SSL_CTX objects in a way that its index is stored in + // cert_tree. The SSL_CTXs stored in the same index share the same + // hostname, but could have different signature algorithm. The + // selection among them are performed by hostname presented by SNI, + // and signature algorithm presented by client. + std::vector<std::vector<SSL_CTX *>> indexed_ssl_ctx_; +#ifdef ENABLE_HTTP3 + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes_; + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> + lingering_cid_prefixes_; + int quic_ipc_fd_; + std::vector<QUICLingeringWorkerProcess> quic_lingering_worker_processes_; +# ifdef HAVE_LIBBPF + std::vector<BPFRef> quic_bpf_refs_; +# endif // HAVE_LIBBPF + std::shared_ptr<QUICKeyingMaterials> quic_keying_materials_; + std::vector<SSL_CTX *> quic_all_ssl_ctx_; + std::vector<std::vector<SSL_CTX *>> quic_indexed_ssl_ctx_; +#endif // ENABLE_HTTP3 + OCSPUpdateContext ocsp_; + std::mt19937 &gen_; + // ev_loop for each worker + std::vector<struct ev_loop *> worker_loops_; + // Worker instances when multi threaded mode (-nN, N >= 2) is used. + // If at least one frontend enables API request, we allocate 1 + // additional worker dedicated to API request . + std::vector<std::unique_ptr<Worker>> workers_; + // mutex for serial event resive buffer handling + std::mutex serial_event_mu_; + // SerialEvent receive buffer + std::vector<SerialEvent> serial_events_; + // Worker instance used when single threaded mode (-n1) is used. + // Otherwise, nullptr and workers_ has instances of Worker instead. + std::unique_ptr<Worker> single_worker_; + std::unique_ptr<tls::CertLookupTree> cert_tree_; +#ifdef ENABLE_HTTP3 + std::unique_ptr<tls::CertLookupTree> quic_cert_tree_; +#endif // ENABLE_HTTP3 + std::unique_ptr<MemcachedDispatcher> tls_ticket_key_memcached_dispatcher_; + // Current TLS session ticket keys. Note that TLS connection does + // not refer to this field directly. They use TicketKeys object in + // Worker object. + std::shared_ptr<TicketKeys> ticket_keys_; + struct ev_loop *loop_; + std::vector<std::unique_ptr<AcceptHandler>> acceptors_; +#ifdef HAVE_NEVERBLEED + neverbleed_t *nb_; +#endif // HAVE_NEVERBLEED + ev_timer disable_acceptor_timer_; + ev_timer ocsp_timer_; + ev_async thread_join_asyncev_; + ev_async serial_event_asyncev_; +#ifndef NOTHREADS + std::future<void> thread_join_fut_; +#endif // NOTHREADS + size_t tls_ticket_key_memcached_get_retry_count_; + size_t tls_ticket_key_memcached_fail_count_; + unsigned int worker_round_robin_cnt_; + bool graceful_shutdown_; + // true if acceptors should be enabled after the initial ocsp update + // has finished. + bool enable_acceptor_on_ocsp_completion_; +}; + +} // namespace shrpx + +#endif // SHRPX_CONNECTION_HANDLER_H diff --git a/src/shrpx_dns_resolver.cc b/src/shrpx_dns_resolver.cc new file mode 100644 index 0000000..f83ecb7 --- /dev/null +++ b/src/shrpx_dns_resolver.cc @@ -0,0 +1,353 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_dns_resolver.h" + +#include <cstring> +#include <sys/time.h> + +#include "shrpx_log.h" +#include "shrpx_connection.h" +#include "shrpx_config.h" + +namespace shrpx { + +namespace { +void sock_state_cb(void *data, int s, int read, int write) { + auto resolv = static_cast<DNSResolver *>(data); + + if (resolv->get_status(nullptr) != DNSResolverStatus::RUNNING) { + return; + } + + if (read) { + resolv->start_rev(s); + } else { + resolv->stop_rev(s); + } + if (write) { + resolv->start_wev(s); + } else { + resolv->stop_wev(s); + } +} +} // namespace + +namespace { +void host_cb(void *arg, int status, int timeouts, hostent *hostent) { + auto resolv = static_cast<DNSResolver *>(arg); + resolv->on_result(status, hostent); +} +} // namespace + +namespace { +void process_result(DNSResolver *resolv) { + auto cb = resolv->get_complete_cb(); + if (!cb) { + return; + } + Address result; + auto status = resolv->get_status(&result); + switch (status) { + case DNSResolverStatus::OK: + case DNSResolverStatus::ERROR: + cb(status, &result); + break; + default: + break; + } + // resolv may be deleted here. +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto resolv = static_cast<DNSResolver *>(w->data); + resolv->on_read(w->fd); + process_result(resolv); +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto resolv = static_cast<DNSResolver *>(w->data); + resolv->on_write(w->fd); + process_result(resolv); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto resolv = static_cast<DNSResolver *>(w->data); + resolv->on_timeout(); + process_result(resolv); +} +} // namespace + +namespace { +void stop_ev(struct ev_loop *loop, + const std::vector<std::unique_ptr<ev_io>> &evs) { + for (auto &w : evs) { + ev_io_stop(loop, w.get()); + } +} +} // namespace + +DNSResolver::DNSResolver(struct ev_loop *loop) + : result_{}, + loop_(loop), + channel_(nullptr), + family_(AF_UNSPEC), + status_(DNSResolverStatus::IDLE) { + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; +} + +DNSResolver::~DNSResolver() { + if (channel_) { + ares_destroy(channel_); + } + + stop_ev(loop_, revs_); + stop_ev(loop_, wevs_); + + ev_timer_stop(loop_, &timer_); +} + +int DNSResolver::resolve(const StringRef &name, int family) { + if (status_ != DNSResolverStatus::IDLE) { + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Start resolving host " << name << " in IPv" + << (family == AF_INET ? "4" : "6"); + } + + name_ = name; + family_ = family; + + int rv; + + auto &dnsconf = get_config()->dns; + + ares_options opts{}; + opts.sock_state_cb = sock_state_cb; + opts.sock_state_cb_data = this; + opts.timeout = static_cast<int>(dnsconf.timeout.lookup * 1000); + opts.tries = dnsconf.max_try; + + auto optmask = ARES_OPT_SOCK_STATE_CB | ARES_OPT_TIMEOUTMS | ARES_OPT_TRIES; + + ares_channel chan; + rv = ares_init_options(&chan, &opts, optmask); + if (rv != ARES_SUCCESS) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "ares_init_options failed: " << ares_strerror(rv); + } + status_ = DNSResolverStatus::ERROR; + return -1; + } + + channel_ = chan; + status_ = DNSResolverStatus::RUNNING; + + ares_gethostbyname(channel_, name_.c_str(), family_, host_cb, this); + reset_timeout(); + + return 0; +} + +int DNSResolver::on_read(int fd) { return handle_event(fd, ARES_SOCKET_BAD); } + +int DNSResolver::on_write(int fd) { return handle_event(ARES_SOCKET_BAD, fd); } + +int DNSResolver::on_timeout() { + return handle_event(ARES_SOCKET_BAD, ARES_SOCKET_BAD); +} + +int DNSResolver::handle_event(int rfd, int wfd) { + if (status_ == DNSResolverStatus::IDLE) { + return -1; + } + + ares_process_fd(channel_, rfd, wfd); + + switch (status_) { + case DNSResolverStatus::RUNNING: + reset_timeout(); + return 0; + case DNSResolverStatus::OK: + return 0; + case DNSResolverStatus::ERROR: + return -1; + default: + // Unreachable + assert(0); + abort(); + } +} + +void DNSResolver::reset_timeout() { + if (status_ != DNSResolverStatus::RUNNING) { + return; + } + timeval tvout; + auto tv = ares_timeout(channel_, nullptr, &tvout); + if (tv == nullptr) { + return; + } + // To avoid that timer_.repeat becomes 0, which makes ev_timer_again + // useless, add tiny fraction of time. + timer_.repeat = tv->tv_sec + tv->tv_usec / 1000000. + 1e-9; + ev_timer_again(loop_, &timer_); +} + +DNSResolverStatus DNSResolver::get_status(Address *result) const { + if (status_ != DNSResolverStatus::OK) { + return status_; + } + + if (result) { + memcpy(result, &result_, sizeof(result_)); + } + + return status_; +} + +namespace { +void start_ev(std::vector<std::unique_ptr<ev_io>> &evs, struct ev_loop *loop, + int fd, int event, IOCb cb, void *data) { + for (auto &w : evs) { + if (w->fd == fd) { + return; + } + } + for (auto &w : evs) { + if (w->fd == -1) { + ev_io_set(w.get(), fd, event); + ev_io_start(loop, w.get()); + return; + } + } + + auto w = std::make_unique<ev_io>(); + ev_io_init(w.get(), cb, fd, event); + w->data = data; + ev_io_start(loop, w.get()); + evs.emplace_back(std::move(w)); +} +} // namespace + +namespace { +void stop_ev(std::vector<std::unique_ptr<ev_io>> &evs, struct ev_loop *loop, + int fd, int event) { + for (auto &w : evs) { + if (w->fd == fd) { + ev_io_stop(loop, w.get()); + ev_io_set(w.get(), -1, event); + return; + } + } +} +} // namespace + +void DNSResolver::start_rev(int fd) { + start_ev(revs_, loop_, fd, EV_READ, readcb, this); +} + +void DNSResolver::stop_rev(int fd) { stop_ev(revs_, loop_, fd, EV_READ); } + +void DNSResolver::start_wev(int fd) { + start_ev(wevs_, loop_, fd, EV_WRITE, writecb, this); +} + +void DNSResolver::stop_wev(int fd) { stop_ev(wevs_, loop_, fd, EV_WRITE); } + +void DNSResolver::on_result(int status, hostent *hostent) { + stop_ev(loop_, revs_); + stop_ev(loop_, wevs_); + ev_timer_stop(loop_, &timer_); + + if (status != ARES_SUCCESS) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup for " << name_ + << " failed: " << ares_strerror(status); + } + status_ = DNSResolverStatus::ERROR; + return; + } + + auto ap = *hostent->h_addr_list; + if (!ap) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup for " << name_ << "failed: no address returned"; + } + status_ = DNSResolverStatus::ERROR; + return; + } + + switch (hostent->h_addrtype) { + case AF_INET: + status_ = DNSResolverStatus::OK; + result_.len = sizeof(result_.su.in); + result_.su.in = {}; + result_.su.in.sin_family = AF_INET; +#ifdef HAVE_SOCKADDR_IN_SIN_LEN + result_.su.in.sin_len = sizeof(result_.su.in); +#endif // HAVE_SOCKADDR_IN_SIN_LEN + memcpy(&result_.su.in.sin_addr, ap, sizeof(result_.su.in.sin_addr)); + break; + case AF_INET6: + status_ = DNSResolverStatus::OK; + result_.len = sizeof(result_.su.in6); + result_.su.in6 = {}; + result_.su.in6.sin6_family = AF_INET6; +#ifdef HAVE_SOCKADDR_IN6_SIN6_LEN + result_.su.in6.sin6_len = sizeof(result_.su.in6); +#endif // HAVE_SOCKADDR_IN6_SIN6_LEN + memcpy(&result_.su.in6.sin6_addr, ap, sizeof(result_.su.in6.sin6_addr)); + break; + default: + assert(0); + } + + if (status_ == DNSResolverStatus::OK) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup succeeded: " << name_ << " -> " + << util::numeric_name(&result_.su.sa, result_.len); + } + return; + } + + status_ = DNSResolverStatus::ERROR; +} + +void DNSResolver::set_complete_cb(CompleteCb cb) { + completeCb_ = std::move(cb); +} + +CompleteCb DNSResolver::get_complete_cb() const { return completeCb_; } + +} // namespace shrpx diff --git a/src/shrpx_dns_resolver.h b/src/shrpx_dns_resolver.h new file mode 100644 index 0000000..e622f99 --- /dev/null +++ b/src/shrpx_dns_resolver.h @@ -0,0 +1,118 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DNS_RESOLVER_H +#define SHRPX_DNS_RESOLVER_H + +#include "shrpx.h" + +#include <sys/socket.h> +#include <netinet/in.h> + +#include <vector> + +#include <ev.h> +#include <ares.h> + +#include "template.h" +#include "network.h" + +using namespace nghttp2; + +namespace shrpx { + +enum class DNSResolverStatus { + // Resolver is in initial status + IDLE, + // Resolver is currently resolving host name + RUNNING, + // Resolver successfully resolved host name + OK, + // Resolver failed to resolve host name + ERROR, +}; + +// Callback function called when host name lookup is finished. +// |status| is either DNSResolverStatus::OK, or +// DNSResolverStatus::ERROR. If |status| is DNSResolverStatus::OK, +// |result| points to the resolved address. Note that port portion of +// |result| is undefined, and must be initialized by application. +// This callback function is not called if name lookup finishes in +// DNSResolver::resolve() completely. In this case, application +// should call DNSResolver::get_status() to get current status and +// result. In other words, callback is called if get_status() returns +// DNSResolverStatus::RUNNING. +using CompleteCb = + std::function<void(DNSResolverStatus status, const Address *result)>; + +// DNSResolver is asynchronous name resolver, backed by c-ares +// library. +class DNSResolver { +public: + DNSResolver(struct ev_loop *loop); + ~DNSResolver(); + + // Starts resolving hostname |name|. + int resolve(const StringRef &name, int family); + // Returns status. If status_ is DNSResolverStatus::SUCCESS && + // |result| is not nullptr, |*result| is filled. + DNSResolverStatus get_status(Address *result) const; + // Sets callback function when name lookup finishes. The callback + // function is called in a way that it can destroy this DNSResolver. + void set_complete_cb(CompleteCb cb); + CompleteCb get_complete_cb() const; + + // Calls these functions when read/write event occurred respectively. + int on_read(int fd); + int on_write(int fd); + int on_timeout(); + // Calls this function when DNS query finished. + void on_result(int status, hostent *hostent); + void reset_timeout(); + + void start_rev(int fd); + void stop_rev(int fd); + void start_wev(int fd); + void stop_wev(int fd); + +private: + int handle_event(int rfd, int wfd); + + std::vector<std::unique_ptr<ev_io>> revs_, wevs_; + Address result_; + CompleteCb completeCb_; + ev_timer timer_; + StringRef name_; + struct ev_loop *loop_; + // ares_channel is pointer type + ares_channel channel_; + // AF_INET or AF_INET6. AF_INET for A record lookup, and AF_INET6 + // for AAAA record lookup. + int family_; + DNSResolverStatus status_; +}; + +} // namespace shrpx + +#endif // SHRPX_DNS_RESOLVER_H diff --git a/src/shrpx_dns_tracker.cc b/src/shrpx_dns_tracker.cc new file mode 100644 index 0000000..57387ce --- /dev/null +++ b/src/shrpx_dns_tracker.cc @@ -0,0 +1,328 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_dns_tracker.h" +#include "shrpx_config.h" +#include "shrpx_log.h" +#include "util.h" + +namespace shrpx { + +namespace { +void gccb(struct ev_loop *loop, ev_timer *w, int revents) { + auto dns_tracker = static_cast<DNSTracker *>(w->data); + dns_tracker->gc(); +} +} // namespace + +DNSTracker::DNSTracker(struct ev_loop *loop, int family) + : loop_(loop), family_(family) { + ev_timer_init(&gc_timer_, gccb, 0., 12_h); + gc_timer_.data = this; +} + +DNSTracker::~DNSTracker() { + ev_timer_stop(loop_, &gc_timer_); + + for (auto &p : ents_) { + auto &qlist = p.second.qlist; + while (!qlist.empty()) { + auto head = qlist.head; + qlist.remove(head); + head->status = DNSResolverStatus::ERROR; + head->in_qlist = false; + // TODO Not sure we should call callback here, or it is even be + // safe to do that. + } + } +} + +ResolverEntry DNSTracker::make_entry(std::unique_ptr<DualDNSResolver> resolv, + ImmutableString host, + DNSResolverStatus status, + const Address *result) { + auto &dnsconf = get_config()->dns; + + auto ent = ResolverEntry{}; + ent.resolv = std::move(resolv); + ent.host = std::move(host); + ent.status = status; + switch (status) { + case DNSResolverStatus::ERROR: + case DNSResolverStatus::OK: + ent.expiry = std::chrono::steady_clock::now() + + util::duration_from(dnsconf.timeout.cache); + break; + default: + break; + } + if (result) { + ent.result = *result; + } + return ent; +} + +void DNSTracker::update_entry(ResolverEntry &ent, + std::unique_ptr<DualDNSResolver> resolv, + DNSResolverStatus status, const Address *result) { + auto &dnsconf = get_config()->dns; + + ent.resolv = std::move(resolv); + ent.status = status; + switch (status) { + case DNSResolverStatus::ERROR: + case DNSResolverStatus::OK: + ent.expiry = std::chrono::steady_clock::now() + + util::duration_from(dnsconf.timeout.cache); + break; + default: + break; + } + if (result) { + ent.result = *result; + } +} + +DNSResolverStatus DNSTracker::resolve(Address *result, DNSQuery *dnsq) { + int rv; + + auto it = ents_.find(dnsq->host); + + if (it == std::end(ents_)) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "DNS entry not found for " << dnsq->host; + } + + auto resolv = std::make_unique<DualDNSResolver>(loop_, family_); + auto host_copy = + ImmutableString{std::begin(dnsq->host), std::end(dnsq->host)}; + auto host = StringRef{host_copy}; + + rv = resolv->resolve(host); + if (rv != 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup failed for " << host; + } + + ents_.emplace(host, make_entry(nullptr, std::move(host_copy), + DNSResolverStatus::ERROR, nullptr)); + + start_gc_timer(); + + return DNSResolverStatus::ERROR; + } + + switch (resolv->get_status(result)) { + case DNSResolverStatus::ERROR: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup failed for " << host; + } + + ents_.emplace(host, make_entry(nullptr, std::move(host_copy), + DNSResolverStatus::ERROR, nullptr)); + + start_gc_timer(); + + return DNSResolverStatus::ERROR; + case DNSResolverStatus::OK: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup succeeded: " << host << " -> " + << util::numeric_name(&result->su.sa, result->len); + } + + ents_.emplace(host, make_entry(nullptr, std::move(host_copy), + DNSResolverStatus::OK, result)); + + start_gc_timer(); + + return DNSResolverStatus::OK; + case DNSResolverStatus::RUNNING: { + auto p = ents_.emplace(host, + make_entry(std::move(resolv), std::move(host_copy), + DNSResolverStatus::RUNNING, nullptr)); + + start_gc_timer(); + + auto &ent = (*p.first).second; + + add_to_qlist(ent, dnsq); + + return DNSResolverStatus::RUNNING; + } + default: + assert(0); + } + } + + auto &ent = (*it).second; + + if (ent.status != DNSResolverStatus::RUNNING && + ent.expiry < std::chrono::steady_clock::now()) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "DNS entry found for " << dnsq->host + << ", but it has been expired"; + } + + auto resolv = std::make_unique<DualDNSResolver>(loop_, family_); + auto host = StringRef{ent.host}; + + rv = resolv->resolve(host); + if (rv != 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup failed for " << host; + } + + update_entry(ent, nullptr, DNSResolverStatus::ERROR, nullptr); + + return DNSResolverStatus::ERROR; + } + + switch (resolv->get_status(result)) { + case DNSResolverStatus::ERROR: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup failed for " << host; + } + + update_entry(ent, nullptr, DNSResolverStatus::ERROR, nullptr); + + return DNSResolverStatus::ERROR; + case DNSResolverStatus::OK: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup succeeded: " << host << " -> " + << util::numeric_name(&result->su.sa, result->len); + } + + update_entry(ent, nullptr, DNSResolverStatus::OK, result); + + return DNSResolverStatus::OK; + case DNSResolverStatus::RUNNING: + update_entry(ent, std::move(resolv), DNSResolverStatus::RUNNING, nullptr); + add_to_qlist(ent, dnsq); + + return DNSResolverStatus::RUNNING; + default: + assert(0); + } + } + + switch (ent.status) { + case DNSResolverStatus::RUNNING: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Waiting for name lookup complete for " << dnsq->host; + } + ent.qlist.append(dnsq); + dnsq->in_qlist = true; + return DNSResolverStatus::RUNNING; + case DNSResolverStatus::ERROR: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup failed for " << dnsq->host << " (cached)"; + } + return DNSResolverStatus::ERROR; + case DNSResolverStatus::OK: + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Name lookup succeeded (cached): " << dnsq->host << " -> " + << util::numeric_name(&ent.result.su.sa, ent.result.len); + } + if (result) { + memcpy(result, &ent.result, sizeof(*result)); + } + return DNSResolverStatus::OK; + default: + assert(0); + abort(); + } +} + +void DNSTracker::add_to_qlist(ResolverEntry &ent, DNSQuery *dnsq) { + ent.resolv->set_complete_cb( + [&ent](DNSResolverStatus status, const Address *result) { + auto &qlist = ent.qlist; + while (!qlist.empty()) { + auto head = qlist.head; + qlist.remove(head); + head->status = status; + head->in_qlist = false; + auto cb = head->cb; + cb(status, result); + } + + auto &dnsconf = get_config()->dns; + + ent.resolv.reset(); + ent.status = status; + ent.expiry = std::chrono::steady_clock::now() + + util::duration_from(dnsconf.timeout.cache); + if (ent.status == DNSResolverStatus::OK) { + ent.result = *result; + } + }); + ent.qlist.append(dnsq); + dnsq->in_qlist = true; +} + +void DNSTracker::cancel(DNSQuery *dnsq) { + if (!dnsq->in_qlist) { + return; + } + + auto it = ents_.find(dnsq->host); + if (it == std::end(ents_)) { + return; + } + + auto &ent = (*it).second; + ent.qlist.remove(dnsq); + dnsq->in_qlist = false; +} + +void DNSTracker::start_gc_timer() { + if (ev_is_active(&gc_timer_)) { + return; + } + + ev_timer_again(loop_, &gc_timer_); +} + +void DNSTracker::gc() { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Starting removing expired DNS cache entries"; + } + + auto now = std::chrono::steady_clock::now(); + for (auto it = std::begin(ents_); it != std::end(ents_);) { + auto &ent = (*it).second; + if (ent.expiry >= now) { + ++it; + continue; + } + + it = ents_.erase(it); + } + + if (ents_.empty()) { + ev_timer_stop(loop_, &gc_timer_); + } +} + +} // namespace shrpx diff --git a/src/shrpx_dns_tracker.h b/src/shrpx_dns_tracker.h new file mode 100644 index 0000000..c7caac0 --- /dev/null +++ b/src/shrpx_dns_tracker.h @@ -0,0 +1,121 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DNS_TRACKER_H +#define SHRPX_DNS_TRACKER_H + +#include "shrpx.h" + +#include <map> +#include <chrono> + +#include "shrpx_dual_dns_resolver.h" + +using namespace nghttp2; + +namespace shrpx { + +struct DNSQuery { + DNSQuery(StringRef host, CompleteCb cb) + : host(std::move(host)), + cb(std::move(cb)), + dlnext(nullptr), + dlprev(nullptr), + status(DNSResolverStatus::IDLE), + in_qlist(false) {} + + // Host name we lookup for. + StringRef host; + // Callback function called when name lookup finished. This + // callback is not called if name lookup finishes within + // DNSTracker::resolve(). + CompleteCb cb; + DNSQuery *dlnext, *dlprev; + DNSResolverStatus status; + // true if this object is in linked list ResolverEntry::qlist. + bool in_qlist; +}; + +struct ResolverEntry { + // Host name this entry lookups for. + ImmutableString host; + // DNS resolver. Only non-nullptr if status is + // DNSResolverStatus::RUNNING. + std::unique_ptr<DualDNSResolver> resolv; + // DNSQuery interested in this name lookup result. The result is + // notified to them all. + DList<DNSQuery> qlist; + // Use the same enum with DNSResolverStatus + DNSResolverStatus status; + // result and its expiry time + Address result; + // time point when cached result expires. + std::chrono::steady_clock::time_point expiry; +}; + +class DNSTracker { +public: + DNSTracker(struct ev_loop *loop, int family); + ~DNSTracker(); + + // Lookups host name described in |dnsq|. If name lookup finishes + // within this function (either it came from /etc/hosts, host name + // is numeric, lookup result is cached, etc), it returns + // DNSResolverStatus::OK or DNSResolverStatus::ERROR. If lookup is + // successful, DNSResolverStatus::OK is returned, and |result| is + // filled. If lookup failed, DNSResolverStatus::ERROR is returned. + // If name lookup is being done background, it returns + // DNSResolverStatus::RUNNING. Its completion is notified by + // calling dnsq->cb. + DNSResolverStatus resolve(Address *result, DNSQuery *dnsq); + // Cancels name lookup requested by |dnsq|. + void cancel(DNSQuery *dnsq); + // Removes expired entries from ents_. + void gc(); + // Starts GC timer. + void start_gc_timer(); + +private: + ResolverEntry make_entry(std::unique_ptr<DualDNSResolver> resolv, + ImmutableString host, DNSResolverStatus status, + const Address *result); + + void update_entry(ResolverEntry &ent, std::unique_ptr<DualDNSResolver> resolv, + DNSResolverStatus status, const Address *result); + + void add_to_qlist(ResolverEntry &ent, DNSQuery *dnsq); + + std::map<StringRef, ResolverEntry> ents_; + // Periodically iterates ents_, and removes expired entries to avoid + // excessive use of memory. Since only backend API can potentially + // increase memory consumption, interval could be very long. + ev_timer gc_timer_; + struct ev_loop *loop_; + // IP version preference. + int family_; +}; + +} // namespace shrpx + +#endif // SHRPX_DNS_TRACKER_H diff --git a/src/shrpx_downstream.cc b/src/shrpx_downstream.cc new file mode 100644 index 0000000..9ea52b4 --- /dev/null +++ b/src/shrpx_downstream.cc @@ -0,0 +1,1189 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_downstream.h" + +#include <cassert> + +#include "url-parser/url_parser.h" + +#include "shrpx_upstream.h" +#include "shrpx_client_handler.h" +#include "shrpx_config.h" +#include "shrpx_error.h" +#include "shrpx_downstream_connection.h" +#include "shrpx_downstream_queue.h" +#include "shrpx_worker.h" +#include "shrpx_http2_session.h" +#include "shrpx_log.h" +#ifdef HAVE_MRUBY +# include "shrpx_mruby.h" +#endif // HAVE_MRUBY +#include "util.h" +#include "http2.h" + +namespace shrpx { + +namespace { +void upstream_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto downstream = static_cast<Downstream *>(w->data); + auto upstream = downstream->get_upstream(); + + auto which = revents == EV_READ ? "read" : "write"; + + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "upstream timeout stream_id=" + << downstream->get_stream_id() << " event=" << which; + } + + downstream->disable_upstream_rtimer(); + downstream->disable_upstream_wtimer(); + + upstream->on_timeout(downstream); +} +} // namespace + +namespace { +void upstream_rtimeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + upstream_timeoutcb(loop, w, EV_READ); +} +} // namespace + +namespace { +void upstream_wtimeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + upstream_timeoutcb(loop, w, EV_WRITE); +} +} // namespace + +namespace { +void downstream_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto downstream = static_cast<Downstream *>(w->data); + + auto which = revents == EV_READ ? "read" : "write"; + + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "downstream timeout stream_id=" + << downstream->get_downstream_stream_id() + << " event=" << which; + } + + downstream->disable_downstream_rtimer(); + downstream->disable_downstream_wtimer(); + + auto dconn = downstream->get_downstream_connection(); + + if (dconn) { + dconn->on_timeout(); + } +} +} // namespace + +namespace { +void downstream_rtimeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + downstream_timeoutcb(loop, w, EV_READ); +} +} // namespace + +namespace { +void downstream_wtimeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + downstream_timeoutcb(loop, w, EV_WRITE); +} +} // namespace + +// upstream could be nullptr for unittests +Downstream::Downstream(Upstream *upstream, MemchunkPool *mcpool, + int64_t stream_id) + : dlnext(nullptr), + dlprev(nullptr), + response_sent_body_length(0), + balloc_(1024, 1024), + req_(balloc_), + resp_(balloc_), + request_start_time_(std::chrono::high_resolution_clock::now()), + blocked_request_buf_(mcpool), + request_buf_(mcpool), + response_buf_(mcpool), + upstream_(upstream), + blocked_link_(nullptr), + addr_(nullptr), + num_retry_(0), + stream_id_(stream_id), + assoc_stream_id_(-1), + downstream_stream_id_(-1), + response_rst_stream_error_code_(NGHTTP2_NO_ERROR), + affinity_cookie_(0), + request_state_(DownstreamState::INITIAL), + response_state_(DownstreamState::INITIAL), + dispatch_state_(DispatchState::NONE), + upgraded_(false), + chunked_request_(false), + chunked_response_(false), + expect_final_response_(false), + request_pending_(false), + request_header_sent_(false), + accesslog_written_(false), + new_affinity_cookie_(false), + blocked_request_data_eof_(false), + expect_100_continue_(false), + stop_reading_(false) { + + auto &timeoutconf = get_config()->http2.timeout; + + ev_timer_init(&upstream_rtimer_, &upstream_rtimeoutcb, 0., + timeoutconf.stream_read); + ev_timer_init(&upstream_wtimer_, &upstream_wtimeoutcb, 0., + timeoutconf.stream_write); + ev_timer_init(&downstream_rtimer_, &downstream_rtimeoutcb, 0., + timeoutconf.stream_read); + ev_timer_init(&downstream_wtimer_, &downstream_wtimeoutcb, 0., + timeoutconf.stream_write); + + upstream_rtimer_.data = this; + upstream_wtimer_.data = this; + downstream_rtimer_.data = this; + downstream_wtimer_.data = this; + + rcbufs_.reserve(32); +#ifdef ENABLE_HTTP3 + rcbufs3_.reserve(32); +#endif // ENABLE_HTTP3 +} + +Downstream::~Downstream() { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, this) << "Deleting"; + } + + // check nullptr for unittest + if (upstream_) { + auto loop = upstream_->get_client_handler()->get_loop(); + + ev_timer_stop(loop, &upstream_rtimer_); + ev_timer_stop(loop, &upstream_wtimer_); + ev_timer_stop(loop, &downstream_rtimer_); + ev_timer_stop(loop, &downstream_wtimer_); + +#ifdef HAVE_MRUBY + auto handler = upstream_->get_client_handler(); + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + mruby_ctx->delete_downstream(this); +#endif // HAVE_MRUBY + } + +#ifdef HAVE_MRUBY + if (dconn_) { + const auto &group = dconn_->get_downstream_addr_group(); + if (group) { + const auto &mruby_ctx = group->shared_addr->mruby_ctx; + mruby_ctx->delete_downstream(this); + } + } +#endif // HAVE_MRUBY + + // DownstreamConnection may refer to this object. Delete it now + // explicitly. + dconn_.reset(); + +#ifdef ENABLE_HTTP3 + for (auto rcbuf : rcbufs3_) { + nghttp3_rcbuf_decref(rcbuf); + } +#endif // ENABLE_HTTP3 + + for (auto rcbuf : rcbufs_) { + nghttp2_rcbuf_decref(rcbuf); + } + + if (LOG_ENABLED(INFO)) { + DLOG(INFO, this) << "Deleted"; + } +} + +int Downstream::attach_downstream_connection( + std::unique_ptr<DownstreamConnection> dconn) { + if (dconn->attach_downstream(this) != 0) { + return -1; + } + + dconn_ = std::move(dconn); + + return 0; +} + +void Downstream::detach_downstream_connection() { + if (!dconn_) { + return; + } + +#ifdef HAVE_MRUBY + const auto &group = dconn_->get_downstream_addr_group(); + if (group) { + const auto &mruby_ctx = group->shared_addr->mruby_ctx; + mruby_ctx->delete_downstream(this); + } +#endif // HAVE_MRUBY + + dconn_->detach_downstream(this); + + auto handler = dconn_->get_client_handler(); + + handler->pool_downstream_connection( + std::unique_ptr<DownstreamConnection>(dconn_.release())); +} + +DownstreamConnection *Downstream::get_downstream_connection() { + return dconn_.get(); +} + +std::unique_ptr<DownstreamConnection> Downstream::pop_downstream_connection() { +#ifdef HAVE_MRUBY + if (!dconn_) { + return nullptr; + } + + const auto &group = dconn_->get_downstream_addr_group(); + if (group) { + const auto &mruby_ctx = group->shared_addr->mruby_ctx; + mruby_ctx->delete_downstream(this); + } +#endif // HAVE_MRUBY + + return std::unique_ptr<DownstreamConnection>(dconn_.release()); +} + +void Downstream::pause_read(IOCtrlReason reason) { + if (dconn_) { + dconn_->pause_read(reason); + } +} + +int Downstream::resume_read(IOCtrlReason reason, size_t consumed) { + if (dconn_) { + return dconn_->resume_read(reason, consumed); + } + + return 0; +} + +void Downstream::force_resume_read() { + if (dconn_) { + dconn_->force_resume_read(); + } +} + +namespace { +const HeaderRefs::value_type * +search_header_linear_backwards(const HeaderRefs &headers, + const StringRef &name) { + for (auto it = headers.rbegin(); it != headers.rend(); ++it) { + auto &kv = *it; + if (kv.name == name) { + return &kv; + } + } + return nullptr; +} +} // namespace + +StringRef Downstream::assemble_request_cookie() { + size_t len = 0; + + for (auto &kv : req_.fs.headers()) { + if (kv.token != http2::HD_COOKIE || kv.value.empty()) { + continue; + } + + len += kv.value.size() + str_size("; "); + } + + auto iov = make_byte_ref(balloc_, len + 1); + auto p = iov.base; + + for (auto &kv : req_.fs.headers()) { + if (kv.token != http2::HD_COOKIE || kv.value.empty()) { + continue; + } + + auto end = std::end(kv.value); + for (auto it = std::begin(kv.value) + kv.value.size(); + it != std::begin(kv.value); --it) { + auto c = *(it - 1); + if (c == ' ' || c == ';') { + continue; + } + end = it; + break; + } + + p = std::copy(std::begin(kv.value), end, p); + p = util::copy_lit(p, "; "); + } + + // cut trailing "; " + if (p - iov.base >= 2) { + p -= 2; + } + + return StringRef{iov.base, p}; +} + +uint32_t Downstream::find_affinity_cookie(const StringRef &name) { + for (auto &kv : req_.fs.headers()) { + if (kv.token != http2::HD_COOKIE) { + continue; + } + + for (auto it = std::begin(kv.value); it != std::end(kv.value);) { + if (*it == '\t' || *it == ' ' || *it == ';') { + ++it; + continue; + } + + auto end = std::find(it, std::end(kv.value), '='); + if (end == std::end(kv.value)) { + return 0; + } + + if (!util::streq(name, StringRef{it, end})) { + it = std::find(it, std::end(kv.value), ';'); + continue; + } + + it = std::find(end + 1, std::end(kv.value), ';'); + auto val = StringRef{end + 1, it}; + if (val.size() != 8) { + return 0; + } + uint32_t h = 0; + for (auto c : val) { + auto n = util::hex_to_uint(c); + if (n == 256) { + return 0; + } + h <<= 4; + h += n; + } + affinity_cookie_ = h; + return h; + } + } + return 0; +} + +size_t Downstream::count_crumble_request_cookie() { + size_t n = 0; + for (auto &kv : req_.fs.headers()) { + if (kv.token != http2::HD_COOKIE) { + continue; + } + + for (auto it = std::begin(kv.value); it != std::end(kv.value);) { + if (*it == '\t' || *it == ' ' || *it == ';') { + ++it; + continue; + } + + it = std::find(it, std::end(kv.value), ';'); + + ++n; + } + } + return n; +} + +void Downstream::crumble_request_cookie(std::vector<nghttp2_nv> &nva) { + for (auto &kv : req_.fs.headers()) { + if (kv.token != http2::HD_COOKIE) { + continue; + } + + for (auto it = std::begin(kv.value); it != std::end(kv.value);) { + if (*it == '\t' || *it == ' ' || *it == ';') { + ++it; + continue; + } + + auto first = it; + + it = std::find(it, std::end(kv.value), ';'); + + nva.push_back({(uint8_t *)"cookie", (uint8_t *)first, str_size("cookie"), + (size_t)(it - first), + (uint8_t)(NGHTTP2_NV_FLAG_NO_COPY_NAME | + NGHTTP2_NV_FLAG_NO_COPY_VALUE | + (kv.no_index ? NGHTTP2_NV_FLAG_NO_INDEX : 0))}); + } + } +} + +namespace { +void add_header(size_t &sum, HeaderRefs &headers, const StringRef &name, + const StringRef &value, bool no_index, int32_t token) { + sum += name.size() + value.size(); + headers.emplace_back(name, value, no_index, token); +} +} // namespace + +namespace { +StringRef alloc_header_name(BlockAllocator &balloc, const StringRef &name) { + auto iov = make_byte_ref(balloc, name.size() + 1); + auto p = iov.base; + p = std::copy(std::begin(name), std::end(name), p); + util::inp_strlower(iov.base, p); + *p = '\0'; + + return StringRef{iov.base, p}; +} +} // namespace + +namespace { +void append_last_header_key(BlockAllocator &balloc, bool &key_prev, size_t &sum, + HeaderRefs &headers, const char *data, size_t len) { + assert(key_prev); + sum += len; + auto &item = headers.back(); + auto name = + realloc_concat_string_ref(balloc, item.name, StringRef{data, len}); + + auto p = const_cast<uint8_t *>(name.byte()); + util::inp_strlower(p + name.size() - len, p + name.size()); + + item.name = name; + item.token = http2::lookup_token(item.name); +} +} // namespace + +namespace { +void append_last_header_value(BlockAllocator &balloc, bool &key_prev, + size_t &sum, HeaderRefs &headers, + const char *data, size_t len) { + key_prev = false; + sum += len; + auto &item = headers.back(); + item.value = + realloc_concat_string_ref(balloc, item.value, StringRef{data, len}); +} +} // namespace + +int FieldStore::parse_content_length() { + content_length = -1; + + for (auto &kv : headers_) { + if (kv.token != http2::HD_CONTENT_LENGTH) { + continue; + } + + auto len = util::parse_uint(kv.value); + if (len == -1) { + return -1; + } + if (content_length != -1) { + return -1; + } + content_length = len; + } + return 0; +} + +const HeaderRefs::value_type *FieldStore::header(int32_t token) const { + for (auto it = headers_.rbegin(); it != headers_.rend(); ++it) { + auto &kv = *it; + if (kv.token == token) { + return &kv; + } + } + return nullptr; +} + +HeaderRefs::value_type *FieldStore::header(int32_t token) { + for (auto it = headers_.rbegin(); it != headers_.rend(); ++it) { + auto &kv = *it; + if (kv.token == token) { + return &kv; + } + } + return nullptr; +} + +const HeaderRefs::value_type *FieldStore::header(const StringRef &name) const { + return search_header_linear_backwards(headers_, name); +} + +void FieldStore::add_header_token(const StringRef &name, const StringRef &value, + bool no_index, int32_t token) { + shrpx::add_header(buffer_size_, headers_, name, value, no_index, token); +} + +void FieldStore::alloc_add_header_name(const StringRef &name) { + auto name_ref = alloc_header_name(balloc_, name); + auto token = http2::lookup_token(name_ref); + add_header_token(name_ref, StringRef{}, false, token); + header_key_prev_ = true; +} + +void FieldStore::append_last_header_key(const char *data, size_t len) { + shrpx::append_last_header_key(balloc_, header_key_prev_, buffer_size_, + headers_, data, len); +} + +void FieldStore::append_last_header_value(const char *data, size_t len) { + shrpx::append_last_header_value(balloc_, header_key_prev_, buffer_size_, + headers_, data, len); +} + +void FieldStore::clear_headers() { + headers_.clear(); + header_key_prev_ = false; +} + +void FieldStore::add_trailer_token(const StringRef &name, + const StringRef &value, bool no_index, + int32_t token) { + // Header size limit should be applied to all header and trailer + // fields combined. + shrpx::add_header(buffer_size_, trailers_, name, value, no_index, token); +} + +void FieldStore::alloc_add_trailer_name(const StringRef &name) { + auto name_ref = alloc_header_name(balloc_, name); + auto token = http2::lookup_token(name_ref); + add_trailer_token(name_ref, StringRef{}, false, token); + trailer_key_prev_ = true; +} + +void FieldStore::append_last_trailer_key(const char *data, size_t len) { + shrpx::append_last_header_key(balloc_, trailer_key_prev_, buffer_size_, + trailers_, data, len); +} + +void FieldStore::append_last_trailer_value(const char *data, size_t len) { + shrpx::append_last_header_value(balloc_, trailer_key_prev_, buffer_size_, + trailers_, data, len); +} + +void FieldStore::erase_content_length_and_transfer_encoding() { + for (auto &kv : headers_) { + switch (kv.token) { + case http2::HD_CONTENT_LENGTH: + case http2::HD_TRANSFER_ENCODING: + kv.name = StringRef{}; + kv.token = -1; + break; + } + } +} + +void Downstream::set_request_start_time( + std::chrono::high_resolution_clock::time_point time) { + request_start_time_ = std::move(time); +} + +const std::chrono::high_resolution_clock::time_point & +Downstream::get_request_start_time() const { + return request_start_time_; +} + +void Downstream::reset_upstream(Upstream *upstream) { + upstream_ = upstream; + if (dconn_) { + dconn_->on_upstream_change(upstream); + } +} + +Upstream *Downstream::get_upstream() const { return upstream_; } + +void Downstream::set_stream_id(int64_t stream_id) { stream_id_ = stream_id; } + +int64_t Downstream::get_stream_id() const { return stream_id_; } + +void Downstream::set_request_state(DownstreamState state) { + request_state_ = state; +} + +DownstreamState Downstream::get_request_state() const { return request_state_; } + +bool Downstream::get_chunked_request() const { return chunked_request_; } + +void Downstream::set_chunked_request(bool f) { chunked_request_ = f; } + +bool Downstream::request_buf_full() { + auto handler = upstream_->get_client_handler(); + auto faddr = handler->get_upstream_addr(); + auto worker = handler->get_worker(); + + // We don't check buffer size here for API endpoint. + if (faddr->alt_mode == UpstreamAltMode::API) { + return false; + } + + if (dconn_) { + auto &downstreamconf = *worker->get_downstream_config(); + return blocked_request_buf_.rleft() + request_buf_.rleft() >= + downstreamconf.request_buffer_size; + } + + return false; +} + +DefaultMemchunks *Downstream::get_request_buf() { return &request_buf_; } + +// Call this function after this object is attached to +// Downstream. Otherwise, the program will crash. +int Downstream::push_request_headers() { + if (!dconn_) { + DLOG(INFO, this) << "dconn_ is NULL"; + return -1; + } + return dconn_->push_request_headers(); +} + +int Downstream::push_upload_data_chunk(const uint8_t *data, size_t datalen) { + req_.recv_body_length += datalen; + + if (!dconn_ && !request_header_sent_) { + blocked_request_buf_.append(data, datalen); + req_.unconsumed_body_length += datalen; + return 0; + } + + // Assumes that request headers have already been pushed to output + // buffer using push_request_headers(). + if (!dconn_) { + DLOG(INFO, this) << "dconn_ is NULL"; + return -1; + } + if (dconn_->push_upload_data_chunk(data, datalen) != 0) { + return -1; + } + + req_.unconsumed_body_length += datalen; + + return 0; +} + +int Downstream::end_upload_data() { + if (!dconn_ && !request_header_sent_) { + blocked_request_data_eof_ = true; + return 0; + } + if (!dconn_) { + DLOG(INFO, this) << "dconn_ is NULL"; + return -1; + } + return dconn_->end_upload_data(); +} + +void Downstream::rewrite_location_response_header( + const StringRef &upstream_scheme) { + auto hd = resp_.fs.header(http2::HD_LOCATION); + if (!hd) { + return; + } + + if (request_downstream_host_.empty() || req_.authority.empty()) { + return; + } + + http_parser_url u{}; + auto rv = http_parser_parse_url(hd->value.c_str(), hd->value.size(), 0, &u); + if (rv != 0) { + return; + } + + auto new_uri = http2::rewrite_location_uri(balloc_, hd->value, u, + request_downstream_host_, + req_.authority, upstream_scheme); + + if (new_uri.empty()) { + return; + } + + hd->value = new_uri; +} + +bool Downstream::get_chunked_response() const { return chunked_response_; } + +void Downstream::set_chunked_response(bool f) { chunked_response_ = f; } + +int Downstream::on_read() { + if (!dconn_) { + DLOG(INFO, this) << "dconn_ is NULL"; + return -1; + } + return dconn_->on_read(); +} + +void Downstream::set_response_state(DownstreamState state) { + response_state_ = state; +} + +DownstreamState Downstream::get_response_state() const { + return response_state_; +} + +DefaultMemchunks *Downstream::get_response_buf() { return &response_buf_; } + +bool Downstream::response_buf_full() { + if (dconn_) { + auto handler = upstream_->get_client_handler(); + auto worker = handler->get_worker(); + auto &downstreamconf = *worker->get_downstream_config(); + + return response_buf_.rleft() >= downstreamconf.response_buffer_size; + } + + return false; +} + +bool Downstream::validate_request_recv_body_length() const { + if (req_.fs.content_length == -1) { + return true; + } + + if (req_.fs.content_length != req_.recv_body_length) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, this) << "request invalid bodylen: content-length=" + << req_.fs.content_length + << ", received=" << req_.recv_body_length; + } + return false; + } + + return true; +} + +bool Downstream::validate_response_recv_body_length() const { + if (!expect_response_body() || resp_.fs.content_length == -1) { + return true; + } + + if (resp_.fs.content_length != resp_.recv_body_length) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, this) << "response invalid bodylen: content-length=" + << resp_.fs.content_length + << ", received=" << resp_.recv_body_length; + } + return false; + } + + return true; +} + +void Downstream::check_upgrade_fulfilled_http2() { + // This handles nonzero req_.connect_proto and h1 frontend requests + // WebSocket upgrade. + upgraded_ = (req_.method == HTTP_CONNECT || + req_.connect_proto == ConnectProto::WEBSOCKET) && + resp_.http_status / 100 == 2; +} + +void Downstream::check_upgrade_fulfilled_http1() { + if (req_.method == HTTP_CONNECT) { + if (req_.connect_proto == ConnectProto::WEBSOCKET) { + if (resp_.http_status != 101) { + return; + } + + // This is done for HTTP/2 frontend only. + auto accept = resp_.fs.header(http2::HD_SEC_WEBSOCKET_ACCEPT); + if (!accept) { + return; + } + + std::array<uint8_t, base64::encode_length(20)> accept_buf; + auto expected = + http2::make_websocket_accept_token(accept_buf.data(), ws_key_); + + upgraded_ = expected != "" && expected == accept->value; + } else { + upgraded_ = resp_.http_status / 100 == 2; + } + + return; + } + + if (resp_.http_status == 101) { + // TODO Do more strict checking for upgrade headers + upgraded_ = req_.upgrade_request; + + return; + } +} + +void Downstream::inspect_http2_request() { + if (req_.method == HTTP_CONNECT) { + req_.upgrade_request = true; + } +} + +void Downstream::inspect_http1_request() { + if (req_.method == HTTP_CONNECT) { + req_.upgrade_request = true; + } else if (req_.http_minor > 0) { + auto upgrade = req_.fs.header(http2::HD_UPGRADE); + if (upgrade) { + const auto &val = upgrade->value; + // TODO Perform more strict checking for upgrade headers + if (util::streq_l(NGHTTP2_CLEARTEXT_PROTO_VERSION_ID, val.c_str(), + val.size())) { + req_.http2_upgrade_seen = true; + } else { + req_.upgrade_request = true; + + // TODO Should we check Sec-WebSocket-Key, and + // Sec-WebSocket-Version as well? + if (util::strieq_l("websocket", val)) { + req_.connect_proto = ConnectProto::WEBSOCKET; + } + } + } + } + auto transfer_encoding = req_.fs.header(http2::HD_TRANSFER_ENCODING); + if (transfer_encoding) { + req_.fs.content_length = -1; + } + + auto expect = req_.fs.header(http2::HD_EXPECT); + expect_100_continue_ = + expect && + util::strieq(expect->value, StringRef::from_lit("100-continue")); +} + +void Downstream::inspect_http1_response() { + auto transfer_encoding = resp_.fs.header(http2::HD_TRANSFER_ENCODING); + if (transfer_encoding) { + resp_.fs.content_length = -1; + } +} + +void Downstream::reset_response() { + resp_.http_status = 0; + resp_.http_major = 1; + resp_.http_minor = 1; +} + +bool Downstream::get_non_final_response() const { + return !upgraded_ && resp_.http_status / 100 == 1; +} + +bool Downstream::supports_non_final_response() const { + return req_.http_major == 3 || req_.http_major == 2 || + (req_.http_major == 1 && req_.http_minor == 1); +} + +bool Downstream::get_upgraded() const { return upgraded_; } + +bool Downstream::get_http2_upgrade_request() const { + return req_.http2_upgrade_seen && req_.fs.header(http2::HD_HTTP2_SETTINGS) && + response_state_ == DownstreamState::INITIAL; +} + +StringRef Downstream::get_http2_settings() const { + auto http2_settings = req_.fs.header(http2::HD_HTTP2_SETTINGS); + if (!http2_settings) { + return StringRef{}; + } + return http2_settings->value; +} + +void Downstream::set_downstream_stream_id(int64_t stream_id) { + downstream_stream_id_ = stream_id; +} + +int64_t Downstream::get_downstream_stream_id() const { + return downstream_stream_id_; +} + +uint32_t Downstream::get_response_rst_stream_error_code() const { + return response_rst_stream_error_code_; +} + +void Downstream::set_response_rst_stream_error_code(uint32_t error_code) { + response_rst_stream_error_code_ = error_code; +} + +void Downstream::set_expect_final_response(bool f) { + expect_final_response_ = f; +} + +bool Downstream::get_expect_final_response() const { + return expect_final_response_; +} + +bool Downstream::expect_response_body() const { + return !resp_.headers_only && + http2::expect_response_body(req_.method, resp_.http_status); +} + +bool Downstream::expect_response_trailer() const { + // In HTTP/2, if final response HEADERS does not bear END_STREAM it + // is possible trailer fields might come, regardless of request + // method or status code. + return !resp_.headers_only && + (resp_.http_major == 3 || resp_.http_major == 2); +} + +namespace { +void reset_timer(struct ev_loop *loop, ev_timer *w) { ev_timer_again(loop, w); } +} // namespace + +namespace { +void try_reset_timer(struct ev_loop *loop, ev_timer *w) { + if (!ev_is_active(w)) { + return; + } + ev_timer_again(loop, w); +} +} // namespace + +namespace { +void ensure_timer(struct ev_loop *loop, ev_timer *w) { + if (ev_is_active(w)) { + return; + } + ev_timer_again(loop, w); +} +} // namespace + +namespace { +void disable_timer(struct ev_loop *loop, ev_timer *w) { + ev_timer_stop(loop, w); +} +} // namespace + +void Downstream::reset_upstream_rtimer() { + if (get_config()->http2.timeout.stream_read == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + reset_timer(loop, &upstream_rtimer_); +} + +void Downstream::reset_upstream_wtimer() { + auto loop = upstream_->get_client_handler()->get_loop(); + auto &timeoutconf = get_config()->http2.timeout; + + if (timeoutconf.stream_write != 0.) { + reset_timer(loop, &upstream_wtimer_); + } + if (timeoutconf.stream_read != 0.) { + try_reset_timer(loop, &upstream_rtimer_); + } +} + +void Downstream::ensure_upstream_wtimer() { + if (get_config()->http2.timeout.stream_write == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + ensure_timer(loop, &upstream_wtimer_); +} + +void Downstream::disable_upstream_rtimer() { + if (get_config()->http2.timeout.stream_read == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + disable_timer(loop, &upstream_rtimer_); +} + +void Downstream::disable_upstream_wtimer() { + if (get_config()->http2.timeout.stream_write == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + disable_timer(loop, &upstream_wtimer_); +} + +void Downstream::reset_downstream_rtimer() { + if (get_config()->http2.timeout.stream_read == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + reset_timer(loop, &downstream_rtimer_); +} + +void Downstream::reset_downstream_wtimer() { + auto loop = upstream_->get_client_handler()->get_loop(); + auto &timeoutconf = get_config()->http2.timeout; + + if (timeoutconf.stream_write != 0.) { + reset_timer(loop, &downstream_wtimer_); + } + if (timeoutconf.stream_read != 0.) { + try_reset_timer(loop, &downstream_rtimer_); + } +} + +void Downstream::ensure_downstream_wtimer() { + if (get_config()->http2.timeout.stream_write == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + ensure_timer(loop, &downstream_wtimer_); +} + +void Downstream::disable_downstream_rtimer() { + if (get_config()->http2.timeout.stream_read == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + disable_timer(loop, &downstream_rtimer_); +} + +void Downstream::disable_downstream_wtimer() { + if (get_config()->http2.timeout.stream_write == 0.) { + return; + } + auto loop = upstream_->get_client_handler()->get_loop(); + disable_timer(loop, &downstream_wtimer_); +} + +bool Downstream::accesslog_ready() const { + return !accesslog_written_ && resp_.http_status > 0; +} + +void Downstream::add_retry() { ++num_retry_; } + +bool Downstream::no_more_retry() const { return num_retry_ > 50; } + +void Downstream::set_request_downstream_host(const StringRef &host) { + request_downstream_host_ = host; +} + +void Downstream::set_request_pending(bool f) { request_pending_ = f; } + +bool Downstream::get_request_pending() const { return request_pending_; } + +void Downstream::set_request_header_sent(bool f) { request_header_sent_ = f; } + +bool Downstream::get_request_header_sent() const { + return request_header_sent_; +} + +bool Downstream::request_submission_ready() const { + return (request_state_ == DownstreamState::HEADER_COMPLETE || + request_state_ == DownstreamState::MSG_COMPLETE) && + (request_pending_ || !request_header_sent_) && + response_state_ == DownstreamState::INITIAL; +} + +DispatchState Downstream::get_dispatch_state() const { return dispatch_state_; } + +void Downstream::set_dispatch_state(DispatchState s) { dispatch_state_ = s; } + +void Downstream::attach_blocked_link(BlockedLink *l) { + assert(!blocked_link_); + + l->downstream = this; + blocked_link_ = l; +} + +BlockedLink *Downstream::detach_blocked_link() { + auto link = blocked_link_; + blocked_link_ = nullptr; + return link; +} + +bool Downstream::can_detach_downstream_connection() const { + // We should check request and response buffer. If request buffer + // is not empty, then we might leave downstream connection in weird + // state, especially for HTTP/1.1 + return dconn_ && response_state_ == DownstreamState::MSG_COMPLETE && + request_state_ == DownstreamState::MSG_COMPLETE && !upgraded_ && + !resp_.connection_close && request_buf_.rleft() == 0; +} + +DefaultMemchunks Downstream::pop_response_buf() { + return std::move(response_buf_); +} + +void Downstream::set_assoc_stream_id(int64_t stream_id) { + assoc_stream_id_ = stream_id; +} + +int64_t Downstream::get_assoc_stream_id() const { return assoc_stream_id_; } + +BlockAllocator &Downstream::get_block_allocator() { return balloc_; } + +void Downstream::add_rcbuf(nghttp2_rcbuf *rcbuf) { + nghttp2_rcbuf_incref(rcbuf); + rcbufs_.push_back(rcbuf); +} + +#ifdef ENABLE_HTTP3 +void Downstream::add_rcbuf(nghttp3_rcbuf *rcbuf) { + nghttp3_rcbuf_incref(rcbuf); + rcbufs3_.push_back(rcbuf); +} +#endif // ENABLE_HTTP3 + +void Downstream::set_downstream_addr_group( + const std::shared_ptr<DownstreamAddrGroup> &group) { + group_ = group; +} + +void Downstream::set_addr(const DownstreamAddr *addr) { addr_ = addr; } + +const DownstreamAddr *Downstream::get_addr() const { return addr_; } + +void Downstream::set_accesslog_written(bool f) { accesslog_written_ = f; } + +void Downstream::renew_affinity_cookie(uint32_t h) { + affinity_cookie_ = h; + new_affinity_cookie_ = true; +} + +uint32_t Downstream::get_affinity_cookie_to_send() const { + if (new_affinity_cookie_) { + return affinity_cookie_; + } + return 0; +} + +DefaultMemchunks *Downstream::get_blocked_request_buf() { + return &blocked_request_buf_; +} + +bool Downstream::get_blocked_request_data_eof() const { + return blocked_request_data_eof_; +} + +void Downstream::set_blocked_request_data_eof(bool f) { + blocked_request_data_eof_ = f; +} + +void Downstream::set_ws_key(const StringRef &key) { ws_key_ = key; } + +bool Downstream::get_expect_100_continue() const { + return expect_100_continue_; +} + +bool Downstream::get_stop_reading() const { return stop_reading_; } + +void Downstream::set_stop_reading(bool f) { stop_reading_ = f; } + +} // namespace shrpx diff --git a/src/shrpx_downstream.h b/src/shrpx_downstream.h new file mode 100644 index 0000000..146cae5 --- /dev/null +++ b/src/shrpx_downstream.h @@ -0,0 +1,628 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DOWNSTREAM_H +#define SHRPX_DOWNSTREAM_H + +#include "shrpx.h" + +#include <cinttypes> +#include <vector> +#include <string> +#include <memory> +#include <chrono> +#include <algorithm> + +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#ifdef ENABLE_HTTP3 +# include <nghttp3/nghttp3.h> +#endif // ENABLE_HTTP3 + +#include "llhttp.h" + +#include "shrpx_io_control.h" +#include "shrpx_log_config.h" +#include "http2.h" +#include "memchunk.h" +#include "allocator.h" + +using namespace nghttp2; + +namespace shrpx { + +class Upstream; +class DownstreamConnection; +struct BlockedLink; +struct DownstreamAddrGroup; +struct DownstreamAddr; + +class FieldStore { +public: + FieldStore(BlockAllocator &balloc, size_t headers_initial_capacity) + : content_length(-1), + balloc_(balloc), + buffer_size_(0), + header_key_prev_(false), + trailer_key_prev_(false) { + headers_.reserve(headers_initial_capacity); + } + + const HeaderRefs &headers() const { return headers_; } + const HeaderRefs &trailers() const { return trailers_; } + + HeaderRefs &headers() { return headers_; } + HeaderRefs &trailers() { return trailers_; } + + const void add_extra_buffer_size(size_t n) { buffer_size_ += n; } + size_t buffer_size() const { return buffer_size_; } + + size_t num_fields() const { return headers_.size() + trailers_.size(); } + + // Returns pointer to the header field with the name |name|. If + // multiple header have |name| as name, return last occurrence from + // the beginning. If no such header is found, returns nullptr. + const HeaderRefs::value_type *header(int32_t token) const; + HeaderRefs::value_type *header(int32_t token); + // Returns pointer to the header field with the name |name|. If no + // such header is found, returns nullptr. + const HeaderRefs::value_type *header(const StringRef &name) const; + + void add_header_token(const StringRef &name, const StringRef &value, + bool no_index, int32_t token); + + // Adds header field name |name|. First, the copy of header field + // name pointed by name.c_str() of length name.size() is made, and + // stored. + void alloc_add_header_name(const StringRef &name); + + void append_last_header_key(const char *data, size_t len); + void append_last_header_value(const char *data, size_t len); + + bool header_key_prev() const { return header_key_prev_; } + + // Parses content-length, and records it in the field. If there are + // multiple Content-Length, returns -1. + int parse_content_length(); + + // Empties headers. + void clear_headers(); + + void add_trailer_token(const StringRef &name, const StringRef &value, + bool no_index, int32_t token); + + // Adds trailer field name |name|. First, the copy of trailer field + // name pointed by name.c_str() of length name.size() is made, and + // stored. + void alloc_add_trailer_name(const StringRef &name); + + void append_last_trailer_key(const char *data, size_t len); + void append_last_trailer_value(const char *data, size_t len); + + bool trailer_key_prev() const { return trailer_key_prev_; } + + // erase_content_length_and_transfer_encoding erases content-length + // and transfer-encoding header fields. + void erase_content_length_and_transfer_encoding(); + + // content-length, -1 if it is unknown. + int64_t content_length; + +private: + BlockAllocator &balloc_; + HeaderRefs headers_; + // trailer fields. For HTTP/1.1, trailer fields are only included + // with chunked encoding. For HTTP/2, there is no such limit. + HeaderRefs trailers_; + // Sum of the length of name and value in headers_ and trailers_. + // This could also be increased by add_extra_buffer_size() to take + // into account for request URI in case of HTTP/1.x request. + size_t buffer_size_; + bool header_key_prev_; + bool trailer_key_prev_; +}; + +// Protocols allowed in HTTP/2 :protocol header field. +enum class ConnectProto { + NONE, + WEBSOCKET, +}; + +struct Request { + Request(BlockAllocator &balloc) + : fs(balloc, 16), + recv_body_length(0), + unconsumed_body_length(0), + method(-1), + http_major(1), + http_minor(1), + connect_proto(ConnectProto::NONE), + upgrade_request(false), + http2_upgrade_seen(false), + connection_close(false), + http2_expect_body(false), + no_authority(false), + forwarded_once(false) {} + + void consume(size_t len) { + assert(unconsumed_body_length >= len); + unconsumed_body_length -= len; + } + + bool regular_connect_method() const { + return method == HTTP_CONNECT && connect_proto == ConnectProto::NONE; + } + + bool extended_connect_method() const { + return connect_proto != ConnectProto::NONE; + } + + FieldStore fs; + // Timestamp when all request header fields are received. + std::shared_ptr<Timestamp> tstamp; + // Request scheme. For HTTP/2, this is :scheme header field value. + // For HTTP/1.1, this is deduced from URI or connection. + StringRef scheme; + // Request authority. This is HTTP/2 :authority header field value + // or host header field value. We may deduce it from absolute-form + // HTTP/1 request. We also store authority-form HTTP/1 request. + // This could be empty if request comes from HTTP/1.0 without Host + // header field and origin-form. + StringRef authority; + // Request path, including query component. For HTTP/1.1, this is + // request-target. For HTTP/2, this is :path header field value. + // For CONNECT request, this is empty. + StringRef path; + // This is original authority which cannot be changed by per-pattern + // mruby script. + StringRef orig_authority; + // This is original path which cannot be changed by per-pattern + // mruby script. + StringRef orig_path; + // the length of request body received so far + int64_t recv_body_length; + // The number of bytes not consumed by the application yet. + size_t unconsumed_body_length; + int method; + // HTTP major and minor version + int http_major, http_minor; + // connect_proto specified in HTTP/2 :protocol pseudo header field + // which enables extended CONNECT method. This field is also set if + // WebSocket upgrade is requested in h1 frontend for convenience. + ConnectProto connect_proto; + // Returns true if the request is HTTP upgrade (HTTP Upgrade or + // CONNECT method). Upgrade to HTTP/2 is excluded. For HTTP/2 + // Upgrade, check get_http2_upgrade_request(). + bool upgrade_request; + // true if h2c is seen in Upgrade header field. + bool http2_upgrade_seen; + bool connection_close; + // true if this is HTTP/2, and request body is expected. Note that + // we don't take into account HTTP method here. + bool http2_expect_body; + // true if request does not have any information about authority. + // This happens when: For HTTP/2 request, :authority is missing. + // For HTTP/1 request, origin or asterisk form is used. + bool no_authority; + // true if backend selection is done for request once. + // orig_authority and orig_path have the authority and path which + // are used for the first backend selection. + bool forwarded_once; +}; + +struct Response { + Response(BlockAllocator &balloc) + : fs(balloc, 32), + recv_body_length(0), + unconsumed_body_length(0), + http_status(0), + http_major(1), + http_minor(1), + connection_close(false), + headers_only(false) {} + + void consume(size_t len) { + assert(unconsumed_body_length >= len); + unconsumed_body_length -= len; + } + + // returns true if a resource denoted by scheme, authority, and path + // has already been pushed. + bool is_resource_pushed(const StringRef &scheme, const StringRef &authority, + const StringRef &path) const { + if (!pushed_resources) { + return false; + } + return std::find(std::begin(*pushed_resources), std::end(*pushed_resources), + std::make_tuple(scheme, authority, path)) != + std::end(*pushed_resources); + } + + // remember that a resource denoted by scheme, authority, and path + // is pushed. + void resource_pushed(const StringRef &scheme, const StringRef &authority, + const StringRef &path) { + if (!pushed_resources) { + pushed_resources = std::make_unique< + std::vector<std::tuple<StringRef, StringRef, StringRef>>>(); + } + pushed_resources->emplace_back(scheme, authority, path); + } + + FieldStore fs; + // array of the tuple of scheme, authority, and path of pushed + // resource. This is required because RFC 8297 says that server + // typically includes header fields appeared in non-final response + // header fields in final response header fields. Without checking + // that a particular resource has already been pushed, or not, we + // end up pushing the same resource at least twice. It is unknown + // that we should use more complex data structure (e.g., std::set) + // to find the resources faster. + std::unique_ptr<std::vector<std::tuple<StringRef, StringRef, StringRef>>> + pushed_resources; + // the length of response body received so far + int64_t recv_body_length; + // The number of bytes not consumed by the application yet. This is + // mainly for HTTP/2 backend. + size_t unconsumed_body_length; + // HTTP status code + unsigned int http_status; + int http_major, http_minor; + bool connection_close; + // true if response only consists of HEADERS, and it bears + // END_STREAM. This is used to tell Http2Upstream that it can send + // response with single HEADERS with END_STREAM flag only. + bool headers_only; +}; + +enum class DownstreamState { + INITIAL, + HEADER_COMPLETE, + MSG_COMPLETE, + STREAM_CLOSED, + CONNECT_FAIL, + MSG_RESET, + // header contains invalid header field. We can safely send error + // response (502) to a client. + MSG_BAD_HEADER, + // header fields in HTTP/1 request exceed the configuration limit. + // This state is only transitioned from INITIAL state, and solely + // used to signal 431 status code to the client. + HTTP1_REQUEST_HEADER_TOO_LARGE, +}; + +enum class DispatchState { + NONE, + PENDING, + BLOCKED, + ACTIVE, + FAILURE, +}; + +class Downstream { +public: + Downstream(Upstream *upstream, MemchunkPool *mcpool, int64_t stream_id); + ~Downstream(); + void reset_upstream(Upstream *upstream); + Upstream *get_upstream() const; + void set_stream_id(int64_t stream_id); + int64_t get_stream_id() const; + void set_assoc_stream_id(int64_t stream_id); + int64_t get_assoc_stream_id() const; + void pause_read(IOCtrlReason reason); + int resume_read(IOCtrlReason reason, size_t consumed); + void force_resume_read(); + // Set stream ID for downstream HTTP2 connection. + void set_downstream_stream_id(int64_t stream_id); + int64_t get_downstream_stream_id() const; + + int attach_downstream_connection(std::unique_ptr<DownstreamConnection> dconn); + void detach_downstream_connection(); + DownstreamConnection *get_downstream_connection(); + // Returns dconn_ and nullifies dconn_. + std::unique_ptr<DownstreamConnection> pop_downstream_connection(); + + // Returns true if output buffer is full. If underlying dconn_ is + // NULL, this function always returns false. + bool request_buf_full(); + // Returns true if upgrade (HTTP Upgrade or CONNECT) is succeeded in + // h1 backend. This should not depend on inspect_http1_response(). + void check_upgrade_fulfilled_http1(); + // Returns true if upgrade (HTTP Upgrade or CONNECT) is succeeded in + // h2 backend. + void check_upgrade_fulfilled_http2(); + // Returns true if the upgrade is succeeded as a result of the call + // check_upgrade_fulfilled_http*(). HTTP/2 Upgrade is excluded. + bool get_upgraded() const; + // Inspects HTTP/2 request. + void inspect_http2_request(); + // Inspects HTTP/1 request. This checks whether the request is + // upgrade request and tranfer-encoding etc. + void inspect_http1_request(); + // Returns true if the request is HTTP Upgrade for HTTP/2 + bool get_http2_upgrade_request() const; + // Returns the value of HTTP2-Settings request header field. + StringRef get_http2_settings() const; + + // downstream request API + const Request &request() const { return req_; } + Request &request() { return req_; } + + // Count number of crumbled cookies + size_t count_crumble_request_cookie(); + // Crumbles (split cookie by ";") in request_headers_ and adds them + // in |nva|. Headers::no_index is inherited. + void crumble_request_cookie(std::vector<nghttp2_nv> &nva); + // Assembles request cookies. The opposite operation against + // crumble_request_cookie(). + StringRef assemble_request_cookie(); + + void + set_request_start_time(std::chrono::high_resolution_clock::time_point time); + const std::chrono::high_resolution_clock::time_point & + get_request_start_time() const; + int push_request_headers(); + bool get_chunked_request() const; + void set_chunked_request(bool f); + int push_upload_data_chunk(const uint8_t *data, size_t datalen); + int end_upload_data(); + // Validates that received request body length and content-length + // matches. + bool validate_request_recv_body_length() const; + void set_request_downstream_host(const StringRef &host); + bool expect_response_body() const; + bool expect_response_trailer() const; + void set_request_state(DownstreamState state); + DownstreamState get_request_state() const; + DefaultMemchunks *get_request_buf(); + void set_request_pending(bool f); + bool get_request_pending() const; + void set_request_header_sent(bool f); + bool get_request_header_sent() const; + // Returns true if request is ready to be submitted to downstream. + // When sending pending request, get_request_pending() should be + // checked too because this function may return true when + // get_request_pending() returns false. + bool request_submission_ready() const; + + DefaultMemchunks *get_blocked_request_buf(); + bool get_blocked_request_data_eof() const; + void set_blocked_request_data_eof(bool f); + + // downstream response API + const Response &response() const { return resp_; } + Response &response() { return resp_; } + + // Rewrites the location response header field. + void rewrite_location_response_header(const StringRef &upstream_scheme); + + bool get_chunked_response() const; + void set_chunked_response(bool f); + + void set_response_state(DownstreamState state); + DownstreamState get_response_state() const; + DefaultMemchunks *get_response_buf(); + bool response_buf_full(); + // Validates that received response body length and content-length + // matches. + bool validate_response_recv_body_length() const; + uint32_t get_response_rst_stream_error_code() const; + void set_response_rst_stream_error_code(uint32_t error_code); + // Inspects HTTP/1 response. This checks tranfer-encoding etc. + void inspect_http1_response(); + // Clears some of member variables for response. + void reset_response(); + // True if the response is non-final (1xx status code). Note that + // if connection was upgraded, 101 status code is treated as final. + bool get_non_final_response() const; + // True if protocol version used by client supports non final + // response. Only HTTP/1.1 and HTTP/2 clients support it. + bool supports_non_final_response() const; + void set_expect_final_response(bool f); + bool get_expect_final_response() const; + + // Call this method when there is incoming data in downstream + // connection. + int on_read(); + + // Resets upstream read timer. If it is active, timeout value is + // reset. If it is not active, timer will be started. + void reset_upstream_rtimer(); + // Resets upstream write timer. If it is active, timeout value is + // reset. If it is not active, timer will be started. This + // function also resets read timer if it has been started. + void reset_upstream_wtimer(); + // Makes sure that upstream write timer is started. If it has been + // started, do nothing. Otherwise, write timer will be started. + void ensure_upstream_wtimer(); + // Disables upstream read timer. + void disable_upstream_rtimer(); + // Disables upstream write timer. + void disable_upstream_wtimer(); + + // Downstream timer functions. They works in a similar way just + // like the upstream timer function. + void reset_downstream_rtimer(); + void reset_downstream_wtimer(); + void ensure_downstream_wtimer(); + void disable_downstream_rtimer(); + void disable_downstream_wtimer(); + + // Returns true if accesslog can be written for this downstream. + bool accesslog_ready() const; + + // Increment retry count + void add_retry(); + // true if retry attempt should not be done. + bool no_more_retry() const; + + DispatchState get_dispatch_state() const; + void set_dispatch_state(DispatchState s); + + void attach_blocked_link(BlockedLink *l); + BlockedLink *detach_blocked_link(); + + // Returns true if downstream_connection can be detached and reused. + bool can_detach_downstream_connection() const; + + DefaultMemchunks pop_response_buf(); + + BlockAllocator &get_block_allocator(); + + void add_rcbuf(nghttp2_rcbuf *rcbuf); +#ifdef ENABLE_HTTP3 + void add_rcbuf(nghttp3_rcbuf *rcbuf); +#endif // ENABLE_HTTP3 + + void + set_downstream_addr_group(const std::shared_ptr<DownstreamAddrGroup> &group); + void set_addr(const DownstreamAddr *addr); + + const DownstreamAddr *get_addr() const; + + void set_accesslog_written(bool f); + + // Finds affinity cookie from request header fields. The name of + // cookie is given in |name|. If an affinity cookie is found, it is + // assigned to a member function, and is returned. If it is not + // found, or is malformed, returns 0. + uint32_t find_affinity_cookie(const StringRef &name); + // Set |h| as affinity cookie. + void renew_affinity_cookie(uint32_t h); + // Returns affinity cookie to send. If it does not need to be sent, + // for example, because the value is retrieved from a request header + // field, returns 0. + uint32_t get_affinity_cookie_to_send() const; + + void set_ws_key(const StringRef &key); + + bool get_expect_100_continue() const; + + bool get_stop_reading() const; + void set_stop_reading(bool f); + + enum { + EVENT_ERROR = 0x1, + EVENT_TIMEOUT = 0x2, + }; + + Downstream *dlnext, *dlprev; + + // the length of response body sent to upstream client + int64_t response_sent_body_length; + +private: + BlockAllocator balloc_; + + std::vector<nghttp2_rcbuf *> rcbufs_; +#ifdef ENABLE_HTTP3 + std::vector<nghttp3_rcbuf *> rcbufs3_; +#endif // ENABLE_HTTP3 + + Request req_; + Response resp_; + + std::chrono::high_resolution_clock::time_point request_start_time_; + + // host we requested to downstream. This is used to rewrite + // location header field to decide the location should be rewritten + // or not. + StringRef request_downstream_host_; + + // Data arrived in frontend before sending header fields to backend + // are stored in this buffer. + DefaultMemchunks blocked_request_buf_; + DefaultMemchunks request_buf_; + DefaultMemchunks response_buf_; + + // The Sec-WebSocket-Key field sent to the peer. This field is used + // if frontend uses RFC 8441 WebSocket bootstrapping via HTTP/2. + StringRef ws_key_; + + ev_timer upstream_rtimer_; + ev_timer upstream_wtimer_; + + ev_timer downstream_rtimer_; + ev_timer downstream_wtimer_; + + Upstream *upstream_; + std::unique_ptr<DownstreamConnection> dconn_; + + // only used by HTTP/2 upstream + BlockedLink *blocked_link_; + // The backend address used to fulfill this request. These are for + // logging purpose. + std::shared_ptr<DownstreamAddrGroup> group_; + const DownstreamAddr *addr_; + // How many times we tried in backend connection + size_t num_retry_; + // The stream ID in frontend connection + int64_t stream_id_; + // The associated stream ID in frontend connection if this is pushed + // stream. + int64_t assoc_stream_id_; + // stream ID in backend connection + int64_t downstream_stream_id_; + // RST_STREAM error_code from downstream HTTP2 connection + uint32_t response_rst_stream_error_code_; + // An affinity cookie value. + uint32_t affinity_cookie_; + // request state + DownstreamState request_state_; + // response state + DownstreamState response_state_; + // only used by HTTP/2 upstream + DispatchState dispatch_state_; + // true if the connection is upgraded (HTTP Upgrade or CONNECT), + // excluding upgrade to HTTP/2. + bool upgraded_; + // true if backend request uses chunked transfer-encoding + bool chunked_request_; + // true if response to client uses chunked transfer-encoding + bool chunked_response_; + // true if we have not got final response code + bool expect_final_response_; + // true if downstream request is pending because backend connection + // has not been established or should be checked before use; + // currently used only with HTTP/2 connection. + bool request_pending_; + // true if downstream request header is considered to be sent. + bool request_header_sent_; + // true if access.log has been written. + bool accesslog_written_; + // true if affinity cookie is generated for this request. + bool new_affinity_cookie_; + // true if eof is received from client before sending header fields + // to backend. + bool blocked_request_data_eof_; + // true if request contains "expect: 100-continue" header field. + bool expect_100_continue_; + bool stop_reading_; +}; + +} // namespace shrpx + +#endif // SHRPX_DOWNSTREAM_H diff --git a/src/shrpx_downstream_connection.cc b/src/shrpx_downstream_connection.cc new file mode 100644 index 0000000..16a2d6f --- /dev/null +++ b/src/shrpx_downstream_connection.cc @@ -0,0 +1,48 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_downstream_connection.h" + +#include "shrpx_client_handler.h" +#include "shrpx_downstream.h" +#include "shrpx_log.h" + +namespace shrpx { + +DownstreamConnection::DownstreamConnection() + : client_handler_(nullptr), downstream_(nullptr) {} + +DownstreamConnection::~DownstreamConnection() {} + +void DownstreamConnection::set_client_handler(ClientHandler *handler) { + client_handler_ = handler; +} + +ClientHandler *DownstreamConnection::get_client_handler() { + return client_handler_; +} + +Downstream *DownstreamConnection::get_downstream() { return downstream_; } + +} // namespace shrpx diff --git a/src/shrpx_downstream_connection.h b/src/shrpx_downstream_connection.h new file mode 100644 index 0000000..8efdcbe --- /dev/null +++ b/src/shrpx_downstream_connection.h @@ -0,0 +1,81 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DOWNSTREAM_CONNECTION_H +#define SHRPX_DOWNSTREAM_CONNECTION_H + +#include "shrpx.h" + +#include <memory> + +#include "shrpx_io_control.h" + +namespace shrpx { + +class ClientHandler; +class Upstream; +class Downstream; +struct DownstreamAddrGroup; +struct DownstreamAddr; + +class DownstreamConnection { +public: + DownstreamConnection(); + virtual ~DownstreamConnection(); + virtual int attach_downstream(Downstream *downstream) = 0; + virtual void detach_downstream(Downstream *downstream) = 0; + + virtual int push_request_headers() = 0; + virtual int push_upload_data_chunk(const uint8_t *data, size_t datalen) = 0; + virtual int end_upload_data() = 0; + + virtual void pause_read(IOCtrlReason reason) = 0; + virtual int resume_read(IOCtrlReason reason, size_t consumed) = 0; + virtual void force_resume_read() = 0; + + virtual int on_read() = 0; + virtual int on_write() = 0; + virtual int on_timeout() { return 0; } + + virtual void on_upstream_change(Upstream *upstream) = 0; + + // true if this object is poolable. + virtual bool poolable() const = 0; + + virtual const std::shared_ptr<DownstreamAddrGroup> & + get_downstream_addr_group() const = 0; + virtual DownstreamAddr *get_addr() const = 0; + + void set_client_handler(ClientHandler *client_handler); + ClientHandler *get_client_handler(); + Downstream *get_downstream(); + +protected: + ClientHandler *client_handler_; + Downstream *downstream_; +}; + +} // namespace shrpx + +#endif // SHRPX_DOWNSTREAM_CONNECTION_H diff --git a/src/shrpx_downstream_connection_pool.cc b/src/shrpx_downstream_connection_pool.cc new file mode 100644 index 0000000..0ee66b6 --- /dev/null +++ b/src/shrpx_downstream_connection_pool.cc @@ -0,0 +1,66 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_downstream_connection_pool.h" +#include "shrpx_downstream_connection.h" + +namespace shrpx { + +DownstreamConnectionPool::DownstreamConnectionPool() {} + +DownstreamConnectionPool::~DownstreamConnectionPool() { remove_all(); } + +void DownstreamConnectionPool::remove_all() { + for (auto dconn : pool_) { + delete dconn; + } + + pool_.clear(); +} + +void DownstreamConnectionPool::add_downstream_connection( + std::unique_ptr<DownstreamConnection> dconn) { + pool_.insert(dconn.release()); +} + +std::unique_ptr<DownstreamConnection> +DownstreamConnectionPool::pop_downstream_connection() { + if (pool_.empty()) { + return nullptr; + } + + auto it = std::begin(pool_); + auto dconn = std::unique_ptr<DownstreamConnection>(*it); + pool_.erase(it); + + return dconn; +} + +void DownstreamConnectionPool::remove_downstream_connection( + DownstreamConnection *dconn) { + pool_.erase(dconn); + delete dconn; +} + +} // namespace shrpx diff --git a/src/shrpx_downstream_connection_pool.h b/src/shrpx_downstream_connection_pool.h new file mode 100644 index 0000000..34dc30d --- /dev/null +++ b/src/shrpx_downstream_connection_pool.h @@ -0,0 +1,53 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DOWNSTREAM_CONNECTION_POOL_H +#define SHRPX_DOWNSTREAM_CONNECTION_POOL_H + +#include "shrpx.h" + +#include <memory> +#include <set> + +namespace shrpx { + +class DownstreamConnection; + +class DownstreamConnectionPool { +public: + DownstreamConnectionPool(); + ~DownstreamConnectionPool(); + + void add_downstream_connection(std::unique_ptr<DownstreamConnection> dconn); + std::unique_ptr<DownstreamConnection> pop_downstream_connection(); + void remove_downstream_connection(DownstreamConnection *dconn); + void remove_all(); + +private: + std::set<DownstreamConnection *> pool_; +}; + +} // namespace shrpx + +#endif // SHRPX_DOWNSTREAM_CONNECTION_POOL_H diff --git a/src/shrpx_downstream_queue.cc b/src/shrpx_downstream_queue.cc new file mode 100644 index 0000000..f8906e8 --- /dev/null +++ b/src/shrpx_downstream_queue.cc @@ -0,0 +1,175 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_downstream_queue.h" + +#include <cassert> +#include <limits> + +#include "shrpx_downstream.h" + +namespace shrpx { + +DownstreamQueue::HostEntry::HostEntry(ImmutableString &&key) + : key(std::move(key)), num_active(0) {} + +DownstreamQueue::DownstreamQueue(size_t conn_max_per_host, bool unified_host) + : conn_max_per_host_(conn_max_per_host == 0 + ? std::numeric_limits<size_t>::max() + : conn_max_per_host), + unified_host_(unified_host) {} + +DownstreamQueue::~DownstreamQueue() { + dlist_delete_all(downstreams_); + for (auto &p : host_entries_) { + auto &ent = p.second; + dlist_delete_all(ent.blocked); + } +} + +void DownstreamQueue::add_pending(std::unique_ptr<Downstream> downstream) { + downstream->set_dispatch_state(DispatchState::PENDING); + downstreams_.append(downstream.release()); +} + +void DownstreamQueue::mark_failure(Downstream *downstream) { + downstream->set_dispatch_state(DispatchState::FAILURE); +} + +DownstreamQueue::HostEntry & +DownstreamQueue::find_host_entry(const StringRef &host) { + auto itr = host_entries_.find(host); + if (itr == std::end(host_entries_)) { + auto key = ImmutableString{std::begin(host), std::end(host)}; + auto key_ref = StringRef{key}; +#ifdef HAVE_STD_MAP_EMPLACE + std::tie(itr, std::ignore) = + host_entries_.emplace(key_ref, HostEntry(std::move(key))); +#else // !HAVE_STD_MAP_EMPLACE + // for g++-4.7 + std::tie(itr, std::ignore) = host_entries_.insert( + std::make_pair(key_ref, HostEntry(std::move(key)))); +#endif // !HAVE_STD_MAP_EMPLACE + } + return (*itr).second; +} + +StringRef DownstreamQueue::make_host_key(const StringRef &host) const { + return unified_host_ ? StringRef{} : host; +} + +StringRef DownstreamQueue::make_host_key(Downstream *downstream) const { + return make_host_key(downstream->request().authority); +} + +void DownstreamQueue::mark_active(Downstream *downstream) { + auto &ent = find_host_entry(make_host_key(downstream)); + ++ent.num_active; + + downstream->set_dispatch_state(DispatchState::ACTIVE); +} + +void DownstreamQueue::mark_blocked(Downstream *downstream) { + auto &ent = find_host_entry(make_host_key(downstream)); + + downstream->set_dispatch_state(DispatchState::BLOCKED); + + auto link = new BlockedLink{}; + downstream->attach_blocked_link(link); + ent.blocked.append(link); +} + +bool DownstreamQueue::can_activate(const StringRef &host) const { + auto itr = host_entries_.find(make_host_key(host)); + if (itr == std::end(host_entries_)) { + return true; + } + auto &ent = (*itr).second; + return ent.num_active < conn_max_per_host_; +} + +namespace { +bool remove_host_entry_if_empty(const DownstreamQueue::HostEntry &ent, + DownstreamQueue::HostEntryMap &host_entries, + const StringRef &host) { + if (ent.blocked.empty() && ent.num_active == 0) { + host_entries.erase(host); + return true; + } + return false; +} +} // namespace + +Downstream *DownstreamQueue::remove_and_get_blocked(Downstream *downstream, + bool next_blocked) { + // Delete downstream when this function returns. + auto delptr = std::unique_ptr<Downstream>(downstream); + + downstreams_.remove(downstream); + + auto host = make_host_key(downstream); + auto &ent = find_host_entry(host); + + if (downstream->get_dispatch_state() == DispatchState::ACTIVE) { + --ent.num_active; + } else { + // For those downstreams deleted while in blocked state + auto link = downstream->detach_blocked_link(); + if (link) { + ent.blocked.remove(link); + delete link; + } + } + + if (remove_host_entry_if_empty(ent, host_entries_, host)) { + return nullptr; + } + + if (!next_blocked || ent.num_active >= conn_max_per_host_) { + return nullptr; + } + + auto link = ent.blocked.head; + + if (!link) { + return nullptr; + } + + auto next_downstream = link->downstream; + auto link2 = next_downstream->detach_blocked_link(); + // This is required with --disable-assert. + (void)link2; + assert(link2 == link); + ent.blocked.remove(link); + delete link; + remove_host_entry_if_empty(ent, host_entries_, host); + + return next_downstream; +} + +Downstream *DownstreamQueue::get_downstreams() const { + return downstreams_.head; +} + +} // namespace shrpx diff --git a/src/shrpx_downstream_queue.h b/src/shrpx_downstream_queue.h new file mode 100644 index 0000000..a5b980f --- /dev/null +++ b/src/shrpx_downstream_queue.h @@ -0,0 +1,116 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DOWNSTREAM_QUEUE_H +#define SHRPX_DOWNSTREAM_QUEUE_H + +#include "shrpx.h" + +#include <cinttypes> +#include <map> +#include <set> +#include <memory> + +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +class Downstream; + +// Link entry in HostEntry.blocked and downstream because downstream +// could be deleted in anytime and we'd like to find Downstream in +// O(1). Downstream has field to link back to this object. +struct BlockedLink { + Downstream *downstream; + BlockedLink *dlnext, *dlprev; +}; + +class DownstreamQueue { +public: + struct HostEntry { + HostEntry(ImmutableString &&key); + + HostEntry(HostEntry &&) = default; + HostEntry &operator=(HostEntry &&) = default; + + HostEntry(const HostEntry &) = delete; + HostEntry &operator=(const HostEntry &) = delete; + + // Key that associates this object + ImmutableString key; + // Set of stream ID that blocked by conn_max_per_host_. + DList<BlockedLink> blocked; + // The number of connections currently made to this host. + size_t num_active; + }; + + using HostEntryMap = std::map<StringRef, HostEntry>; + + // conn_max_per_host == 0 means no limit for downstream connection. + DownstreamQueue(size_t conn_max_per_host = 0, bool unified_host = true); + ~DownstreamQueue(); + // Add |downstream| to this queue. This is entry point for + // Downstream object. + void add_pending(std::unique_ptr<Downstream> downstream); + // Set |downstream| to failure state, which means that downstream + // failed to connect to backend. + void mark_failure(Downstream *downstream); + // Set |downstream| to active state, which means that downstream + // connection has started. + void mark_active(Downstream *downstream); + // Set |downstream| to blocked state, which means that download + // connection was blocked because conn_max_per_host_ limit. + void mark_blocked(Downstream *downstream); + // Returns true if we can make downstream connection to given + // |host|. + bool can_activate(const StringRef &host) const; + // Removes and frees |downstream| object. If |downstream| is in + // DispatchState::ACTIVE, and |next_blocked| is true, this function + // may return Downstream object with the same target host in + // DispatchState::BLOCKED if its connection is now not blocked by + // conn_max_per_host_ limit. + Downstream *remove_and_get_blocked(Downstream *downstream, + bool next_blocked = true); + Downstream *get_downstreams() const; + HostEntry &find_host_entry(const StringRef &host); + StringRef make_host_key(const StringRef &host) const; + StringRef make_host_key(Downstream *downstream) const; + +private: + // Per target host structure to keep track of the number of + // connections to the same host. + HostEntryMap host_entries_; + DList<Downstream> downstreams_; + // Maximum number of concurrent connections to the same host. + size_t conn_max_per_host_; + // true if downstream host is treated as the same. Used for reverse + // proxying. + bool unified_host_; +}; + +} // namespace shrpx + +#endif // SHRPX_DOWNSTREAM_QUEUE_H diff --git a/src/shrpx_downstream_test.cc b/src/shrpx_downstream_test.cc new file mode 100644 index 0000000..6100b18 --- /dev/null +++ b/src/shrpx_downstream_test.cc @@ -0,0 +1,231 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_downstream_test.h" + +#include <iostream> + +#include <CUnit/CUnit.h> + +#include "shrpx_downstream.h" + +namespace shrpx { + +void test_downstream_field_store_append_last_header(void) { + BlockAllocator balloc(16, 16); + FieldStore fs(balloc, 0); + fs.alloc_add_header_name(StringRef::from_lit("alpha")); + auto bravo = StringRef::from_lit("BRAVO"); + fs.append_last_header_key(bravo.c_str(), bravo.size()); + // Add more characters so that relloc occurs + auto golf = StringRef::from_lit("golF0123456789"); + fs.append_last_header_key(golf.c_str(), golf.size()); + + auto charlie = StringRef::from_lit("Charlie"); + fs.append_last_header_value(charlie.c_str(), charlie.size()); + auto delta = StringRef::from_lit("deltA"); + fs.append_last_header_value(delta.c_str(), delta.size()); + // Add more characters so that relloc occurs + auto echo = StringRef::from_lit("echo0123456789"); + fs.append_last_header_value(echo.c_str(), echo.size()); + + fs.add_header_token(StringRef::from_lit("echo"), + StringRef::from_lit("foxtrot"), false, -1); + + auto ans = + HeaderRefs{{StringRef::from_lit("alphabravogolf0123456789"), + StringRef::from_lit("CharliedeltAecho0123456789")}, + {StringRef::from_lit("echo"), StringRef::from_lit("foxtrot")}}; + CU_ASSERT(ans == fs.headers()); +} + +void test_downstream_field_store_header(void) { + BlockAllocator balloc(16, 16); + FieldStore fs(balloc, 0); + fs.add_header_token(StringRef::from_lit("alpha"), StringRef::from_lit("0"), + false, -1); + fs.add_header_token(StringRef::from_lit(":authority"), + StringRef::from_lit("1"), false, http2::HD__AUTHORITY); + fs.add_header_token(StringRef::from_lit("content-length"), + StringRef::from_lit("2"), false, + http2::HD_CONTENT_LENGTH); + + // By token + CU_ASSERT(HeaderRef(StringRef{":authority"}, StringRef{"1"}) == + *fs.header(http2::HD__AUTHORITY)); + CU_ASSERT(nullptr == fs.header(http2::HD__METHOD)); + + // By name + CU_ASSERT(HeaderRef(StringRef{"alpha"}, StringRef{"0"}) == + *fs.header(StringRef::from_lit("alpha"))); + CU_ASSERT(nullptr == fs.header(StringRef::from_lit("bravo"))); +} + +void test_downstream_crumble_request_cookie(void) { + Downstream d(nullptr, nullptr, 0); + auto &req = d.request(); + req.fs.add_header_token(StringRef::from_lit(":method"), + StringRef::from_lit("get"), false, -1); + req.fs.add_header_token(StringRef::from_lit(":path"), + StringRef::from_lit("/"), false, -1); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("alpha; bravo; ; ;; charlie;;"), + true, http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit(";delta"), false, + http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("echo"), false, http2::HD_COOKIE); + + std::vector<nghttp2_nv> nva; + d.crumble_request_cookie(nva); + + auto num_cookies = d.count_crumble_request_cookie(); + + CU_ASSERT(5 == nva.size()); + CU_ASSERT(5 == num_cookies); + + HeaderRefs cookies; + std::transform(std::begin(nva), std::end(nva), std::back_inserter(cookies), + [](const nghttp2_nv &nv) { + return HeaderRef(StringRef{nv.name, nv.namelen}, + StringRef{nv.value, nv.valuelen}, + nv.flags & NGHTTP2_NV_FLAG_NO_INDEX); + }); + + HeaderRefs ans = { + {StringRef::from_lit("cookie"), StringRef::from_lit("alpha")}, + {StringRef::from_lit("cookie"), StringRef::from_lit("bravo")}, + {StringRef::from_lit("cookie"), StringRef::from_lit("charlie")}, + {StringRef::from_lit("cookie"), StringRef::from_lit("delta")}, + {StringRef::from_lit("cookie"), StringRef::from_lit("echo")}}; + + CU_ASSERT(ans == cookies); + CU_ASSERT(cookies[0].no_index); + CU_ASSERT(cookies[1].no_index); + CU_ASSERT(cookies[2].no_index); +} + +void test_downstream_assemble_request_cookie(void) { + Downstream d(nullptr, nullptr, 0); + auto &req = d.request(); + + req.fs.add_header_token(StringRef::from_lit(":method"), + StringRef::from_lit("get"), false, -1); + req.fs.add_header_token(StringRef::from_lit(":path"), + StringRef::from_lit("/"), false, -1); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("alpha"), false, + http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("bravo;"), false, + http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("charlie; "), false, + http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("delta;;"), false, + http2::HD_COOKIE); + CU_ASSERT("alpha; bravo; charlie; delta" == d.assemble_request_cookie()); +} + +void test_downstream_rewrite_location_response_header(void) { + Downstream d(nullptr, nullptr, 0); + auto &req = d.request(); + auto &resp = d.response(); + d.set_request_downstream_host(StringRef::from_lit("localhost2")); + req.authority = StringRef::from_lit("localhost:8443"); + resp.fs.add_header_token(StringRef::from_lit("location"), + StringRef::from_lit("http://localhost2:3000/"), + false, http2::HD_LOCATION); + d.rewrite_location_response_header(StringRef::from_lit("https")); + auto location = resp.fs.header(http2::HD_LOCATION); + CU_ASSERT("https://localhost:8443/" == (*location).value); +} + +void test_downstream_supports_non_final_response(void) { + Downstream d(nullptr, nullptr, 0); + auto &req = d.request(); + + req.http_major = 3; + req.http_minor = 0; + + CU_ASSERT(d.supports_non_final_response()); + + req.http_major = 2; + req.http_minor = 0; + + CU_ASSERT(d.supports_non_final_response()); + + req.http_major = 1; + req.http_minor = 1; + + CU_ASSERT(d.supports_non_final_response()); + + req.http_major = 1; + req.http_minor = 0; + + CU_ASSERT(!d.supports_non_final_response()); + + req.http_major = 0; + req.http_minor = 9; + + CU_ASSERT(!d.supports_non_final_response()); +} + +void test_downstream_find_affinity_cookie(void) { + Downstream d(nullptr, nullptr, 0); + + auto &req = d.request(); + req.fs.add_header_token(StringRef::from_lit("cookie"), StringRef{}, false, + http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("a=b;;c=d"), false, + http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("content-length"), + StringRef::from_lit("599"), false, + http2::HD_CONTENT_LENGTH); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("lb=deadbeef;LB=f1f2f3f4"), false, + http2::HD_COOKIE); + req.fs.add_header_token(StringRef::from_lit("cookie"), + StringRef::from_lit("short=e1e2e3e"), false, + http2::HD_COOKIE); + + uint32_t aff; + + aff = d.find_affinity_cookie(StringRef::from_lit("lb")); + + CU_ASSERT(0xdeadbeef == aff); + + aff = d.find_affinity_cookie(StringRef::from_lit("LB")); + + CU_ASSERT(0xf1f2f3f4 == aff); + + aff = d.find_affinity_cookie(StringRef::from_lit("short")); + + CU_ASSERT(0 == aff); +} + +} // namespace shrpx diff --git a/src/shrpx_downstream_test.h b/src/shrpx_downstream_test.h new file mode 100644 index 0000000..ef06ea3 --- /dev/null +++ b/src/shrpx_downstream_test.h @@ -0,0 +1,44 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DOWNSTREAM_TEST_H +#define SHRPX_DOWNSTREAM_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_downstream_field_store_append_last_header(void); +void test_downstream_field_store_header(void); +void test_downstream_crumble_request_cookie(void); +void test_downstream_assemble_request_cookie(void); +void test_downstream_rewrite_location_response_header(void); +void test_downstream_supports_non_final_response(void); +void test_downstream_find_affinity_cookie(void); + +} // namespace shrpx + +#endif // SHRPX_DOWNSTREAM_TEST_H diff --git a/src/shrpx_dual_dns_resolver.cc b/src/shrpx_dual_dns_resolver.cc new file mode 100644 index 0000000..8c6c5c9 --- /dev/null +++ b/src/shrpx_dual_dns_resolver.cc @@ -0,0 +1,93 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_dual_dns_resolver.h" + +namespace shrpx { + +DualDNSResolver::DualDNSResolver(struct ev_loop *loop, int family) + : family_(family), resolv4_(loop), resolv6_(loop) { + auto cb = [this](DNSResolverStatus, const Address *) { + Address result; + + auto status = this->get_status(&result); + switch (status) { + case DNSResolverStatus::ERROR: + case DNSResolverStatus::OK: + break; + default: + return; + } + + auto cb = this->get_complete_cb(); + cb(status, &result); + }; + + if (family_ == AF_UNSPEC || family_ == AF_INET) { + resolv4_.set_complete_cb(cb); + } + if (family_ == AF_UNSPEC || family_ == AF_INET6) { + resolv6_.set_complete_cb(cb); + } +} + +int DualDNSResolver::resolve(const StringRef &host) { + int rv4 = 0, rv6 = 0; + if (family_ == AF_UNSPEC || family_ == AF_INET) { + rv4 = resolv4_.resolve(host, AF_INET); + } + if (family_ == AF_UNSPEC || family_ == AF_INET6) { + rv6 = resolv6_.resolve(host, AF_INET6); + } + + if (rv4 != 0 && rv6 != 0) { + return -1; + } + + return 0; +} + +CompleteCb DualDNSResolver::get_complete_cb() const { return complete_cb_; } + +void DualDNSResolver::set_complete_cb(CompleteCb cb) { complete_cb_ = cb; } + +DNSResolverStatus DualDNSResolver::get_status(Address *result) const { + auto rv6 = resolv6_.get_status(result); + if (rv6 == DNSResolverStatus::OK) { + return DNSResolverStatus::OK; + } + auto rv4 = resolv4_.get_status(result); + if (rv4 == DNSResolverStatus::OK) { + return DNSResolverStatus::OK; + } + if (rv4 == DNSResolverStatus::RUNNING || rv6 == DNSResolverStatus::RUNNING) { + return DNSResolverStatus::RUNNING; + } + if (rv4 == DNSResolverStatus::ERROR || rv6 == DNSResolverStatus::ERROR) { + return DNSResolverStatus::ERROR; + } + return DNSResolverStatus::IDLE; +} + +} // namespace shrpx diff --git a/src/shrpx_dual_dns_resolver.h b/src/shrpx_dual_dns_resolver.h new file mode 100644 index 0000000..98065da --- /dev/null +++ b/src/shrpx_dual_dns_resolver.h @@ -0,0 +1,69 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_DUAL_DNS_RESOLVER_H +#define SHRPX_DUAL_DNS_RESOLVER_H + +#include "shrpx.h" + +#include <ev.h> + +#include "shrpx_dns_resolver.h" + +using namespace nghttp2; + +namespace shrpx { + +// DualDNSResolver performs name resolution for both A and AAAA +// records at the same time. The first successful return (or if we +// have both successful results, prefer to AAAA) is chosen. This is +// wrapper around 2 DNSResolver inside. resolve(), get_status(), and +// how CompleteCb is called have the same semantics with DNSResolver. +class DualDNSResolver { +public: + // |family| controls IP version preference. If |family| == + // AF_UNSPEC, bot A and AAAA lookups are performed. If |family| == + // AF_INET, only A lookup is performed. If |family| == AF_INET6, + // only AAAA lookup is performed. + DualDNSResolver(struct ev_loop *loop, int family); + + // Resolves |host|. |host| must be NULL-terminated string. + int resolve(const StringRef &host); + CompleteCb get_complete_cb() const; + void set_complete_cb(CompleteCb cb); + DNSResolverStatus get_status(Address *result) const; + +private: + // IP version preference. + int family_; + // For A record + DNSResolver resolv4_; + // For AAAA record + DNSResolver resolv6_; + CompleteCb complete_cb_; +}; + +} // namespace shrpx + +#endif // SHRPX_DUAL_DNS_RESOLVER_H diff --git a/src/shrpx_error.h b/src/shrpx_error.h new file mode 100644 index 0000000..c89611f --- /dev/null +++ b/src/shrpx_error.h @@ -0,0 +1,47 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_ERROR_H +#define SHRPX_ERROR_H + +#include "shrpx.h" + +namespace shrpx { + +// Deprecated, do not use. +enum ErrorCode { + SHRPX_ERR_SUCCESS = 0, + SHRPX_ERR_ERROR = -1, + SHRPX_ERR_NETWORK = -100, + SHRPX_ERR_EOF = -101, + SHRPX_ERR_INPROGRESS = -102, + SHRPX_ERR_DCONN_CANCELED = -103, + SHRPX_ERR_RETRY = -104, + SHRPX_ERR_TLS_REQUIRED = -105, + SHRPX_ERR_SEND_BLOCKED = -106, +}; + +} // namespace shrpx + +#endif // SHRPX_ERROR_H diff --git a/src/shrpx_exec.cc b/src/shrpx_exec.cc new file mode 100644 index 0000000..d5cd091 --- /dev/null +++ b/src/shrpx_exec.cc @@ -0,0 +1,138 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_exec.h" + +#include <cerrno> + +#include "shrpx_signal.h" +#include "shrpx_log.h" +#include "util.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +// inspired by h2o_read_command function from h2o project: +// https://github.com/h2o/h2o +int exec_read_command(Process &proc, char *const argv[]) { + int rv; + int pfd[2]; + +#ifdef O_CLOEXEC + if (pipe2(pfd, O_CLOEXEC) == -1) { + return -1; + } +#else // !O_CLOEXEC + if (pipe(pfd) == -1) { + return -1; + } + util::make_socket_closeonexec(pfd[0]); + util::make_socket_closeonexec(pfd[1]); +#endif // !O_CLOEXEC + + auto closer = defer([&pfd]() { + if (pfd[0] != -1) { + close(pfd[0]); + } + + if (pfd[1] != -1) { + close(pfd[1]); + } + }); + + sigset_t oldset; + + rv = shrpx_signal_block_all(&oldset); + if (rv != 0) { + auto error = errno; + LOG(ERROR) << "Blocking all signals failed: errno=" << error; + + return -1; + } + + auto pid = fork(); + + if (pid == 0) { + // This is multithreaded program, and we are allowed to use only + // async-signal-safe functions here. + + // child process + shrpx_signal_unset_worker_proc_ign_handler(); + + rv = shrpx_signal_unblock_all(); + if (rv != 0) { + static constexpr char msg[] = "Unblocking all signals failed\n"; + while (write(STDERR_FILENO, msg, str_size(msg)) == -1 && errno == EINTR) + ; + nghttp2_Exit(EXIT_FAILURE); + } + + dup2(pfd[1], 1); + close(pfd[0]); + + rv = execv(argv[0], argv); + if (rv == -1) { + static constexpr char msg[] = "Could not execute command\n"; + while (write(STDERR_FILENO, msg, str_size(msg)) == -1 && errno == EINTR) + ; + nghttp2_Exit(EXIT_FAILURE); + } + // unreachable + } + + // parent process + if (pid == -1) { + auto error = errno; + LOG(ERROR) << "Could not execute command: " << argv[0] + << ", fork() failed, errno=" << error; + } + + rv = shrpx_signal_set(&oldset); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Restoring all signals failed: errno=" << error; + + nghttp2_Exit(EXIT_FAILURE); + } + + if (pid == -1) { + return -1; + } + + close(pfd[1]); + pfd[1] = -1; + + util::make_socket_nonblocking(pfd[0]); + + proc.pid = pid; + proc.rfd = pfd[0]; + + pfd[0] = -1; + + return 0; +} + +} // namespace shrpx diff --git a/src/shrpx_exec.h b/src/shrpx_exec.h new file mode 100644 index 0000000..2d8a770 --- /dev/null +++ b/src/shrpx_exec.h @@ -0,0 +1,47 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_EXEC_H +#define SHRPX_EXEC_H + +#include "unistd.h" + +namespace shrpx { + +struct Process { + pid_t pid; + // fd to read from process + int rfd; +}; + +// Executes command |argv| after forking current process. The command +// should not expect to read from stdin. Parent process can read the +// stdout from command using proc.rfd. On success, this function +// returns 0, and process information is stored in |proc|. Otherwise, +// returns -1. +int exec_read_command(Process &proc, char *const argv[]); + +} // namespace shrpx + +#endif // SHRPX_EXEC_H diff --git a/src/shrpx_health_monitor_downstream_connection.cc b/src/shrpx_health_monitor_downstream_connection.cc new file mode 100644 index 0000000..89e5396 --- /dev/null +++ b/src/shrpx_health_monitor_downstream_connection.cc @@ -0,0 +1,116 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_health_monitor_downstream_connection.h" + +#include "shrpx_client_handler.h" +#include "shrpx_upstream.h" +#include "shrpx_downstream.h" +#include "shrpx_log.h" + +namespace shrpx { + +HealthMonitorDownstreamConnection::HealthMonitorDownstreamConnection() {} + +HealthMonitorDownstreamConnection::~HealthMonitorDownstreamConnection() {} + +int HealthMonitorDownstreamConnection::attach_downstream( + Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Attaching to DOWNSTREAM:" << downstream; + } + + downstream_ = downstream; + + return 0; +} + +void HealthMonitorDownstreamConnection::detach_downstream( + Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Detaching from DOWNSTREAM:" << downstream; + } + downstream_ = nullptr; +} + +int HealthMonitorDownstreamConnection::push_request_headers() { + downstream_->set_request_header_sent(true); + auto src = downstream_->get_blocked_request_buf(); + auto dest = downstream_->get_request_buf(); + src->remove(*dest); + + return 0; +} + +int HealthMonitorDownstreamConnection::push_upload_data_chunk( + const uint8_t *data, size_t datalen) { + return 0; +} + +int HealthMonitorDownstreamConnection::end_upload_data() { + auto upstream = downstream_->get_upstream(); + auto &resp = downstream_->response(); + + resp.http_status = 200; + + resp.fs.add_header_token(StringRef::from_lit("content-length"), + StringRef::from_lit("0"), false, + http2::HD_CONTENT_LENGTH); + + if (upstream->send_reply(downstream_, nullptr, 0) != 0) { + return -1; + } + + return 0; +} + +void HealthMonitorDownstreamConnection::pause_read(IOCtrlReason reason) {} + +int HealthMonitorDownstreamConnection::resume_read(IOCtrlReason reason, + size_t consumed) { + return 0; +} + +void HealthMonitorDownstreamConnection::force_resume_read() {} + +int HealthMonitorDownstreamConnection::on_read() { return 0; } + +int HealthMonitorDownstreamConnection::on_write() { return 0; } + +void HealthMonitorDownstreamConnection::on_upstream_change(Upstream *upstream) { +} + +bool HealthMonitorDownstreamConnection::poolable() const { return false; } + +const std::shared_ptr<DownstreamAddrGroup> & +HealthMonitorDownstreamConnection::get_downstream_addr_group() const { + static std::shared_ptr<DownstreamAddrGroup> s; + return s; +} + +DownstreamAddr *HealthMonitorDownstreamConnection::get_addr() const { + return nullptr; +} + +} // namespace shrpx diff --git a/src/shrpx_health_monitor_downstream_connection.h b/src/shrpx_health_monitor_downstream_connection.h new file mode 100644 index 0000000..c0eb633 --- /dev/null +++ b/src/shrpx_health_monitor_downstream_connection.h @@ -0,0 +1,64 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HEALTH_MONITOR_DOWNSTREAM_CONNECTION_H +#define SHRPX_HEALTH_MONITOR_DOWNSTREAM_CONNECTION_H + +#include "shrpx_downstream_connection.h" + +namespace shrpx { + +class Worker; + +class HealthMonitorDownstreamConnection : public DownstreamConnection { +public: + HealthMonitorDownstreamConnection(); + virtual ~HealthMonitorDownstreamConnection(); + virtual int attach_downstream(Downstream *downstream); + virtual void detach_downstream(Downstream *downstream); + + virtual int push_request_headers(); + virtual int push_upload_data_chunk(const uint8_t *data, size_t datalen); + virtual int end_upload_data(); + + virtual void pause_read(IOCtrlReason reason); + virtual int resume_read(IOCtrlReason reason, size_t consumed); + virtual void force_resume_read(); + + virtual int on_read(); + virtual int on_write(); + + virtual void on_upstream_change(Upstream *upstream); + + // true if this object is poolable. + virtual bool poolable() const; + + virtual const std::shared_ptr<DownstreamAddrGroup> & + get_downstream_addr_group() const; + virtual DownstreamAddr *get_addr() const; +}; + +} // namespace shrpx + +#endif // SHRPX_HEALTH_MONITOR_DOWNSTREAM_CONNECTION_H diff --git a/src/shrpx_http.cc b/src/shrpx_http.cc new file mode 100644 index 0000000..ad32dc9 --- /dev/null +++ b/src/shrpx_http.cc @@ -0,0 +1,280 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_http.h" + +#include "shrpx_config.h" +#include "shrpx_log.h" +#include "http2.h" +#include "util.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace http { + +StringRef create_error_html(BlockAllocator &balloc, unsigned int http_status) { + auto &httpconf = get_config()->http; + + const auto &error_pages = httpconf.error_pages; + for (const auto &page : error_pages) { + if (page.http_status == 0 || page.http_status == http_status) { + return StringRef{std::begin(page.content), std::end(page.content)}; + } + } + + auto status_string = http2::stringify_status(balloc, http_status); + auto reason_phrase = http2::get_reason_phrase(http_status); + + return concat_string_ref( + balloc, StringRef::from_lit(R"(<!DOCTYPE html><html lang="en"><title>)"), + status_string, StringRef::from_lit(" "), reason_phrase, + StringRef::from_lit("</title><body><h1>"), status_string, + StringRef::from_lit(" "), reason_phrase, + StringRef::from_lit("</h1><footer>"), httpconf.server_name, + StringRef::from_lit("</footer></body></html>")); +} + +StringRef create_forwarded(BlockAllocator &balloc, int params, + const StringRef &node_by, const StringRef &node_for, + const StringRef &host, const StringRef &proto) { + size_t len = 0; + if ((params & FORWARDED_BY) && !node_by.empty()) { + len += str_size("by=\"") + node_by.size() + str_size("\";"); + } + if ((params & FORWARDED_FOR) && !node_for.empty()) { + len += str_size("for=\"") + node_for.size() + str_size("\";"); + } + if ((params & FORWARDED_HOST) && !host.empty()) { + len += str_size("host=\"") + host.size() + str_size("\";"); + } + if ((params & FORWARDED_PROTO) && !proto.empty()) { + len += str_size("proto=") + proto.size() + str_size(";"); + } + + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + + if ((params & FORWARDED_BY) && !node_by.empty()) { + // This must be quoted-string unless it is obfuscated version + // (which starts with "_") or some special value (e.g., + // "localhost" for UNIX domain socket), since ':' is not allowed + // in token. ':' is used to separate host and port. + if (node_by[0] == '_' || node_by[0] == 'l') { + p = util::copy_lit(p, "by="); + p = std::copy(std::begin(node_by), std::end(node_by), p); + p = util::copy_lit(p, ";"); + } else { + p = util::copy_lit(p, "by=\""); + p = std::copy(std::begin(node_by), std::end(node_by), p); + p = util::copy_lit(p, "\";"); + } + } + if ((params & FORWARDED_FOR) && !node_for.empty()) { + // We only quote IPv6 literal address only, which starts with '['. + if (node_for[0] == '[') { + p = util::copy_lit(p, "for=\""); + p = std::copy(std::begin(node_for), std::end(node_for), p); + p = util::copy_lit(p, "\";"); + } else { + p = util::copy_lit(p, "for="); + p = std::copy(std::begin(node_for), std::end(node_for), p); + p = util::copy_lit(p, ";"); + } + } + if ((params & FORWARDED_HOST) && !host.empty()) { + // Just be quoted to skip checking characters. + p = util::copy_lit(p, "host=\""); + p = std::copy(std::begin(host), std::end(host), p); + p = util::copy_lit(p, "\";"); + } + if ((params & FORWARDED_PROTO) && !proto.empty()) { + // Scheme production rule only allow characters which are all in + // token. + p = util::copy_lit(p, "proto="); + p = std::copy(std::begin(proto), std::end(proto), p); + *p++ = ';'; + } + + if (iov.base == p) { + return StringRef{}; + } + + --p; + *p = '\0'; + + return StringRef{iov.base, p}; +} + +std::string colorizeHeaders(const char *hdrs) { + std::string nhdrs; + const char *p = strchr(hdrs, '\n'); + if (!p) { + // Not valid HTTP header + return hdrs; + } + nhdrs.append(hdrs, p + 1); + ++p; + while (1) { + const char *np = strchr(p, ':'); + if (!np) { + nhdrs.append(p); + break; + } + nhdrs += TTY_HTTP_HD; + nhdrs.append(p, np); + nhdrs += TTY_RST; + auto redact = util::strieq_l("authorization", StringRef{p, np}); + p = np; + np = strchr(p, '\n'); + if (!np) { + if (redact) { + nhdrs.append(": <redacted>"); + } else { + nhdrs.append(p); + } + break; + } + if (redact) { + nhdrs.append(": <redacted>\n"); + } else { + nhdrs.append(p, np + 1); + } + p = np + 1; + } + return nhdrs; +} + +ssize_t select_padding_callback(nghttp2_session *session, + const nghttp2_frame *frame, size_t max_payload, + void *user_data) { + return std::min(max_payload, frame->hd.length + get_config()->padding); +} + +StringRef create_affinity_cookie(BlockAllocator &balloc, const StringRef &name, + uint32_t affinity_cookie, + const StringRef &path, bool secure) { + static constexpr auto PATH_PREFIX = StringRef::from_lit("; Path="); + static constexpr auto SECURE = StringRef::from_lit("; Secure"); + // <name>=<value>[; Path=<path>][; Secure] + size_t len = name.size() + 1 + 8; + + if (!path.empty()) { + len += PATH_PREFIX.size() + path.size(); + } + if (secure) { + len += SECURE.size(); + } + + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + p = std::copy(std::begin(name), std::end(name), p); + *p++ = '='; + affinity_cookie = htonl(affinity_cookie); + p = util::format_hex(p, + StringRef{reinterpret_cast<uint8_t *>(&affinity_cookie), + reinterpret_cast<uint8_t *>(&affinity_cookie) + + sizeof(affinity_cookie)}); + if (!path.empty()) { + p = std::copy(std::begin(PATH_PREFIX), std::end(PATH_PREFIX), p); + p = std::copy(std::begin(path), std::end(path), p); + } + if (secure) { + p = std::copy(std::begin(SECURE), std::end(SECURE), p); + } + *p = '\0'; + return StringRef{iov.base, p}; +} + +bool require_cookie_secure_attribute(SessionAffinityCookieSecure secure, + const StringRef &scheme) { + switch (secure) { + case SessionAffinityCookieSecure::AUTO: + return scheme == "https"; + case SessionAffinityCookieSecure::YES: + return true; + default: + return false; + } +} + +StringRef create_altsvc_header_value(BlockAllocator &balloc, + const std::vector<AltSvc> &altsvcs) { + // <PROTOID>="<HOST>:<SERVICE>"; <PARAMS> + size_t len = 0; + + if (altsvcs.empty()) { + return StringRef{}; + } + + for (auto &altsvc : altsvcs) { + len += util::percent_encode_tokenlen(altsvc.protocol_id); + len += str_size("=\""); + len += util::quote_stringlen(altsvc.host); + len += str_size(":"); + len += altsvc.service.size(); + len += str_size("\""); + if (!altsvc.params.empty()) { + len += str_size("; "); + len += altsvc.params.size(); + } + } + + // ", " between items. + len += (altsvcs.size() - 1) * 2; + + // We will write additional ", " at the end, and cut it later. + auto iov = make_byte_ref(balloc, len + 2); + auto p = iov.base; + + for (auto &altsvc : altsvcs) { + p = util::percent_encode_token(p, altsvc.protocol_id); + p = util::copy_lit(p, "=\""); + p = util::quote_string(p, altsvc.host); + *p++ = ':'; + p = std::copy(std::begin(altsvc.service), std::end(altsvc.service), p); + *p++ = '"'; + if (!altsvc.params.empty()) { + p = util::copy_lit(p, "; "); + p = std::copy(std::begin(altsvc.params), std::end(altsvc.params), p); + } + p = util::copy_lit(p, ", "); + } + + p -= 2; + *p = '\0'; + + assert(static_cast<size_t>(p - iov.base) == len); + + return StringRef{iov.base, p}; +} + +bool check_http_scheme(const StringRef &scheme, bool encrypted) { + return encrypted ? scheme == "https" : scheme == "http"; +} + +} // namespace http + +} // namespace shrpx diff --git a/src/shrpx_http.h b/src/shrpx_http.h new file mode 100644 index 0000000..18935d8 --- /dev/null +++ b/src/shrpx_http.h @@ -0,0 +1,96 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP_H +#define SHRPX_HTTP_H + +#include "shrpx.h" + +#include <string> + +#include <nghttp2/nghttp2.h> + +#include "shrpx_config.h" +#include "util.h" +#include "allocator.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace http { + +StringRef create_error_html(BlockAllocator &balloc, unsigned int status_code); + +template <typename OutputIt> +OutputIt create_via_header_value(OutputIt dst, int major, int minor) { + *dst++ = static_cast<char>(major + '0'); + if (major < 2) { + *dst++ = '.'; + *dst++ = static_cast<char>(minor + '0'); + } + return util::copy_lit(dst, " nghttpx"); +} + +// Returns generated RFC 7239 Forwarded header field value. The +// |params| is bitwise-OR of zero or more of shrpx_forwarded_param +// defined in shrpx_config.h. +StringRef create_forwarded(BlockAllocator &balloc, int params, + const StringRef &node_by, const StringRef &node_for, + const StringRef &host, const StringRef &proto); + +// Adds ANSI color codes to HTTP headers |hdrs|. +std::string colorizeHeaders(const char *hdrs); + +ssize_t select_padding_callback(nghttp2_session *session, + const nghttp2_frame *frame, size_t max_payload, + void *user_data); + +// Creates set-cookie-string for cookie based affinity. If |path| is +// not empty, "; <path>" is added. If |secure| is true, "; Secure" is +// added. +StringRef create_affinity_cookie(BlockAllocator &balloc, const StringRef &name, + uint32_t affinity_cookie, + const StringRef &path, bool secure); + +// Returns true if |secure| indicates that Secure attribute should be +// set. +bool require_cookie_secure_attribute(SessionAffinityCookieSecure secure, + const StringRef &scheme); + +// Returns RFC 7838 alt-svc header field value. +StringRef create_altsvc_header_value(BlockAllocator &balloc, + const std::vector<AltSvc> &altsvcs); + +// Returns true if either of the following conditions holds: +// - scheme is https and encrypted is true +// - scheme is http and encrypted is false +// Otherwise returns false. +bool check_http_scheme(const StringRef &scheme, bool encrypted); + +} // namespace http + +} // namespace shrpx + +#endif // SHRPX_HTTP_H diff --git a/src/shrpx_http2_downstream_connection.cc b/src/shrpx_http2_downstream_connection.cc new file mode 100644 index 0000000..ee48799 --- /dev/null +++ b/src/shrpx_http2_downstream_connection.cc @@ -0,0 +1,621 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_http2_downstream_connection.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H + +#include "llhttp.h" + +#include "shrpx_client_handler.h" +#include "shrpx_upstream.h" +#include "shrpx_downstream.h" +#include "shrpx_config.h" +#include "shrpx_error.h" +#include "shrpx_http.h" +#include "shrpx_http2_session.h" +#include "shrpx_worker.h" +#include "shrpx_log.h" +#include "http2.h" +#include "util.h" +#include "ssl_compat.h" + +using namespace nghttp2; + +namespace shrpx { + +Http2DownstreamConnection::Http2DownstreamConnection(Http2Session *http2session) + : dlnext(nullptr), + dlprev(nullptr), + http2session_(http2session), + sd_(nullptr) {} + +Http2DownstreamConnection::~Http2DownstreamConnection() { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Deleting"; + } + if (downstream_) { + downstream_->disable_downstream_rtimer(); + downstream_->disable_downstream_wtimer(); + + uint32_t error_code; + if (downstream_->get_request_state() == DownstreamState::STREAM_CLOSED && + downstream_->get_upgraded()) { + // For upgraded connection, send NO_ERROR. Should we consider + // request states other than DownstreamState::STREAM_CLOSED ? + error_code = NGHTTP2_NO_ERROR; + } else { + error_code = NGHTTP2_INTERNAL_ERROR; + } + + if (http2session_->get_state() == Http2SessionState::CONNECTED && + downstream_->get_downstream_stream_id() != -1) { + submit_rst_stream(downstream_, error_code); + + auto &resp = downstream_->response(); + + http2session_->consume(downstream_->get_downstream_stream_id(), + resp.unconsumed_body_length); + + resp.unconsumed_body_length = 0; + + http2session_->signal_write(); + } + } + http2session_->remove_downstream_connection(this); + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Deleted"; + } +} + +int Http2DownstreamConnection::attach_downstream(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Attaching to DOWNSTREAM:" << downstream; + } + http2session_->add_downstream_connection(this); + http2session_->signal_write(); + + downstream_ = downstream; + downstream_->reset_downstream_rtimer(); + + auto &req = downstream_->request(); + + // HTTP/2 disables HTTP Upgrade. + if (req.method != HTTP_CONNECT && req.connect_proto == ConnectProto::NONE) { + req.upgrade_request = false; + } + + return 0; +} + +void Http2DownstreamConnection::detach_downstream(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Detaching from DOWNSTREAM:" << downstream; + } + + auto &resp = downstream_->response(); + + if (downstream_->get_downstream_stream_id() != -1) { + if (submit_rst_stream(downstream) == 0) { + http2session_->signal_write(); + } + + http2session_->consume(downstream_->get_downstream_stream_id(), + resp.unconsumed_body_length); + + resp.unconsumed_body_length = 0; + + http2session_->signal_write(); + } + + downstream->disable_downstream_rtimer(); + downstream->disable_downstream_wtimer(); + downstream_ = nullptr; +} + +int Http2DownstreamConnection::submit_rst_stream(Downstream *downstream, + uint32_t error_code) { + int rv = -1; + if (http2session_->get_state() == Http2SessionState::CONNECTED && + downstream->get_downstream_stream_id() != -1) { + switch (downstream->get_response_state()) { + case DownstreamState::MSG_RESET: + case DownstreamState::MSG_BAD_HEADER: + case DownstreamState::MSG_COMPLETE: + break; + default: + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Submit RST_STREAM for DOWNSTREAM:" << downstream + << ", stream_id=" + << downstream->get_downstream_stream_id() + << ", error_code=" << error_code; + } + rv = http2session_->submit_rst_stream( + downstream->get_downstream_stream_id(), error_code); + } + } + return rv; +} + +namespace { +ssize_t http2_data_read_callback(nghttp2_session *session, int32_t stream_id, + uint8_t *buf, size_t length, + uint32_t *data_flags, + nghttp2_data_source *source, void *user_data) { + int rv; + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + if (!sd || !sd->dconn) { + return NGHTTP2_ERR_DEFERRED; + } + auto dconn = sd->dconn; + auto downstream = dconn->get_downstream(); + if (!downstream) { + // In this case, RST_STREAM should have been issued. But depending + // on the priority, DATA frame may come first. + return NGHTTP2_ERR_DEFERRED; + } + const auto &req = downstream->request(); + auto input = downstream->get_request_buf(); + + auto nread = std::min(input->rleft(), length); + auto input_empty = input->rleft() == nread; + + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + + if (input_empty && + downstream->get_request_state() == DownstreamState::MSG_COMPLETE && + // If connection is upgraded, don't set EOF flag, since HTTP/1 + // will set MSG_COMPLETE to request state after upgrade response + // header is seen. + (!req.upgrade_request || + (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE && + !downstream->get_upgraded()))) { + + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + + const auto &trailers = req.fs.trailers(); + if (!trailers.empty()) { + std::vector<nghttp2_nv> nva; + nva.reserve(trailers.size()); + http2::copy_headers_to_nva_nocopy(nva, trailers, http2::HDOP_STRIP_ALL); + if (!nva.empty()) { + rv = nghttp2_submit_trailer(session, stream_id, nva.data(), nva.size()); + if (rv != 0) { + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } else { + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + } + } + } + } + + if (nread == 0 && (*data_flags & NGHTTP2_DATA_FLAG_EOF) == 0) { + downstream->disable_downstream_wtimer(); + + return NGHTTP2_ERR_DEFERRED; + } + + return nread; +} +} // namespace + +int Http2DownstreamConnection::push_request_headers() { + int rv; + if (!downstream_) { + return 0; + } + if (!http2session_->can_push_request(downstream_)) { + // The HTTP2 session to the backend has not been established or + // connection is now being checked. This function will be called + // again just after it is established. + downstream_->set_request_pending(true); + http2session_->start_checking_connection(); + return 0; + } + + downstream_->set_request_pending(false); + + const auto &req = downstream_->request(); + + if (req.connect_proto != ConnectProto::NONE && + !http2session_->get_allow_connect_proto()) { + return -1; + } + + auto &balloc = downstream_->get_block_allocator(); + + auto config = get_config(); + auto &httpconf = config->http; + auto &http2conf = config->http2; + + auto no_host_rewrite = httpconf.no_host_rewrite || config->http2_proxy || + req.regular_connect_method(); + + // http2session_ has already in CONNECTED state, so we can get + // addr_idx here. + const auto &downstream_hostport = http2session_->get_addr()->hostport; + + // For HTTP/1.0 request, there is no authority in request. In that + // case, we use backend server's host nonetheless. + auto authority = StringRef(downstream_hostport); + + if (no_host_rewrite && !req.authority.empty()) { + authority = req.authority; + } + + downstream_->set_request_downstream_host(authority); + + size_t num_cookies = 0; + if (!http2conf.no_cookie_crumbling) { + num_cookies = downstream_->count_crumble_request_cookie(); + } + + // 11 means: + // 1. :method + // 2. :scheme + // 3. :path + // 4. :authority (or host) + // 5. :protocol (optional) + // 6. via (optional) + // 7. x-forwarded-for (optional) + // 8. x-forwarded-proto (optional) + // 9. te (optional) + // 10. forwarded (optional) + // 11. early-data (optional) + auto nva = std::vector<nghttp2_nv>(); + nva.reserve(req.fs.headers().size() + 11 + num_cookies + + httpconf.add_request_headers.size()); + + if (req.connect_proto == ConnectProto::WEBSOCKET) { + nva.push_back(http2::make_nv_ll(":method", "CONNECT")); + nva.push_back(http2::make_nv_ll(":protocol", "websocket")); + } else { + nva.push_back(http2::make_nv_ls_nocopy( + ":method", http2::to_method_string(req.method))); + } + + if (!req.regular_connect_method()) { + assert(!req.scheme.empty()); + + auto addr = http2session_->get_addr(); + assert(addr); + // We will handle more protocol scheme upgrade in the future. + if (addr->tls && addr->upgrade_scheme && req.scheme == "http") { + nva.push_back(http2::make_nv_ll(":scheme", "https")); + } else { + nva.push_back(http2::make_nv_ls_nocopy(":scheme", req.scheme)); + } + + if (req.method == HTTP_OPTIONS && req.path.empty()) { + nva.push_back(http2::make_nv_ll(":path", "*")); + } else { + nva.push_back(http2::make_nv_ls_nocopy(":path", req.path)); + } + + if (!req.no_authority || req.connect_proto != ConnectProto::NONE) { + nva.push_back(http2::make_nv_ls_nocopy(":authority", authority)); + } else { + nva.push_back(http2::make_nv_ls_nocopy("host", authority)); + } + } else { + nva.push_back(http2::make_nv_ls_nocopy(":authority", authority)); + } + + auto &fwdconf = httpconf.forwarded; + auto &xffconf = httpconf.xff; + auto &xfpconf = httpconf.xfp; + auto &earlydataconf = httpconf.early_data; + + uint32_t build_flags = + (fwdconf.strip_incoming ? http2::HDOP_STRIP_FORWARDED : 0) | + (xffconf.strip_incoming ? http2::HDOP_STRIP_X_FORWARDED_FOR : 0) | + (xfpconf.strip_incoming ? http2::HDOP_STRIP_X_FORWARDED_PROTO : 0) | + (earlydataconf.strip_incoming ? http2::HDOP_STRIP_EARLY_DATA : 0) | + http2::HDOP_STRIP_SEC_WEBSOCKET_KEY; + + http2::copy_headers_to_nva_nocopy(nva, req.fs.headers(), build_flags); + + if (!http2conf.no_cookie_crumbling) { + downstream_->crumble_request_cookie(nva); + } + + auto upstream = downstream_->get_upstream(); + auto handler = upstream->get_client_handler(); + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + auto conn = handler->get_connection(); + + if (conn->tls.ssl && !SSL_is_init_finished(conn->tls.ssl)) { + nva.push_back(http2::make_nv_ll("early-data", "1")); + } +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_BORINGSSL + + auto fwd = + fwdconf.strip_incoming ? nullptr : req.fs.header(http2::HD_FORWARDED); + + if (fwdconf.params) { + auto params = fwdconf.params; + + if (config->http2_proxy || req.regular_connect_method()) { + params &= ~FORWARDED_PROTO; + } + + auto value = http::create_forwarded( + balloc, params, handler->get_forwarded_by(), + handler->get_forwarded_for(), req.authority, req.scheme); + + if (fwd || !value.empty()) { + if (fwd) { + if (value.empty()) { + value = fwd->value; + } else { + value = concat_string_ref(balloc, fwd->value, + StringRef::from_lit(", "), value); + } + } + + nva.push_back(http2::make_nv_ls_nocopy("forwarded", value)); + } + } else if (fwd) { + nva.push_back(http2::make_nv_ls_nocopy("forwarded", fwd->value)); + } + + auto xff = xffconf.strip_incoming ? nullptr + : req.fs.header(http2::HD_X_FORWARDED_FOR); + + if (xffconf.add) { + StringRef xff_value; + const auto &addr = upstream->get_client_handler()->get_ipaddr(); + if (xff) { + xff_value = concat_string_ref(balloc, xff->value, + StringRef::from_lit(", "), addr); + } else { + xff_value = addr; + } + nva.push_back(http2::make_nv_ls_nocopy("x-forwarded-for", xff_value)); + } else if (xff) { + nva.push_back(http2::make_nv_ls_nocopy("x-forwarded-for", xff->value)); + } + + if (!config->http2_proxy && !req.regular_connect_method()) { + auto xfp = xfpconf.strip_incoming + ? nullptr + : req.fs.header(http2::HD_X_FORWARDED_PROTO); + + if (xfpconf.add) { + StringRef xfp_value; + // We use same protocol with :scheme header field + if (xfp) { + xfp_value = concat_string_ref(balloc, xfp->value, + StringRef::from_lit(", "), req.scheme); + } else { + xfp_value = req.scheme; + } + nva.push_back(http2::make_nv_ls_nocopy("x-forwarded-proto", xfp_value)); + } else if (xfp) { + nva.push_back(http2::make_nv_ls_nocopy("x-forwarded-proto", xfp->value)); + } + } + + auto via = req.fs.header(http2::HD_VIA); + if (httpconf.no_via) { + if (via) { + nva.push_back(http2::make_nv_ls_nocopy("via", (*via).value)); + } + } else { + size_t vialen = 16; + if (via) { + vialen += via->value.size() + 2; + } + + auto iov = make_byte_ref(balloc, vialen + 1); + auto p = iov.base; + + if (via) { + p = std::copy(std::begin(via->value), std::end(via->value), p); + p = util::copy_lit(p, ", "); + } + p = http::create_via_header_value(p, req.http_major, req.http_minor); + *p = '\0'; + + nva.push_back(http2::make_nv_ls_nocopy("via", StringRef{iov.base, p})); + } + + auto te = req.fs.header(http2::HD_TE); + // HTTP/1 upstream request can contain keyword other than + // "trailers". We just forward "trailers". + // TODO more strict handling required here. + if (te && http2::contains_trailers(te->value)) { + nva.push_back(http2::make_nv_ll("te", "trailers")); + } + + for (auto &p : httpconf.add_request_headers) { + nva.push_back(http2::make_nv_nocopy(p.name, p.value)); + } + + if (LOG_ENABLED(INFO)) { + std::stringstream ss; + for (auto &nv : nva) { + if (util::streq_l("authorization", nv.name, nv.namelen)) { + ss << TTY_HTTP_HD << StringRef{nv.name, nv.namelen} << TTY_RST + << ": <redacted>\n"; + continue; + } + ss << TTY_HTTP_HD << StringRef{nv.name, nv.namelen} << TTY_RST << ": " + << StringRef{nv.value, nv.valuelen} << "\n"; + } + DCLOG(INFO, this) << "HTTP request headers\n" << ss.str(); + } + + auto transfer_encoding = req.fs.header(http2::HD_TRANSFER_ENCODING); + + nghttp2_data_provider *data_prdptr = nullptr; + nghttp2_data_provider data_prd; + + // Add body as long as transfer-encoding is given even if + // req.fs.content_length == 0 to forward trailer fields. + if (req.method == HTTP_CONNECT || req.connect_proto != ConnectProto::NONE || + transfer_encoding || req.fs.content_length > 0 || req.http2_expect_body) { + // Request-body is expected. + data_prd = {{}, http2_data_read_callback}; + data_prdptr = &data_prd; + } + + rv = http2session_->submit_request(this, nva.data(), nva.size(), data_prdptr); + if (rv != 0) { + DCLOG(FATAL, this) << "nghttp2_submit_request() failed"; + return -1; + } + + if (data_prdptr) { + downstream_->reset_downstream_wtimer(); + } + + http2session_->signal_write(); + return 0; +} + +int Http2DownstreamConnection::push_upload_data_chunk(const uint8_t *data, + size_t datalen) { + if (!downstream_->get_request_header_sent()) { + auto output = downstream_->get_blocked_request_buf(); + auto &req = downstream_->request(); + output->append(data, datalen); + req.unconsumed_body_length += datalen; + return 0; + } + + int rv; + auto output = downstream_->get_request_buf(); + output->append(data, datalen); + if (downstream_->get_downstream_stream_id() != -1) { + rv = http2session_->resume_data(this); + if (rv != 0) { + return -1; + } + + downstream_->ensure_downstream_wtimer(); + + http2session_->signal_write(); + } + return 0; +} + +int Http2DownstreamConnection::end_upload_data() { + if (!downstream_->get_request_header_sent()) { + downstream_->set_blocked_request_data_eof(true); + return 0; + } + + int rv; + if (downstream_->get_downstream_stream_id() != -1) { + rv = http2session_->resume_data(this); + if (rv != 0) { + return -1; + } + + downstream_->ensure_downstream_wtimer(); + + http2session_->signal_write(); + } + return 0; +} + +int Http2DownstreamConnection::resume_read(IOCtrlReason reason, + size_t consumed) { + int rv; + + if (http2session_->get_state() != Http2SessionState::CONNECTED) { + return 0; + } + + if (!downstream_ || downstream_->get_downstream_stream_id() == -1) { + return 0; + } + + if (consumed > 0) { + rv = http2session_->consume(downstream_->get_downstream_stream_id(), + consumed); + + if (rv != 0) { + return -1; + } + + auto &resp = downstream_->response(); + + resp.unconsumed_body_length -= consumed; + + http2session_->signal_write(); + } + + return 0; +} + +int Http2DownstreamConnection::on_read() { return 0; } + +int Http2DownstreamConnection::on_write() { return 0; } + +void Http2DownstreamConnection::attach_stream_data(StreamData *sd) { + // It is possible sd->dconn is not NULL. sd is detached when + // on_stream_close_callback. Before that, after MSG_COMPLETE is set + // to Downstream::set_response_state(), upstream's readcb is called + // and execution path eventually could reach here. Since the + // response was already handled, we just detach sd. + detach_stream_data(); + sd_ = sd; + sd_->dconn = this; +} + +StreamData *Http2DownstreamConnection::detach_stream_data() { + if (sd_) { + auto sd = sd_; + sd_ = nullptr; + sd->dconn = nullptr; + return sd; + } + return nullptr; +} + +int Http2DownstreamConnection::on_timeout() { + if (!downstream_) { + return 0; + } + + return submit_rst_stream(downstream_, NGHTTP2_NO_ERROR); +} + +const std::shared_ptr<DownstreamAddrGroup> & +Http2DownstreamConnection::get_downstream_addr_group() const { + return http2session_->get_downstream_addr_group(); +} + +DownstreamAddr *Http2DownstreamConnection::get_addr() const { return nullptr; } + +} // namespace shrpx diff --git a/src/shrpx_http2_downstream_connection.h b/src/shrpx_http2_downstream_connection.h new file mode 100644 index 0000000..0fc7d91 --- /dev/null +++ b/src/shrpx_http2_downstream_connection.h @@ -0,0 +1,88 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP2_DOWNSTREAM_CONNECTION_H +#define SHRPX_HTTP2_DOWNSTREAM_CONNECTION_H + +#include "shrpx.h" + +#include <openssl/ssl.h> + +#include <nghttp2/nghttp2.h> + +#include "shrpx_downstream_connection.h" + +namespace shrpx { + +struct StreamData; +class Http2Session; +class DownstreamConnectionPool; + +class Http2DownstreamConnection : public DownstreamConnection { +public: + Http2DownstreamConnection(Http2Session *http2session); + virtual ~Http2DownstreamConnection(); + virtual int attach_downstream(Downstream *downstream); + virtual void detach_downstream(Downstream *downstream); + + virtual int push_request_headers(); + virtual int push_upload_data_chunk(const uint8_t *data, size_t datalen); + virtual int end_upload_data(); + + virtual void pause_read(IOCtrlReason reason) {} + virtual int resume_read(IOCtrlReason reason, size_t consumed); + virtual void force_resume_read() {} + + virtual int on_read(); + virtual int on_write(); + virtual int on_timeout(); + + virtual void on_upstream_change(Upstream *upstream) {} + + // This object is not poolable because we don't have facility to + // migrate to another Http2Session object. + virtual bool poolable() const { return false; } + + virtual const std::shared_ptr<DownstreamAddrGroup> & + get_downstream_addr_group() const; + virtual DownstreamAddr *get_addr() const; + + int send(); + + void attach_stream_data(StreamData *sd); + StreamData *detach_stream_data(); + + int submit_rst_stream(Downstream *downstream, + uint32_t error_code = NGHTTP2_INTERNAL_ERROR); + + Http2DownstreamConnection *dlnext, *dlprev; + +private: + Http2Session *http2session_; + StreamData *sd_; +}; + +} // namespace shrpx + +#endif // SHRPX_HTTP2_DOWNSTREAM_CONNECTION_H diff --git a/src/shrpx_http2_session.cc b/src/shrpx_http2_session.cc new file mode 100644 index 0000000..f58ed2f --- /dev/null +++ b/src/shrpx_http2_session.cc @@ -0,0 +1,2426 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_http2_session.h" + +#include <netinet/tcp.h> +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H + +#include <vector> + +#include <openssl/err.h> + +#include "shrpx_upstream.h" +#include "shrpx_downstream.h" +#include "shrpx_config.h" +#include "shrpx_error.h" +#include "shrpx_http2_downstream_connection.h" +#include "shrpx_client_handler.h" +#include "shrpx_tls.h" +#include "shrpx_http.h" +#include "shrpx_worker.h" +#include "shrpx_connect_blocker.h" +#include "shrpx_log.h" +#include "http2.h" +#include "util.h" +#include "base64.h" +#include "tls.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +constexpr ev_tstamp CONNCHK_TIMEOUT = 5.; +constexpr ev_tstamp CONNCHK_PING_TIMEOUT = 1.; +} // namespace + +namespace { +constexpr size_t MAX_BUFFER_SIZE = 32_k; +} // namespace + +namespace { +void connchk_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto http2session = static_cast<Http2Session *>(w->data); + + ev_timer_stop(loop, w); + + switch (http2session->get_connection_check_state()) { + case ConnectionCheck::STARTED: + // ping timeout; disconnect + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "ping timeout"; + } + + delete http2session; + + return; + default: + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "connection check required"; + } + http2session->set_connection_check_state(ConnectionCheck::REQUIRED); + } +} +} // namespace + +namespace { +void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto http2session = static_cast<Http2Session *>(w->data); + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "SETTINGS timeout"; + } + + downstream_failure(http2session->get_addr(), http2session->get_raddr()); + + if (http2session->terminate_session(NGHTTP2_SETTINGS_TIMEOUT) != 0) { + delete http2session; + + return; + } + http2session->signal_write(); +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto http2session = static_cast<Http2Session *>(conn->data); + + if (w == &conn->rt && !conn->expired_rt()) { + return; + } + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "Timeout"; + } + + http2session->on_timeout(); + + delete http2session; +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto conn = static_cast<Connection *>(w->data); + auto http2session = static_cast<Http2Session *>(conn->data); + rv = http2session->do_read(); + if (rv != 0) { + delete http2session; + + return; + } + http2session->connection_alive(); +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto conn = static_cast<Connection *>(w->data); + auto http2session = static_cast<Http2Session *>(conn->data); + rv = http2session->do_write(); + if (rv != 0) { + delete http2session; + + return; + } + http2session->reset_connection_check_timer_if_not_checking(); +} +} // namespace + +namespace { +void initiate_connection_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto http2session = static_cast<Http2Session *>(w->data); + ev_timer_stop(loop, w); + if (http2session->initiate_connection() != 0) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "Could not initiate backend connection"; + } + + delete http2session; + + return; + } +} +} // namespace + +namespace { +void prepare_cb(struct ev_loop *loop, ev_prepare *w, int revents) { + auto http2session = static_cast<Http2Session *>(w->data); + http2session->check_retire(); +} +} // namespace + +Http2Session::Http2Session(struct ev_loop *loop, SSL_CTX *ssl_ctx, + Worker *worker, + const std::shared_ptr<DownstreamAddrGroup> &group, + DownstreamAddr *addr) + : dlnext(nullptr), + dlprev(nullptr), + conn_(loop, -1, nullptr, worker->get_mcpool(), + group->shared_addr->timeout.write, group->shared_addr->timeout.read, + {}, {}, writecb, readcb, timeoutcb, this, + get_config()->tls.dyn_rec.warmup_threshold, + get_config()->tls.dyn_rec.idle_timeout, Proto::HTTP2), + wb_(worker->get_mcpool()), + worker_(worker), + ssl_ctx_(ssl_ctx), + group_(group), + addr_(addr), + session_(nullptr), + raddr_(nullptr), + state_(Http2SessionState::DISCONNECTED), + connection_check_state_(ConnectionCheck::NONE), + freelist_zone_(FreelistZone::NONE), + settings_recved_(false), + allow_connect_proto_(false) { + read_ = write_ = &Http2Session::noop; + + on_read_ = &Http2Session::read_noop; + on_write_ = &Http2Session::write_noop; + + // We will reuse this many times, so use repeat timeout value. The + // timeout value is set later. + ev_timer_init(&connchk_timer_, connchk_timeout_cb, 0., 0.); + + connchk_timer_.data = this; + + // SETTINGS ACK timeout is 10 seconds for now. We will reuse this + // many times, so use repeat timeout value. + ev_timer_init(&settings_timer_, settings_timeout_cb, 0., 0.); + + settings_timer_.data = this; + + ev_timer_init(&initiate_connection_timer_, initiate_connection_cb, 0., 0.); + initiate_connection_timer_.data = this; + + ev_prepare_init(&prep_, prepare_cb); + prep_.data = this; + ev_prepare_start(loop, &prep_); +} + +Http2Session::~Http2Session() { + exclude_from_scheduling(); + disconnect(should_hard_fail()); +} + +int Http2Session::disconnect(bool hard) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Disconnecting"; + } + nghttp2_session_del(session_); + session_ = nullptr; + + wb_.reset(); + + if (dns_query_) { + auto dns_tracker = worker_->get_dns_tracker(); + dns_tracker->cancel(dns_query_.get()); + } + + conn_.rlimit.stopw(); + conn_.wlimit.stopw(); + + ev_prepare_stop(conn_.loop, &prep_); + + ev_timer_stop(conn_.loop, &initiate_connection_timer_); + ev_timer_stop(conn_.loop, &settings_timer_); + ev_timer_stop(conn_.loop, &connchk_timer_); + + read_ = write_ = &Http2Session::noop; + + on_read_ = &Http2Session::read_noop; + on_write_ = &Http2Session::write_noop; + + conn_.disconnect(); + + if (proxy_htp_) { + proxy_htp_.reset(); + } + + connection_check_state_ = ConnectionCheck::NONE; + state_ = Http2SessionState::DISCONNECTED; + + // When deleting Http2DownstreamConnection, it calls this object's + // remove_downstream_connection(). The multiple + // Http2DownstreamConnection objects belong to the same + // ClientHandler object if upstream is h2. So be careful when you + // delete ClientHandler here. + // + // We allow creating new pending Http2DownstreamConnection with this + // object. Upstream::on_downstream_reset() may add + // Http2DownstreamConnection to another Http2Session. + + for (auto dc = dconns_.head; dc;) { + auto next = dc->dlnext; + auto downstream = dc->get_downstream(); + auto upstream = downstream->get_upstream(); + + // Failure is allowed only for HTTP/1 upstream where upstream is + // not shared by multiple Downstreams. + if (upstream->on_downstream_reset(downstream, hard) != 0) { + delete upstream->get_client_handler(); + } + + // dc was deleted + dc = next; + } + + auto streams = std::move(streams_); + for (auto s = streams.head; s;) { + auto next = s->dlnext; + delete s; + s = next; + } + + return 0; +} + +int Http2Session::resolve_name() { + auto dns_query = std::make_unique<DNSQuery>( + addr_->host, [this](DNSResolverStatus status, const Address *result) { + int rv; + + if (status == DNSResolverStatus::OK) { + *resolved_addr_ = *result; + util::set_port(*this->resolved_addr_, this->addr_->port); + } + + rv = this->initiate_connection(); + if (rv != 0) { + delete this; + } + }); + resolved_addr_ = std::make_unique<Address>(); + auto dns_tracker = worker_->get_dns_tracker(); + switch (dns_tracker->resolve(resolved_addr_.get(), dns_query.get())) { + case DNSResolverStatus::ERROR: + return -1; + case DNSResolverStatus::RUNNING: + dns_query_ = std::move(dns_query); + state_ = Http2SessionState::RESOLVING_NAME; + return 0; + case DNSResolverStatus::OK: + util::set_port(*resolved_addr_, addr_->port); + return 0; + default: + assert(0); + abort(); + } +} + +namespace { +int htp_hdrs_completecb(llhttp_t *htp); +} // namespace + +namespace { +constexpr llhttp_settings_t htp_hooks = { + nullptr, // llhttp_cb on_message_begin; + nullptr, // llhttp_data_cb on_url; + nullptr, // llhttp_data_cb on_status; + nullptr, // llhttp_data_cb on_method; + nullptr, // llhttp_data_cb on_version; + nullptr, // llhttp_data_cb on_header_field; + nullptr, // llhttp_data_cb on_header_value; + nullptr, // llhttp_data_cb on_chunk_extension_name; + nullptr, // llhttp_data_cb on_chunk_extension_value; + htp_hdrs_completecb, // llhttp_cb on_headers_complete; + nullptr, // llhttp_data_cb on_body; + nullptr, // llhttp_cb on_message_complete; + nullptr, // llhttp_cb on_url_complete; + nullptr, // llhttp_cb on_status_complete; + nullptr, // llhttp_cb on_method_complete; + nullptr, // llhttp_cb on_version_complete; + nullptr, // llhttp_cb on_header_field_complete; + nullptr, // llhttp_cb on_header_value_complete; + nullptr, // llhttp_cb on_chunk_extension_name_complete; + nullptr, // llhttp_cb on_chunk_extension_value_complete; + nullptr, // llhttp_cb on_chunk_header; + nullptr, // llhttp_cb on_chunk_complete; + nullptr, // llhttp_cb on_reset; +}; +} // namespace + +int Http2Session::initiate_connection() { + int rv = 0; + + auto worker_blocker = worker_->get_connect_blocker(); + + if (state_ == Http2SessionState::DISCONNECTED || + state_ == Http2SessionState::RESOLVING_NAME) { + if (worker_blocker->blocked()) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) + << "Worker wide backend connection was blocked temporarily"; + } + return -1; + } + } + + auto &downstreamconf = *get_config()->conn.downstream; + + const auto &proxy = get_config()->downstream_http_proxy; + if (!proxy.host.empty() && state_ == Http2SessionState::DISCONNECTED) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Connecting to the proxy " << proxy.host << ":" + << proxy.port; + } + + conn_.fd = util::create_nonblock_socket(proxy.addr.su.storage.ss_family); + + if (conn_.fd == -1) { + auto error = errno; + SSLOG(WARN, this) << "Backend proxy socket() failed; addr=" + << util::to_numeric_addr(&proxy.addr) + << ", errno=" << error; + + worker_blocker->on_failure(); + return -1; + } + + rv = connect(conn_.fd, &proxy.addr.su.sa, proxy.addr.len); + if (rv != 0 && errno != EINPROGRESS) { + auto error = errno; + SSLOG(WARN, this) << "Backend proxy connect() failed; addr=" + << util::to_numeric_addr(&proxy.addr) + << ", errno=" << error; + + worker_blocker->on_failure(); + + return -1; + } + + raddr_ = &proxy.addr; + + worker_blocker->on_success(); + + ev_io_set(&conn_.rev, conn_.fd, EV_READ); + ev_io_set(&conn_.wev, conn_.fd, EV_WRITE); + + conn_.wlimit.startw(); + + conn_.wt.repeat = downstreamconf.timeout.connect; + ev_timer_again(conn_.loop, &conn_.wt); + + write_ = &Http2Session::connected; + + on_read_ = &Http2Session::downstream_read_proxy; + on_write_ = &Http2Session::downstream_connect_proxy; + + proxy_htp_ = std::make_unique<llhttp_t>(); + llhttp_init(proxy_htp_.get(), HTTP_RESPONSE, &htp_hooks); + proxy_htp_->data = this; + + state_ = Http2SessionState::PROXY_CONNECTING; + + return 0; + } + + if (state_ == Http2SessionState::DISCONNECTED || + state_ == Http2SessionState::PROXY_CONNECTED || + state_ == Http2SessionState::RESOLVING_NAME) { + if (LOG_ENABLED(INFO)) { + if (state_ != Http2SessionState::RESOLVING_NAME) { + SSLOG(INFO, this) << "Connecting to downstream server"; + } + } + if (addr_->tls) { + assert(ssl_ctx_); + + if (state_ != Http2SessionState::RESOLVING_NAME) { + auto ssl = tls::create_ssl(ssl_ctx_); + if (!ssl) { + return -1; + } + + tls::setup_downstream_http2_alpn(ssl); + + conn_.set_ssl(ssl); + conn_.tls.client_session_cache = &addr_->tls_session_cache; + + auto sni_name = + addr_->sni.empty() ? StringRef{addr_->host} : StringRef{addr_->sni}; + + if (!util::numeric_host(sni_name.c_str())) { + // TLS extensions: SNI. There is no documentation about the return + // code for this function (actually this is macro wrapping SSL_ctrl + // at the time of this writing). + SSL_set_tlsext_host_name(conn_.tls.ssl, sni_name.c_str()); + } + + auto tls_session = tls::reuse_tls_session(addr_->tls_session_cache); + if (tls_session) { + SSL_set_session(conn_.tls.ssl, tls_session); + SSL_SESSION_free(tls_session); + } + } + + if (state_ == Http2SessionState::DISCONNECTED) { + if (addr_->dns) { + rv = resolve_name(); + if (rv != 0) { + downstream_failure(addr_, nullptr); + return -1; + } + if (state_ == Http2SessionState::RESOLVING_NAME) { + return 0; + } + raddr_ = resolved_addr_.get(); + } else { + raddr_ = &addr_->addr; + } + } + + if (state_ == Http2SessionState::RESOLVING_NAME) { + if (dns_query_->status == DNSResolverStatus::ERROR) { + downstream_failure(addr_, nullptr); + return -1; + } + assert(dns_query_->status == DNSResolverStatus::OK); + state_ = Http2SessionState::DISCONNECTED; + dns_query_.reset(); + raddr_ = resolved_addr_.get(); + } + + // If state_ == Http2SessionState::PROXY_CONNECTED, we have + // connected to the proxy using conn_.fd and tunnel has been + // established. + if (state_ == Http2SessionState::DISCONNECTED) { + assert(conn_.fd == -1); + + conn_.fd = util::create_nonblock_socket(raddr_->su.storage.ss_family); + if (conn_.fd == -1) { + auto error = errno; + SSLOG(WARN, this) + << "socket() failed; addr=" << util::to_numeric_addr(raddr_) + << ", errno=" << error; + + worker_blocker->on_failure(); + return -1; + } + + worker_blocker->on_success(); + + rv = connect(conn_.fd, + // TODO maybe not thread-safe? + const_cast<sockaddr *>(&raddr_->su.sa), raddr_->len); + if (rv != 0 && errno != EINPROGRESS) { + auto error = errno; + SSLOG(WARN, this) + << "connect() failed; addr=" << util::to_numeric_addr(raddr_) + << ", errno=" << error; + + downstream_failure(addr_, raddr_); + return -1; + } + + ev_io_set(&conn_.rev, conn_.fd, EV_READ); + ev_io_set(&conn_.wev, conn_.fd, EV_WRITE); + } + + conn_.prepare_client_handshake(); + } else { + if (state_ == Http2SessionState::DISCONNECTED) { + // Without TLS and proxy. + if (addr_->dns) { + rv = resolve_name(); + if (rv != 0) { + downstream_failure(addr_, nullptr); + return -1; + } + if (state_ == Http2SessionState::RESOLVING_NAME) { + return 0; + } + raddr_ = resolved_addr_.get(); + } else { + raddr_ = &addr_->addr; + } + } + + if (state_ == Http2SessionState::RESOLVING_NAME) { + if (dns_query_->status == DNSResolverStatus::ERROR) { + downstream_failure(addr_, nullptr); + return -1; + } + assert(dns_query_->status == DNSResolverStatus::OK); + state_ = Http2SessionState::DISCONNECTED; + dns_query_.reset(); + raddr_ = resolved_addr_.get(); + } + + if (state_ == Http2SessionState::DISCONNECTED) { + // Without TLS and proxy. + assert(conn_.fd == -1); + + conn_.fd = util::create_nonblock_socket(raddr_->su.storage.ss_family); + + if (conn_.fd == -1) { + auto error = errno; + SSLOG(WARN, this) + << "socket() failed; addr=" << util::to_numeric_addr(raddr_) + << ", errno=" << error; + + worker_blocker->on_failure(); + return -1; + } + + worker_blocker->on_success(); + + rv = connect(conn_.fd, const_cast<sockaddr *>(&raddr_->su.sa), + raddr_->len); + if (rv != 0 && errno != EINPROGRESS) { + auto error = errno; + SSLOG(WARN, this) + << "connect() failed; addr=" << util::to_numeric_addr(raddr_) + << ", errno=" << error; + + downstream_failure(addr_, raddr_); + return -1; + } + + ev_io_set(&conn_.rev, conn_.fd, EV_READ); + ev_io_set(&conn_.wev, conn_.fd, EV_WRITE); + } + } + + // We have been already connected when no TLS and proxy is used. + if (state_ == Http2SessionState::PROXY_CONNECTED) { + on_read_ = &Http2Session::read_noop; + on_write_ = &Http2Session::write_noop; + + return connected(); + } + + write_ = &Http2Session::connected; + + state_ = Http2SessionState::CONNECTING; + conn_.wlimit.startw(); + + conn_.wt.repeat = downstreamconf.timeout.connect; + ev_timer_again(conn_.loop, &conn_.wt); + + return 0; + } + + // Unreachable + assert(0); + + return 0; +} + +namespace { +int htp_hdrs_completecb(llhttp_t *htp) { + auto http2session = static_cast<Http2Session *>(htp->data); + + // We only read HTTP header part. If tunneling succeeds, response + // body is a different protocol (HTTP/2 in this case), we don't read + // them here. + + // We just check status code here + if (htp->status_code / 100 == 2) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "Tunneling success"; + } + http2session->set_state(Http2SessionState::PROXY_CONNECTED); + + return HPE_PAUSED; + } + + SSLOG(WARN, http2session) << "Tunneling failed: " << htp->status_code; + http2session->set_state(Http2SessionState::PROXY_FAILED); + + return HPE_PAUSED; +} +} // namespace + +int Http2Session::downstream_read_proxy(const uint8_t *data, size_t datalen) { + auto htperr = llhttp_execute(proxy_htp_.get(), + reinterpret_cast<const char *>(data), datalen); + if (htperr == HPE_PAUSED) { + switch (state_) { + case Http2SessionState::PROXY_CONNECTED: + // Initiate SSL/TLS handshake through established tunnel. + if (initiate_connection() != 0) { + return -1; + } + return 0; + case Http2SessionState::PROXY_FAILED: + return -1; + default: + break; + } + // should not be here + assert(0); + } + + if (htperr != HPE_OK) { + return -1; + } + + return 0; +} + +int Http2Session::downstream_connect_proxy() { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Connected to the proxy"; + } + + std::string req = "CONNECT "; + req.append(addr_->hostport.c_str(), addr_->hostport.size()); + if (addr_->port == 80 || addr_->port == 443) { + req += ':'; + req += util::utos(addr_->port); + } + req += " HTTP/1.1\r\nHost: "; + req += addr_->host; + req += "\r\n"; + const auto &proxy = get_config()->downstream_http_proxy; + if (!proxy.userinfo.empty()) { + req += "Proxy-Authorization: Basic "; + req += base64::encode(std::begin(proxy.userinfo), std::end(proxy.userinfo)); + req += "\r\n"; + } + req += "\r\n"; + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "HTTP proxy request headers\n" << req; + } + wb_.append(req); + + on_write_ = &Http2Session::write_noop; + + signal_write(); + return 0; +} + +void Http2Session::add_downstream_connection(Http2DownstreamConnection *dconn) { + dconns_.append(dconn); + ++addr_->num_dconn; +} + +void Http2Session::remove_downstream_connection( + Http2DownstreamConnection *dconn) { + --addr_->num_dconn; + dconns_.remove(dconn); + dconn->detach_stream_data(); + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Remove downstream"; + } + + if (freelist_zone_ == FreelistZone::NONE && !max_concurrency_reached()) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Append to http2_extra_freelist, addr=" << addr_ + << ", freelist.size=" + << addr_->http2_extra_freelist.size(); + } + + add_to_extra_freelist(); + } +} + +void Http2Session::remove_stream_data(StreamData *sd) { + streams_.remove(sd); + if (sd->dconn) { + sd->dconn->detach_stream_data(); + } + delete sd; +} + +int Http2Session::submit_request(Http2DownstreamConnection *dconn, + const nghttp2_nv *nva, size_t nvlen, + const nghttp2_data_provider *data_prd) { + assert(state_ == Http2SessionState::CONNECTED); + auto sd = std::make_unique<StreamData>(); + sd->dlnext = sd->dlprev = nullptr; + // TODO Specify nullptr to pri_spec for now + auto stream_id = + nghttp2_submit_request(session_, nullptr, nva, nvlen, data_prd, sd.get()); + if (stream_id < 0) { + SSLOG(FATAL, this) << "nghttp2_submit_request() failed: " + << nghttp2_strerror(stream_id); + return -1; + } + + dconn->attach_stream_data(sd.get()); + dconn->get_downstream()->set_downstream_stream_id(stream_id); + streams_.append(sd.release()); + + return 0; +} + +int Http2Session::submit_rst_stream(int32_t stream_id, uint32_t error_code) { + assert(state_ == Http2SessionState::CONNECTED); + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "RST_STREAM stream_id=" << stream_id + << " with error_code=" << error_code; + } + int rv = nghttp2_submit_rst_stream(session_, NGHTTP2_FLAG_NONE, stream_id, + error_code); + if (rv != 0) { + SSLOG(FATAL, this) << "nghttp2_submit_rst_stream() failed: " + << nghttp2_strerror(rv); + return -1; + } + return 0; +} + +nghttp2_session *Http2Session::get_session() const { return session_; } + +int Http2Session::resume_data(Http2DownstreamConnection *dconn) { + assert(state_ == Http2SessionState::CONNECTED); + auto downstream = dconn->get_downstream(); + int rv = nghttp2_session_resume_data(session_, + downstream->get_downstream_stream_id()); + switch (rv) { + case 0: + case NGHTTP2_ERR_INVALID_ARGUMENT: + return 0; + default: + SSLOG(FATAL, this) << "nghttp2_resume_session() failed: " + << nghttp2_strerror(rv); + return -1; + } +} + +namespace { +void call_downstream_readcb(Http2Session *http2session, + Downstream *downstream) { + auto upstream = downstream->get_upstream(); + if (!upstream) { + return; + } + if (upstream->downstream_read(downstream->get_downstream_connection()) != 0) { + delete upstream->get_client_handler(); + } +} +} // namespace + +namespace { +int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *user_data) { + auto http2session = static_cast<Http2Session *>(user_data); + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) + << "Stream stream_id=" << stream_id + << " is being closed with error code " << error_code; + } + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + if (sd == 0) { + // We might get this close callback when pushed streams are + // closed. + return 0; + } + auto dconn = sd->dconn; + if (dconn) { + auto downstream = dconn->get_downstream(); + auto upstream = downstream->get_upstream(); + + if (downstream->get_downstream_stream_id() % 2 == 0 && + downstream->get_request_state() == DownstreamState::INITIAL) { + // Downstream is canceled in backend before it is submitted in + // frontend session. + + // This will avoid to send RST_STREAM to backend + downstream->set_response_state(DownstreamState::MSG_RESET); + upstream->cancel_premature_downstream(downstream); + } else { + if (downstream->get_upgraded() && downstream->get_response_state() == + DownstreamState::HEADER_COMPLETE) { + // For tunneled connection, we have to submit RST_STREAM to + // upstream *after* whole response body is sent. We just set + // MSG_COMPLETE here. Upstream will take care of that. + downstream->get_upstream()->on_downstream_body_complete(downstream); + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + } else if (error_code == NGHTTP2_NO_ERROR) { + switch (downstream->get_response_state()) { + case DownstreamState::MSG_COMPLETE: + case DownstreamState::MSG_BAD_HEADER: + break; + default: + downstream->set_response_state(DownstreamState::MSG_RESET); + } + } else if (downstream->get_response_state() != + DownstreamState::MSG_BAD_HEADER) { + downstream->set_response_state(DownstreamState::MSG_RESET); + } + if (downstream->get_response_state() == DownstreamState::MSG_RESET && + downstream->get_response_rst_stream_error_code() == + NGHTTP2_NO_ERROR) { + downstream->set_response_rst_stream_error_code(error_code); + } + call_downstream_readcb(http2session, downstream); + } + // dconn may be deleted + } + // The life time of StreamData ends here + http2session->remove_stream_data(sd); + return 0; +} +} // namespace + +void Http2Session::start_settings_timer() { + auto &downstreamconf = get_config()->http2.downstream; + + ev_timer_set(&settings_timer_, downstreamconf.timeout.settings, 0.); + ev_timer_start(conn_.loop, &settings_timer_); +} + +void Http2Session::stop_settings_timer() { + ev_timer_stop(conn_.loop, &settings_timer_); +} + +namespace { +int on_header_callback2(nghttp2_session *session, const nghttp2_frame *frame, + nghttp2_rcbuf *name, nghttp2_rcbuf *value, + uint8_t flags, void *user_data) { + auto http2session = static_cast<Http2Session *>(user_data); + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + return 0; + } + auto downstream = sd->dconn->get_downstream(); + + auto namebuf = nghttp2_rcbuf_get_buf(name); + auto valuebuf = nghttp2_rcbuf_get_buf(value); + + auto &resp = downstream->response(); + auto &httpconf = get_config()->http; + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + auto trailer = frame->headers.cat == NGHTTP2_HCAT_HEADERS && + !downstream->get_expect_final_response(); + + if (resp.fs.buffer_size() + namebuf.len + valuebuf.len > + httpconf.response_header_field_buffer || + resp.fs.num_fields() >= httpconf.max_response_header_fields) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) + << "Too large or many header field size=" + << resp.fs.buffer_size() + namebuf.len + valuebuf.len + << ", num=" << resp.fs.num_fields() + 1; + } + + if (trailer) { + // We don't care trailer part exceeds header size limit; just + // discard it. + return 0; + } + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + auto token = http2::lookup_token(namebuf.base, namebuf.len); + auto no_index = flags & NGHTTP2_NV_FLAG_NO_INDEX; + + downstream->add_rcbuf(name); + downstream->add_rcbuf(value); + + if (trailer) { + // just store header fields for trailer part + resp.fs.add_trailer_token(StringRef{namebuf.base, namebuf.len}, + StringRef{valuebuf.base, valuebuf.len}, + no_index, token); + return 0; + } + + resp.fs.add_header_token(StringRef{namebuf.base, namebuf.len}, + StringRef{valuebuf.base, valuebuf.len}, no_index, + token); + return 0; + } + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + auto promised_sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, promised_stream_id)); + if (!promised_sd || !promised_sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + auto promised_downstream = promised_sd->dconn->get_downstream(); + + auto namebuf = nghttp2_rcbuf_get_buf(name); + auto valuebuf = nghttp2_rcbuf_get_buf(value); + + assert(promised_downstream); + + auto &promised_req = promised_downstream->request(); + + // We use request header limit for PUSH_PROMISE + if (promised_req.fs.buffer_size() + namebuf.len + valuebuf.len > + httpconf.request_header_field_buffer || + promised_req.fs.num_fields() >= httpconf.max_request_header_fields) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) + << "Too large or many header field size=" + << promised_req.fs.buffer_size() + namebuf.len + valuebuf.len + << ", num=" << promised_req.fs.num_fields() + 1; + } + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + promised_downstream->add_rcbuf(name); + promised_downstream->add_rcbuf(value); + + auto token = http2::lookup_token(namebuf.base, namebuf.len); + promised_req.fs.add_header_token(StringRef{namebuf.base, namebuf.len}, + StringRef{valuebuf.base, valuebuf.len}, + flags & NGHTTP2_NV_FLAG_NO_INDEX, token); + + return 0; + } + } + + return 0; +} +} // namespace + +namespace { +int on_invalid_header_callback2(nghttp2_session *session, + const nghttp2_frame *frame, nghttp2_rcbuf *name, + nghttp2_rcbuf *value, uint8_t flags, + void *user_data) { + auto http2session = static_cast<Http2Session *>(user_data); + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + return 0; + } + + int32_t stream_id; + + if (frame->hd.type == NGHTTP2_PUSH_PROMISE) { + stream_id = frame->push_promise.promised_stream_id; + } else { + stream_id = frame->hd.stream_id; + } + + if (LOG_ENABLED(INFO)) { + auto namebuf = nghttp2_rcbuf_get_buf(name); + auto valuebuf = nghttp2_rcbuf_get_buf(value); + + SSLOG(INFO, http2session) + << "Invalid header field for stream_id=" << stream_id + << " in frame type=" << static_cast<uint32_t>(frame->hd.type) + << ": name=[" << StringRef{namebuf.base, namebuf.len} << "], value=[" + << StringRef{valuebuf.base, valuebuf.len} << "]"; + } + + http2session->submit_rst_stream(stream_id, NGHTTP2_PROTOCOL_ERROR); + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; +} +} // namespace + +namespace { +int on_begin_headers_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + auto http2session = static_cast<Http2Session *>(user_data); + + switch (frame->hd.type) { + case NGHTTP2_HEADERS: { + if (frame->headers.cat != NGHTTP2_HCAT_RESPONSE && + frame->headers.cat != NGHTTP2_HCAT_PUSH_RESPONSE) { + return 0; + } + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + http2session->submit_rst_stream(frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + return 0; + } + return 0; + } + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + auto downstream = sd->dconn->get_downstream(); + + assert(downstream); + assert(downstream->get_downstream_stream_id() == frame->hd.stream_id); + + if (http2session->handle_downstream_push_promise(downstream, + promised_stream_id) != 0) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + return 0; + } + } + + return 0; +} +} // namespace + +namespace { +int on_response_headers(Http2Session *http2session, Downstream *downstream, + nghttp2_session *session, const nghttp2_frame *frame) { + int rv; + + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + const auto &req = downstream->request(); + auto &resp = downstream->response(); + + auto &nva = resp.fs.headers(); + + auto config = get_config(); + auto &loggingconf = config->logging; + + downstream->set_expect_final_response(false); + + auto status = resp.fs.header(http2::HD__STATUS); + // libnghttp2 guarantees this exists and can be parsed + assert(status); + auto status_code = http2::parse_http_status_code(status->value); + + resp.http_status = status_code; + resp.http_major = 2; + resp.http_minor = 0; + + downstream->set_downstream_addr_group( + http2session->get_downstream_addr_group()); + downstream->set_addr(http2session->get_addr()); + + if (LOG_ENABLED(INFO)) { + std::stringstream ss; + for (auto &nv : nva) { + ss << TTY_HTTP_HD << nv.name << TTY_RST << ": " << nv.value << "\n"; + } + SSLOG(INFO, http2session) + << "HTTP response headers. stream_id=" << frame->hd.stream_id << "\n" + << ss.str(); + } + + if (downstream->get_non_final_response()) { + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "This is non-final response."; + } + + downstream->set_expect_final_response(true); + rv = upstream->on_downstream_header_complete(downstream); + + // Now Dowstream's response headers are erased. + + if (rv != 0) { + http2session->submit_rst_stream(frame->hd.stream_id, + NGHTTP2_PROTOCOL_ERROR); + downstream->set_response_state(DownstreamState::MSG_RESET); + } + + return 0; + } + + downstream->set_response_state(DownstreamState::HEADER_COMPLETE); + downstream->check_upgrade_fulfilled_http2(); + + if (downstream->get_upgraded()) { + resp.connection_close = true; + // On upgrade success, both ends can send data + if (upstream->resume_read(SHRPX_NO_BUFFER, downstream, 0) != 0) { + // If resume_read fails, just drop connection. Not ideal. + delete handler; + return -1; + } + downstream->set_request_state(DownstreamState::HEADER_COMPLETE); + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) + << "HTTP upgrade success. stream_id=" << frame->hd.stream_id; + } + } else { + auto content_length = resp.fs.header(http2::HD_CONTENT_LENGTH); + if (content_length) { + // libnghttp2 guarantees this can be parsed + resp.fs.content_length = util::parse_uint(content_length->value); + } + + if (resp.fs.content_length == -1 && downstream->expect_response_body()) { + // Here we have response body but Content-Length is not known in + // advance. + if (req.http_major <= 0 || (req.http_major == 1 && req.http_minor == 0)) { + // We simply close connection for pre-HTTP/1.1 in this case. + resp.connection_close = true; + } else { + // Otherwise, use chunked encoding to keep upstream connection + // open. In HTTP2, we are supposed not to receive + // transfer-encoding. + resp.fs.add_header_token(StringRef::from_lit("transfer-encoding"), + StringRef::from_lit("chunked"), false, + http2::HD_TRANSFER_ENCODING); + downstream->set_chunked_response(true); + } + } + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + resp.headers_only = true; + } + + if (loggingconf.access.write_early && downstream->accesslog_ready()) { + handler->write_accesslog(downstream); + downstream->set_accesslog_written(true); + } + + rv = upstream->on_downstream_header_complete(downstream); + if (rv != 0) { + // Handling early return (in other words, response was hijacked by + // mruby scripting). + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + http2session->submit_rst_stream(frame->hd.stream_id, NGHTTP2_CANCEL); + } else { + http2session->submit_rst_stream(frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + downstream->set_response_state(DownstreamState::MSG_RESET); + } + } + + return 0; +} +} // namespace + +namespace { +int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + int rv; + auto http2session = static_cast<Http2Session *>(user_data); + + switch (frame->hd.type) { + case NGHTTP2_DATA: { + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + return 0; + } + auto downstream = sd->dconn->get_downstream(); + auto upstream = downstream->get_upstream(); + rv = upstream->on_downstream_body(downstream, nullptr, 0, true); + if (rv != 0) { + http2session->submit_rst_stream(frame->hd.stream_id, + NGHTTP2_INTERNAL_ERROR); + downstream->set_response_state(DownstreamState::MSG_RESET); + + } else if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + + downstream->disable_downstream_rtimer(); + + if (downstream->get_response_state() == + DownstreamState::HEADER_COMPLETE) { + + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + rv = upstream->on_downstream_body_complete(downstream); + + if (rv != 0) { + downstream->set_response_state(DownstreamState::MSG_RESET); + } + } + } + + call_downstream_readcb(http2session, downstream); + return 0; + } + case NGHTTP2_HEADERS: { + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + return 0; + } + auto downstream = sd->dconn->get_downstream(); + + if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE || + frame->headers.cat == NGHTTP2_HCAT_PUSH_RESPONSE) { + rv = on_response_headers(http2session, downstream, session, frame); + + if (rv != 0) { + return 0; + } + } else if (frame->headers.cat == NGHTTP2_HCAT_HEADERS) { + if (downstream->get_expect_final_response()) { + rv = on_response_headers(http2session, downstream, session, frame); + + if (rv != 0) { + return 0; + } + } + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + downstream->disable_downstream_rtimer(); + + if (downstream->get_response_state() == + DownstreamState::HEADER_COMPLETE) { + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + auto upstream = downstream->get_upstream(); + + rv = upstream->on_downstream_body_complete(downstream); + + if (rv != 0) { + downstream->set_response_state(DownstreamState::MSG_RESET); + } + } + } else { + downstream->reset_downstream_rtimer(); + } + + // This may delete downstream + call_downstream_readcb(http2session, downstream); + + return 0; + } + case NGHTTP2_RST_STREAM: { + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (sd && sd->dconn) { + auto downstream = sd->dconn->get_downstream(); + downstream->set_response_rst_stream_error_code( + frame->rst_stream.error_code); + call_downstream_readcb(http2session, downstream); + } + return 0; + } + case NGHTTP2_SETTINGS: { + if ((frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { + http2session->on_settings_received(frame); + return 0; + } + + http2session->stop_settings_timer(); + + auto addr = http2session->get_addr(); + auto &connect_blocker = addr->connect_blocker; + + connect_blocker->on_success(); + + return 0; + } + case NGHTTP2_PING: + if (frame->hd.flags & NGHTTP2_FLAG_ACK) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "PING ACK received"; + } + http2session->connection_alive(); + } + return 0; + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) + << "Received downstream PUSH_PROMISE stream_id=" + << frame->hd.stream_id + << ", promised_stream_id=" << promised_stream_id; + } + + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd || !sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } + + auto downstream = sd->dconn->get_downstream(); + + assert(downstream); + assert(downstream->get_downstream_stream_id() == frame->hd.stream_id); + + auto promised_sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, promised_stream_id)); + if (!promised_sd || !promised_sd->dconn) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } + + auto promised_downstream = promised_sd->dconn->get_downstream(); + + assert(promised_downstream); + + if (http2session->handle_downstream_push_promise_complete( + downstream, promised_downstream) != 0) { + http2session->submit_rst_stream(promised_stream_id, NGHTTP2_CANCEL); + return 0; + } + + return 0; + } + case NGHTTP2_GOAWAY: + if (LOG_ENABLED(INFO)) { + auto debug_data = util::ascii_dump(frame->goaway.opaque_data, + frame->goaway.opaque_data_len); + + SSLOG(INFO, http2session) + << "GOAWAY received: last-stream-id=" << frame->goaway.last_stream_id + << ", error_code=" << frame->goaway.error_code + << ", debug_data=" << debug_data; + } + return 0; + default: + return 0; + } +} +} // namespace + +namespace { +int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data) { + int rv; + auto http2session = static_cast<Http2Session *>(user_data); + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + if (!sd || !sd->dconn) { + http2session->submit_rst_stream(stream_id, NGHTTP2_INTERNAL_ERROR); + + if (http2session->consume(stream_id, len) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return 0; + } + auto downstream = sd->dconn->get_downstream(); + if (!downstream->expect_response_body()) { + http2session->submit_rst_stream(stream_id, NGHTTP2_INTERNAL_ERROR); + + if (http2session->consume(stream_id, len) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return 0; + } + + // We don't want DATA after non-final response, which is illegal in + // HTTP. + if (downstream->get_non_final_response()) { + http2session->submit_rst_stream(stream_id, NGHTTP2_PROTOCOL_ERROR); + + if (http2session->consume(stream_id, len) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return 0; + } + + downstream->reset_downstream_rtimer(); + + auto &resp = downstream->response(); + + resp.recv_body_length += len; + resp.unconsumed_body_length += len; + + auto upstream = downstream->get_upstream(); + rv = upstream->on_downstream_body(downstream, data, len, false); + if (rv != 0) { + http2session->submit_rst_stream(stream_id, NGHTTP2_INTERNAL_ERROR); + + if (http2session->consume(stream_id, len) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + downstream->set_response_state(DownstreamState::MSG_RESET); + } + + call_downstream_readcb(http2session, downstream); + return 0; +} +} // namespace + +namespace { +int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + auto http2session = static_cast<Http2Session *>(user_data); + + if (frame->hd.type == NGHTTP2_DATA || frame->hd.type == NGHTTP2_HEADERS) { + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + + if (!sd || !sd->dconn) { + return 0; + } + + auto downstream = sd->dconn->get_downstream(); + + if (frame->hd.type == NGHTTP2_HEADERS && + frame->headers.cat == NGHTTP2_HCAT_REQUEST) { + downstream->set_request_header_sent(true); + auto src = downstream->get_blocked_request_buf(); + if (src->rleft()) { + auto dest = downstream->get_request_buf(); + src->remove(*dest); + if (http2session->resume_data(sd->dconn) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + downstream->ensure_downstream_wtimer(); + } + } + + if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) { + return 0; + } + + downstream->reset_downstream_rtimer(); + + return 0; + } + + if (frame->hd.type == NGHTTP2_SETTINGS && + (frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { + http2session->start_settings_timer(); + } + return 0; +} +} // namespace + +namespace { +int on_frame_not_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, int lib_error_code, + void *user_data) { + auto http2session = static_cast<Http2Session *>(user_data); + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, http2session) << "Failed to send control frame type=" + << static_cast<uint32_t>(frame->hd.type) + << ", lib_error_code=" << lib_error_code << ": " + << nghttp2_strerror(lib_error_code); + } + if (frame->hd.type != NGHTTP2_HEADERS || + lib_error_code == NGHTTP2_ERR_STREAM_CLOSED || + lib_error_code == NGHTTP2_ERR_STREAM_CLOSING) { + return 0; + } + + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!sd) { + return 0; + } + if (!sd->dconn) { + return 0; + } + auto downstream = sd->dconn->get_downstream(); + + if (lib_error_code == NGHTTP2_ERR_START_STREAM_NOT_ALLOWED) { + // Migrate to another downstream connection. + auto upstream = downstream->get_upstream(); + + if (upstream->on_downstream_reset(downstream, false)) { + // This should be done for h1 upstream only. Deleting + // ClientHandler for h2 upstream may lead to crash. + delete upstream->get_client_handler(); + } + + return 0; + } + + // To avoid stream hanging around, flag DownstreamState::MSG_RESET. + downstream->set_response_state(DownstreamState::MSG_RESET); + call_downstream_readcb(http2session, downstream); + + return 0; +} +} // namespace + +namespace { +constexpr auto PADDING = std::array<uint8_t, 256>{}; +} // namespace + +namespace { +int send_data_callback(nghttp2_session *session, nghttp2_frame *frame, + const uint8_t *framehd, size_t length, + nghttp2_data_source *source, void *user_data) { + auto http2session = static_cast<Http2Session *>(user_data); + auto sd = static_cast<StreamData *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + + if (sd == nullptr) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + auto dconn = sd->dconn; + auto downstream = dconn->get_downstream(); + auto input = downstream->get_request_buf(); + auto wb = http2session->get_request_buf(); + + size_t padlen = 0; + + wb->append(framehd, 9); + if (frame->data.padlen > 0) { + padlen = frame->data.padlen - 1; + wb->append(static_cast<uint8_t>(padlen)); + } + + input->remove(*wb, length); + + wb->append(PADDING.data(), padlen); + + if (input->rleft() == 0) { + downstream->disable_downstream_wtimer(); + } else { + downstream->reset_downstream_wtimer(); + } + + if (length > 0) { + // This is important because it will handle flow control + // stuff. + if (downstream->get_upstream()->resume_read(SHRPX_NO_BUFFER, downstream, + length) != 0) { + // In this case, downstream may be deleted. + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + // Here sd->dconn could be nullptr, because + // Upstream::resume_read() may delete downstream which will delete + // dconn. Is this still really true? + } + + return 0; +} +} // namespace + +nghttp2_session_callbacks *create_http2_downstream_callbacks() { + int rv; + nghttp2_session_callbacks *callbacks; + + rv = nghttp2_session_callbacks_new(&callbacks); + + if (rv != 0) { + return nullptr; + } + + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback); + + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); + + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, + on_frame_send_callback); + + nghttp2_session_callbacks_set_on_frame_not_send_callback( + callbacks, on_frame_not_send_callback); + + nghttp2_session_callbacks_set_on_header_callback2(callbacks, + on_header_callback2); + + nghttp2_session_callbacks_set_on_invalid_header_callback2( + callbacks, on_invalid_header_callback2); + + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, on_begin_headers_callback); + + nghttp2_session_callbacks_set_send_data_callback(callbacks, + send_data_callback); + + if (get_config()->padding) { + nghttp2_session_callbacks_set_select_padding_callback( + callbacks, http::select_padding_callback); + } + + return callbacks; +} + +int Http2Session::connection_made() { + int rv; + + state_ = Http2SessionState::CONNECTED; + + on_write_ = &Http2Session::downstream_write; + on_read_ = &Http2Session::downstream_read; + + if (addr_->tls) { + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len = 0; + + SSL_get0_alpn_selected(conn_.tls.ssl, &next_proto, &next_proto_len); + + if (!next_proto) { + downstream_failure(addr_, raddr_); + return -1; + } + + auto proto = StringRef{next_proto, next_proto_len}; + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Negotiated next protocol: " << proto; + } + if (!util::check_h2_is_selected(proto)) { + downstream_failure(addr_, raddr_); + return -1; + } + } + + auto config = get_config(); + auto &http2conf = config->http2; + + rv = nghttp2_session_client_new2(&session_, http2conf.downstream.callbacks, + this, http2conf.downstream.option); + + if (rv != 0) { + return -1; + } + + std::array<nghttp2_settings_entry, 5> entry; + size_t nentry = 3; + entry[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + entry[0].value = http2conf.downstream.max_concurrent_streams; + + entry[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + entry[1].value = http2conf.downstream.window_size; + + entry[2].settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; + entry[2].value = 1; + + if (http2conf.no_server_push || config->http2_proxy) { + entry[nentry].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; + entry[nentry].value = 0; + ++nentry; + } + + if (http2conf.downstream.decoder_dynamic_table_size != + NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { + entry[nentry].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; + entry[nentry].value = http2conf.downstream.decoder_dynamic_table_size; + ++nentry; + } + + rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry.data(), + nentry); + if (rv != 0) { + return -1; + } + + rv = nghttp2_session_set_local_window_size( + session_, NGHTTP2_FLAG_NONE, 0, + http2conf.downstream.connection_window_size); + if (rv != 0) { + return -1; + } + + reset_connection_check_timer(CONNCHK_TIMEOUT); + + submit_pending_requests(); + + signal_write(); + return 0; +} + +int Http2Session::do_read() { return read_(*this); } +int Http2Session::do_write() { return write_(*this); } + +int Http2Session::on_read(const uint8_t *data, size_t datalen) { + return on_read_(*this, data, datalen); +} + +int Http2Session::on_write() { return on_write_(*this); } + +int Http2Session::downstream_read(const uint8_t *data, size_t datalen) { + ssize_t rv; + + rv = nghttp2_session_mem_recv(session_, data, datalen); + if (rv < 0) { + SSLOG(ERROR, this) << "nghttp2_session_mem_recv() returned error: " + << nghttp2_strerror(rv); + return -1; + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "No more read/write for this HTTP2 session"; + } + return -1; + } + + signal_write(); + return 0; +} + +int Http2Session::downstream_write() { + for (;;) { + const uint8_t *data; + auto datalen = nghttp2_session_mem_send(session_, &data); + if (datalen < 0) { + SSLOG(ERROR, this) << "nghttp2_session_mem_send() returned error: " + << nghttp2_strerror(datalen); + return -1; + } + if (datalen == 0) { + break; + } + wb_.append(data, datalen); + + if (wb_.rleft() >= MAX_BUFFER_SIZE) { + break; + } + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "No more read/write for this session"; + } + return -1; + } + + return 0; +} + +void Http2Session::signal_write() { + switch (state_) { + case Http2SessionState::DISCONNECTED: + if (!ev_is_active(&initiate_connection_timer_)) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Start connecting to backend server"; + } + // Since the timer is set to 0., these will feed 2 events. We + // will stop the timer in the initiate_connection_timer_ to void + // 2nd event. + ev_timer_start(conn_.loop, &initiate_connection_timer_); + ev_feed_event(conn_.loop, &initiate_connection_timer_, 0); + } + break; + case Http2SessionState::CONNECTED: + conn_.wlimit.startw(); + break; + default: + break; + } +} + +struct ev_loop *Http2Session::get_loop() const { return conn_.loop; } + +ev_io *Http2Session::get_wev() { return &conn_.wev; } + +Http2SessionState Http2Session::get_state() const { return state_; } + +void Http2Session::set_state(Http2SessionState state) { state_ = state; } + +int Http2Session::terminate_session(uint32_t error_code) { + int rv; + rv = nghttp2_session_terminate_session(session_, error_code); + if (rv != 0) { + return -1; + } + return 0; +} + +SSL *Http2Session::get_ssl() const { return conn_.tls.ssl; } + +int Http2Session::consume(int32_t stream_id, size_t len) { + int rv; + + if (!session_) { + return 0; + } + + rv = nghttp2_session_consume(session_, stream_id, len); + + if (rv != 0) { + SSLOG(WARN, this) << "nghttp2_session_consume() returned error: " + << nghttp2_strerror(rv); + + return -1; + } + + return 0; +} + +bool Http2Session::can_push_request(const Downstream *downstream) const { + auto &req = downstream->request(); + return state_ == Http2SessionState::CONNECTED && + connection_check_state_ == ConnectionCheck::NONE && + (req.connect_proto == ConnectProto::NONE || settings_recved_); +} + +void Http2Session::start_checking_connection() { + if (state_ != Http2SessionState::CONNECTED || + connection_check_state_ != ConnectionCheck::REQUIRED) { + return; + } + connection_check_state_ = ConnectionCheck::STARTED; + + SSLOG(INFO, this) << "Start checking connection"; + // If connection is down, we may get error when writing data. Issue + // ping frame to see whether connection is alive. + nghttp2_submit_ping(session_, NGHTTP2_FLAG_NONE, nullptr); + + // set ping timeout and start timer again + reset_connection_check_timer(CONNCHK_PING_TIMEOUT); + + signal_write(); +} + +void Http2Session::reset_connection_check_timer(ev_tstamp t) { + connchk_timer_.repeat = t; + ev_timer_again(conn_.loop, &connchk_timer_); +} + +void Http2Session::reset_connection_check_timer_if_not_checking() { + if (connection_check_state_ != ConnectionCheck::NONE) { + return; + } + + reset_connection_check_timer(CONNCHK_TIMEOUT); +} + +void Http2Session::connection_alive() { + reset_connection_check_timer(CONNCHK_TIMEOUT); + + if (connection_check_state_ == ConnectionCheck::NONE) { + return; + } + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Connection alive"; + } + + connection_check_state_ = ConnectionCheck::NONE; + + submit_pending_requests(); +} + +void Http2Session::submit_pending_requests() { + for (auto dconn = dconns_.head; dconn; dconn = dconn->dlnext) { + auto downstream = dconn->get_downstream(); + + if (!downstream->get_request_pending() || + !downstream->request_submission_ready()) { + continue; + } + + auto &req = downstream->request(); + if (req.connect_proto != ConnectProto::NONE && !settings_recved_) { + continue; + } + + auto upstream = downstream->get_upstream(); + + if (dconn->push_request_headers() != 0) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "backend request failed"; + } + + upstream->on_downstream_abort_request(downstream, 400); + + continue; + } + + upstream->resume_read(SHRPX_NO_BUFFER, downstream, 0); + } +} + +void Http2Session::set_connection_check_state(ConnectionCheck state) { + connection_check_state_ = state; +} + +ConnectionCheck Http2Session::get_connection_check_state() const { + return connection_check_state_; +} + +int Http2Session::noop() { return 0; } + +int Http2Session::read_noop(const uint8_t *data, size_t datalen) { return 0; } + +int Http2Session::write_noop() { return 0; } + +int Http2Session::connected() { + auto sock_error = util::get_socket_error(conn_.fd); + if (sock_error != 0) { + SSLOG(WARN, this) << "Backend connect failed; addr=" + << util::to_numeric_addr(raddr_) + << ": errno=" << sock_error; + + downstream_failure(addr_, raddr_); + + return -1; + } + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Connection established"; + } + + // Reset timeout for write. Previously, we set timeout for connect. + conn_.wt.repeat = group_->shared_addr->timeout.write; + ev_timer_again(conn_.loop, &conn_.wt); + + conn_.rlimit.startw(); + conn_.again_rt(); + + read_ = &Http2Session::read_clear; + write_ = &Http2Session::write_clear; + + if (state_ == Http2SessionState::PROXY_CONNECTING) { + return do_write(); + } + + if (conn_.tls.ssl) { + read_ = &Http2Session::tls_handshake; + write_ = &Http2Session::tls_handshake; + + return do_write(); + } + + if (connection_made() != 0) { + state_ = Http2SessionState::CONNECT_FAILING; + return -1; + } + + return 0; +} + +int Http2Session::read_clear() { + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<uint8_t, 16_k> buf; + + for (;;) { + auto nread = conn_.read_clear(buf.data(), buf.size()); + + if (nread == 0) { + return write_clear(); + } + + if (nread < 0) { + return nread; + } + + if (on_read(buf.data(), nread) != 0) { + return -1; + } + } +} + +int Http2Session::write_clear() { + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<struct iovec, MAX_WR_IOVCNT> iov; + + for (;;) { + if (wb_.rleft() > 0) { + auto iovcnt = wb_.riovec(iov.data(), iov.size()); + auto nwrite = conn_.writev_clear(iov.data(), iovcnt); + + if (nwrite == 0) { + return 0; + } + + if (nwrite < 0) { + // We may have pending data in receive buffer which may + // contain part of response body. So keep reading. Invoke + // read event to get read(2) error just in case. + ev_feed_event(conn_.loop, &conn_.rev, EV_READ); + write_ = &Http2Session::write_void; + break; + } + + wb_.drain(nwrite); + continue; + } + + if (on_write() != 0) { + return -1; + } + if (wb_.rleft() == 0) { + break; + } + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + return 0; +} + +int Http2Session::tls_handshake() { + conn_.last_read = std::chrono::steady_clock::now(); + + ERR_clear_error(); + + auto rv = conn_.tls_handshake(); + + if (rv == SHRPX_ERR_INPROGRESS) { + return 0; + } + + if (rv < 0) { + downstream_failure(addr_, raddr_); + + return rv; + } + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "SSL/TLS handshake completed"; + } + + if (!get_config()->tls.insecure && + tls::check_cert(conn_.tls.ssl, addr_, raddr_) != 0) { + downstream_failure(addr_, raddr_); + + return -1; + } + + read_ = &Http2Session::read_tls; + write_ = &Http2Session::write_tls; + + if (connection_made() != 0) { + state_ = Http2SessionState::CONNECT_FAILING; + return -1; + } + + return 0; +} + +int Http2Session::read_tls() { + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<uint8_t, 16_k> buf; + + ERR_clear_error(); + + for (;;) { + auto nread = conn_.read_tls(buf.data(), buf.size()); + + if (nread == 0) { + return write_tls(); + } + + if (nread < 0) { + return nread; + } + + if (on_read(buf.data(), nread) != 0) { + return -1; + } + } +} + +int Http2Session::write_tls() { + conn_.last_read = std::chrono::steady_clock::now(); + + ERR_clear_error(); + + struct iovec iov; + + for (;;) { + if (wb_.rleft() > 0) { + auto iovcnt = wb_.riovec(&iov, 1); + if (iovcnt != 1) { + assert(0); + return -1; + } + auto nwrite = conn_.write_tls(iov.iov_base, iov.iov_len); + + if (nwrite == 0) { + return 0; + } + + if (nwrite < 0) { + // We may have pending data in receive buffer which may + // contain part of response body. So keep reading. Invoke + // read event to get read(2) error just in case. + ev_feed_event(conn_.loop, &conn_.rev, EV_READ); + write_ = &Http2Session::write_void; + break; + } + + wb_.drain(nwrite); + + continue; + } + + if (on_write() != 0) { + return -1; + } + if (wb_.rleft() == 0) { + conn_.start_tls_write_idle(); + break; + } + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + return 0; +} + +int Http2Session::write_void() { + conn_.wlimit.stopw(); + return 0; +} + +bool Http2Session::should_hard_fail() const { + switch (state_) { + case Http2SessionState::PROXY_CONNECTING: + case Http2SessionState::PROXY_FAILED: + return true; + case Http2SessionState::DISCONNECTED: { + const auto &proxy = get_config()->downstream_http_proxy; + return !proxy.host.empty(); + } + default: + return false; + } +} + +DownstreamAddr *Http2Session::get_addr() const { return addr_; } + +int Http2Session::handle_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + auto upstream = downstream->get_upstream(); + if (!upstream->push_enabled()) { + return -1; + } + + auto promised_downstream = + upstream->on_downstream_push_promise(downstream, promised_stream_id); + if (!promised_downstream) { + return -1; + } + + // Now we have Downstream object for pushed stream. + // promised_downstream->get_stream() still returns 0. + + auto handler = upstream->get_client_handler(); + + auto promised_dconn = std::make_unique<Http2DownstreamConnection>(this); + promised_dconn->set_client_handler(handler); + + auto ptr = promised_dconn.get(); + + if (promised_downstream->attach_downstream_connection( + std::move(promised_dconn)) != 0) { + return -1; + } + + auto promised_sd = std::make_unique<StreamData>(); + + nghttp2_session_set_stream_user_data(session_, promised_stream_id, + promised_sd.get()); + + ptr->attach_stream_data(promised_sd.get()); + streams_.append(promised_sd.release()); + + return 0; +} + +int Http2Session::handle_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + auto &promised_req = promised_downstream->request(); + + auto &promised_balloc = promised_downstream->get_block_allocator(); + + auto authority = promised_req.fs.header(http2::HD__AUTHORITY); + auto path = promised_req.fs.header(http2::HD__PATH); + auto method = promised_req.fs.header(http2::HD__METHOD); + auto scheme = promised_req.fs.header(http2::HD__SCHEME); + + if (!authority) { + authority = promised_req.fs.header(http2::HD_HOST); + } + + auto method_token = http2::lookup_method_token(method->value); + if (method_token == -1) { + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Unrecognized method: " << method->value; + } + + return -1; + } + + // TODO Rewrite authority if we enabled rewrite host. But we + // really don't know how to rewrite host. Should we use the same + // host in associated stream? + if (authority) { + promised_req.authority = authority->value; + } + promised_req.method = method_token; + // libnghttp2 ensures that we don't have CONNECT method in + // PUSH_PROMISE, and guarantees that :scheme exists. + if (scheme) { + promised_req.scheme = scheme->value; + } + + // For server-wide OPTIONS request, path is empty. + if (method_token != HTTP_OPTIONS || path->value != "*") { + promised_req.path = http2::rewrite_clean_path(promised_balloc, path->value); + } + + promised_downstream->inspect_http2_request(); + + auto upstream = promised_downstream->get_upstream(); + + promised_downstream->set_request_state(DownstreamState::MSG_COMPLETE); + promised_downstream->set_request_header_sent(true); + + if (upstream->on_downstream_push_promise_complete(downstream, + promised_downstream) != 0) { + return -1; + } + + return 0; +} + +size_t Http2Session::get_num_dconns() const { return dconns_.size(); } + +bool Http2Session::max_concurrency_reached(size_t extra) const { + if (!session_) { + return dconns_.size() + extra >= 100; + } + + // If session does not allow further requests, it effectively means + // that maximum concurrency is reached. + return !nghttp2_session_check_request_allowed(session_) || + dconns_.size() + extra >= + nghttp2_session_get_remote_settings( + session_, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS); +} + +const std::shared_ptr<DownstreamAddrGroup> & +Http2Session::get_downstream_addr_group() const { + return group_; +} + +void Http2Session::add_to_extra_freelist() { + if (freelist_zone_ != FreelistZone::NONE) { + return; + } + + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Append to http2_extra_freelist, addr=" << addr_ + << ", freelist.size=" + << addr_->http2_extra_freelist.size(); + } + + freelist_zone_ = FreelistZone::EXTRA; + addr_->http2_extra_freelist.append(this); +} + +void Http2Session::remove_from_freelist() { + switch (freelist_zone_) { + case FreelistZone::NONE: + return; + case FreelistZone::EXTRA: + if (LOG_ENABLED(INFO)) { + SSLOG(INFO, this) << "Remove from http2_extra_freelist, addr=" << addr_ + << ", freelist.size=" + << addr_->http2_extra_freelist.size(); + } + addr_->http2_extra_freelist.remove(this); + break; + case FreelistZone::GONE: + return; + } + + freelist_zone_ = FreelistZone::NONE; +} + +void Http2Session::exclude_from_scheduling() { + remove_from_freelist(); + freelist_zone_ = FreelistZone::GONE; +} + +DefaultMemchunks *Http2Session::get_request_buf() { return &wb_; } + +void Http2Session::on_timeout() { + switch (state_) { + case Http2SessionState::PROXY_CONNECTING: { + auto worker_blocker = worker_->get_connect_blocker(); + worker_blocker->on_failure(); + break; + } + case Http2SessionState::CONNECTING: + SSLOG(WARN, this) << "Connect time out; addr=" + << util::to_numeric_addr(raddr_); + + downstream_failure(addr_, raddr_); + break; + default: + break; + } +} + +void Http2Session::check_retire() { + if (!group_->retired) { + return; + } + + ev_prepare_stop(conn_.loop, &prep_); + + if (!session_) { + return; + } + + auto last_stream_id = nghttp2_session_get_last_proc_stream_id(session_); + nghttp2_submit_goaway(session_, NGHTTP2_FLAG_NONE, last_stream_id, + NGHTTP2_NO_ERROR, nullptr, 0); + + signal_write(); +} + +const Address *Http2Session::get_raddr() const { return raddr_; } + +void Http2Session::on_settings_received(const nghttp2_frame *frame) { + // TODO This effectively disallows nghttpx to change its behaviour + // based on the 2nd SETTINGS. + if (settings_recved_) { + return; + } + + settings_recved_ = true; + + for (size_t i = 0; i < frame->settings.niv; ++i) { + auto &ent = frame->settings.iv[i]; + if (ent.settings_id == NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL) { + allow_connect_proto_ = true; + break; + } + } + + submit_pending_requests(); +} + +bool Http2Session::get_allow_connect_proto() const { + return allow_connect_proto_; +} + +} // namespace shrpx diff --git a/src/shrpx_http2_session.h b/src/shrpx_http2_session.h new file mode 100644 index 0000000..31b2545 --- /dev/null +++ b/src/shrpx_http2_session.h @@ -0,0 +1,296 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP2_SESSION_H +#define SHRPX_HTTP2_SESSION_H + +#include "shrpx.h" + +#include <unordered_set> +#include <memory> + +#include <openssl/ssl.h> + +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#include "llhttp.h" + +#include "shrpx_connection.h" +#include "buffer.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +class Http2DownstreamConnection; +class Worker; +class Downstream; +struct DownstreamAddrGroup; +struct DownstreamAddr; +struct DNSQuery; + +struct StreamData { + StreamData *dlnext, *dlprev; + Http2DownstreamConnection *dconn; +}; + +enum class FreelistZone { + // Http2Session object is not linked in any freelist. + NONE, + // Http2Session object is linked in address scope + // http2_extra_freelist. + EXTRA, + // Http2Session object is about to be deleted, and it does not + // belong to any linked list. + GONE +}; + +enum class Http2SessionState { + // Disconnected + DISCONNECTED, + // Connecting proxy and making CONNECT request + PROXY_CONNECTING, + // Tunnel is established with proxy + PROXY_CONNECTED, + // Establishing tunnel is failed + PROXY_FAILED, + // Connecting to downstream and/or performing SSL/TLS handshake + CONNECTING, + // Connected to downstream + CONNECTED, + // Connection is started to fail + CONNECT_FAILING, + // Resolving host name + RESOLVING_NAME, +}; + +enum class ConnectionCheck { + // Connection checking is not required + NONE, + // Connection checking is required + REQUIRED, + // Connection checking has been started + STARTED, +}; + +class Http2Session { +public: + Http2Session(struct ev_loop *loop, SSL_CTX *ssl_ctx, Worker *worker, + const std::shared_ptr<DownstreamAddrGroup> &group, + DownstreamAddr *addr); + ~Http2Session(); + + // If hard is true, all pending requests are abandoned and + // associated ClientHandlers will be deleted. + int disconnect(bool hard = false); + int initiate_connection(); + int resolve_name(); + + void add_downstream_connection(Http2DownstreamConnection *dconn); + void remove_downstream_connection(Http2DownstreamConnection *dconn); + + void remove_stream_data(StreamData *sd); + + int submit_request(Http2DownstreamConnection *dconn, const nghttp2_nv *nva, + size_t nvlen, const nghttp2_data_provider *data_prd); + + int submit_rst_stream(int32_t stream_id, uint32_t error_code); + + int terminate_session(uint32_t error_code); + + nghttp2_session *get_session() const; + + int resume_data(Http2DownstreamConnection *dconn); + + int connection_made(); + + int do_read(); + int do_write(); + + int on_read(const uint8_t *data, size_t datalen); + int on_write(); + + int connected(); + int read_clear(); + int write_clear(); + int tls_handshake(); + int read_tls(); + int write_tls(); + // This is a special write function which just stop write event + // watcher. + int write_void(); + + int downstream_read_proxy(const uint8_t *data, size_t datalen); + int downstream_connect_proxy(); + + int downstream_read(const uint8_t *data, size_t datalen); + int downstream_write(); + + int noop(); + int read_noop(const uint8_t *data, size_t datalen); + int write_noop(); + + void signal_write(); + + struct ev_loop *get_loop() const; + + ev_io *get_wev(); + + Http2SessionState get_state() const; + void set_state(Http2SessionState state); + + void start_settings_timer(); + void stop_settings_timer(); + + SSL *get_ssl() const; + + int consume(int32_t stream_id, size_t len); + + // Returns true if request can be issued on downstream connection. + bool can_push_request(const Downstream *downstream) const; + // Initiates the connection checking if downstream connection has + // been established and connection checking is required. + void start_checking_connection(); + // Resets connection check timer to timeout |t|. After timeout, we + // require connection checking. If connection checking is already + // enabled, this timeout is for PING ACK timeout. + void reset_connection_check_timer(ev_tstamp t); + void reset_connection_check_timer_if_not_checking(); + // Signals that connection is alive. Internally + // reset_connection_check_timer() is called. + void connection_alive(); + // Change connection check state. + void set_connection_check_state(ConnectionCheck state); + ConnectionCheck get_connection_check_state() const; + + bool should_hard_fail() const; + + void submit_pending_requests(); + + DownstreamAddr *get_addr() const; + + const std::shared_ptr<DownstreamAddrGroup> &get_downstream_addr_group() const; + + int handle_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + int handle_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + + // Returns number of downstream connections, including pushed + // streams. + size_t get_num_dconns() const; + + // Adds to group scope http2_avail_freelist. + void add_to_avail_freelist(); + // Adds to address scope http2_extra_freelist. + void add_to_extra_freelist(); + + // Removes this object from any freelist. If this object is not + // linked from any freelist, this function does nothing. + void remove_from_freelist(); + + // Removes this object form any freelist, and marks this object as + // not schedulable. + void exclude_from_scheduling(); + + // Returns true if the maximum concurrency is reached. In other + // words, the number of currently participated streams in this + // session is equal or greater than the max concurrent streams limit + // advertised by server. If |extra| is nonzero, it is added to the + // number of current concurrent streams when comparing against + // server initiated concurrency limit. + bool max_concurrency_reached(size_t extra = 0) const; + + DefaultMemchunks *get_request_buf(); + + void on_timeout(); + + // This is called periodically using ev_prepare watcher, and if + // group_ is retired (backend has been replaced), send GOAWAY to + // shutdown the connection. + void check_retire(); + + // Returns address used to connect to backend. Could be nullptr. + const Address *get_raddr() const; + + // This is called when SETTINGS frame without ACK flag set is + // received. + void on_settings_received(const nghttp2_frame *frame); + + bool get_allow_connect_proto() const; + + using ReadBuf = Buffer<8_k>; + + Http2Session *dlnext, *dlprev; + +private: + Connection conn_; + DefaultMemchunks wb_; + ev_timer settings_timer_; + // This timer has 2 purpose: when it first timeout, set + // connection_check_state_ = ConnectionCheck::REQUIRED. After + // connection check has started, this timer is started again and + // traps PING ACK timeout. + ev_timer connchk_timer_; + // timer to initiate connection. usually, this fires immediately. + ev_timer initiate_connection_timer_; + ev_prepare prep_; + DList<Http2DownstreamConnection> dconns_; + DList<StreamData> streams_; + std::function<int(Http2Session &)> read_, write_; + std::function<int(Http2Session &, const uint8_t *, size_t)> on_read_; + std::function<int(Http2Session &)> on_write_; + // Used to parse the response from HTTP proxy + std::unique_ptr<llhttp_t> proxy_htp_; + Worker *worker_; + // NULL if no TLS is configured + SSL_CTX *ssl_ctx_; + std::shared_ptr<DownstreamAddrGroup> group_; + // Address of remote endpoint + DownstreamAddr *addr_; + nghttp2_session *session_; + // Actual remote address used to contact backend. This is initially + // nullptr, and may point to either &addr_->addr, + // resolved_addr_.get(), or HTTP proxy's address structure. + const Address *raddr_; + // Resolved IP address if dns parameter is used + std::unique_ptr<Address> resolved_addr_; + std::unique_ptr<DNSQuery> dns_query_; + Http2SessionState state_; + ConnectionCheck connection_check_state_; + FreelistZone freelist_zone_; + // true if SETTINGS without ACK is received from peer. + bool settings_recved_; + // true if peer enables RFC 8441 CONNECT protocol. + bool allow_connect_proto_; +}; + +nghttp2_session_callbacks *create_http2_downstream_callbacks(); + +} // namespace shrpx + +#endif // SHRPX_HTTP2_SESSION_H diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc new file mode 100644 index 0000000..c9f8a8c --- /dev/null +++ b/src/shrpx_http2_upstream.cc @@ -0,0 +1,2404 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_http2_upstream.h" + +#include <netinet/tcp.h> +#include <assert.h> +#include <cerrno> +#include <sstream> + +#include "shrpx_client_handler.h" +#include "shrpx_https_upstream.h" +#include "shrpx_downstream.h" +#include "shrpx_downstream_connection.h" +#include "shrpx_config.h" +#include "shrpx_http.h" +#include "shrpx_worker.h" +#include "shrpx_http2_session.h" +#include "shrpx_log.h" +#ifdef HAVE_MRUBY +# include "shrpx_mruby.h" +#endif // HAVE_MRUBY +#include "http2.h" +#include "util.h" +#include "base64.h" +#include "app_helper.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +constexpr size_t MAX_BUFFER_SIZE = 32_k; +} // namespace + +namespace { +int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, + uint32_t error_code, void *user_data) { + auto upstream = static_cast<Http2Upstream *>(user_data); + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Stream stream_id=" << stream_id + << " is being closed"; + } + + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + + if (!downstream) { + return 0; + } + + auto &req = downstream->request(); + + upstream->consume(stream_id, req.unconsumed_body_length); + + req.unconsumed_body_length = 0; + + if (downstream->get_request_state() == DownstreamState::CONNECT_FAIL) { + upstream->remove_downstream(downstream); + // downstream was deleted + + return 0; + } + + if (downstream->can_detach_downstream_connection()) { + // Keep-alive + downstream->detach_downstream_connection(); + } + + downstream->set_request_state(DownstreamState::STREAM_CLOSED); + + // At this point, downstream read may be paused. + + // If shrpx_downstream::push_request_headers() failed, the + // error is handled here. + upstream->remove_downstream(downstream); + // downstream was deleted + + // How to test this case? Request sufficient large download + // and make client send RST_STREAM after it gets first DATA + // frame chunk. + + return 0; +} +} // namespace + +int Http2Upstream::upgrade_upstream(HttpsUpstream *http) { + int rv; + + auto &balloc = http->get_downstream()->get_block_allocator(); + + auto http2_settings = http->get_downstream()->get_http2_settings(); + http2_settings = util::to_base64(balloc, http2_settings); + + auto settings_payload = base64::decode(balloc, std::begin(http2_settings), + std::end(http2_settings)); + + rv = nghttp2_session_upgrade2( + session_, settings_payload.byte(), settings_payload.size(), + http->get_downstream()->request().method == HTTP_HEAD, nullptr); + if (rv != 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "nghttp2_session_upgrade() returned error: " + << nghttp2_strerror(rv); + } + return -1; + } + pre_upstream_.reset(http); + auto downstream = http->pop_downstream(); + downstream->reset_upstream(this); + downstream->set_stream_id(1); + downstream->reset_upstream_rtimer(); + downstream->set_stream_id(1); + + auto ptr = downstream.get(); + + nghttp2_session_set_stream_user_data(session_, 1, ptr); + downstream_queue_.add_pending(std::move(downstream)); + downstream_queue_.mark_active(ptr); + + // TODO This might not be necessary + handler_->stop_read_timer(); + + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Connection upgraded to HTTP/2"; + } + + return 0; +} + +void Http2Upstream::start_settings_timer() { + ev_timer_start(handler_->get_loop(), &settings_timer_); +} + +void Http2Upstream::stop_settings_timer() { + ev_timer_stop(handler_->get_loop(), &settings_timer_); +} + +namespace { +int on_header_callback2(nghttp2_session *session, const nghttp2_frame *frame, + nghttp2_rcbuf *name, nghttp2_rcbuf *value, + uint8_t flags, void *user_data) { + auto namebuf = nghttp2_rcbuf_get_buf(name); + auto valuebuf = nghttp2_rcbuf_get_buf(value); + auto config = get_config(); + + if (config->http2.upstream.debug.frame_debug) { + verbose_on_header_callback(session, frame, namebuf.base, namebuf.len, + valuebuf.base, valuebuf.len, flags, user_data); + } + if (frame->hd.type != NGHTTP2_HEADERS) { + return 0; + } + auto upstream = static_cast<Http2Upstream *>(user_data); + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!downstream) { + return 0; + } + + auto &req = downstream->request(); + + auto &httpconf = config->http; + + if (req.fs.buffer_size() + namebuf.len + valuebuf.len > + httpconf.request_header_field_buffer || + req.fs.num_fields() >= httpconf.max_request_header_fields) { + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Too large or many header field size=" + << req.fs.buffer_size() + namebuf.len + valuebuf.len + << ", num=" << req.fs.num_fields() + 1; + } + + // just ignore header fields if this is trailer part. + if (frame->headers.cat == NGHTTP2_HCAT_HEADERS) { + return 0; + } + + if (upstream->error_reply(downstream, 431) != 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + return 0; + } + + auto token = http2::lookup_token(namebuf.base, namebuf.len); + auto no_index = flags & NGHTTP2_NV_FLAG_NO_INDEX; + + downstream->add_rcbuf(name); + downstream->add_rcbuf(value); + + if (frame->headers.cat == NGHTTP2_HCAT_HEADERS) { + // just store header fields for trailer part + req.fs.add_trailer_token(StringRef{namebuf.base, namebuf.len}, + StringRef{valuebuf.base, valuebuf.len}, no_index, + token); + return 0; + } + + req.fs.add_header_token(StringRef{namebuf.base, namebuf.len}, + StringRef{valuebuf.base, valuebuf.len}, no_index, + token); + return 0; +} +} // namespace + +namespace { +int on_invalid_header_callback2(nghttp2_session *session, + const nghttp2_frame *frame, nghttp2_rcbuf *name, + nghttp2_rcbuf *value, uint8_t flags, + void *user_data) { + auto upstream = static_cast<Http2Upstream *>(user_data); + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!downstream) { + return 0; + } + + if (LOG_ENABLED(INFO)) { + auto namebuf = nghttp2_rcbuf_get_buf(name); + auto valuebuf = nghttp2_rcbuf_get_buf(value); + + ULOG(INFO, upstream) << "Invalid header field for stream_id=" + << frame->hd.stream_id << ": name=[" + << StringRef{namebuf.base, namebuf.len} << "], value=[" + << StringRef{valuebuf.base, valuebuf.len} << "]"; + } + + upstream->rst_stream(downstream, NGHTTP2_PROTOCOL_ERROR); + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; +} +} // namespace + +namespace { +int on_begin_headers_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + auto upstream = static_cast<Http2Upstream *>(user_data); + + if (frame->headers.cat != NGHTTP2_HCAT_REQUEST) { + return 0; + } + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Received upstream request HEADERS stream_id=" + << frame->hd.stream_id; + } + + upstream->on_start_request(frame); + + return 0; +} +} // namespace + +void Http2Upstream::on_start_request(const nghttp2_frame *frame) { + auto downstream = std::make_unique<Downstream>(this, handler_->get_mcpool(), + frame->hd.stream_id); + nghttp2_session_set_stream_user_data(session_, frame->hd.stream_id, + downstream.get()); + + downstream->reset_upstream_rtimer(); + + handler_->repeat_read_timer(); + + auto &req = downstream->request(); + + // Although, we deprecated minor version from HTTP/2, we supply + // minor version 0 to use via header field in a conventional way. + req.http_major = 2; + req.http_minor = 0; + + add_pending_downstream(std::move(downstream)); + + ++num_requests_; + + auto config = get_config(); + auto &httpconf = config->http; + if (httpconf.max_requests <= num_requests_) { + start_graceful_shutdown(); + } +} + +int Http2Upstream::on_request_headers(Downstream *downstream, + const nghttp2_frame *frame) { + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + auto &req = downstream->request(); + req.tstamp = lgconf->tstamp; + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + + auto &nva = req.fs.headers(); + + if (LOG_ENABLED(INFO)) { + std::stringstream ss; + for (auto &nv : nva) { + if (nv.name == "authorization") { + ss << TTY_HTTP_HD << nv.name << TTY_RST << ": <redacted>\n"; + continue; + } + ss << TTY_HTTP_HD << nv.name << TTY_RST << ": " << nv.value << "\n"; + } + ULOG(INFO, this) << "HTTP request headers. stream_id=" + << downstream->get_stream_id() << "\n" + << ss.str(); + } + + auto config = get_config(); + auto &dump = config->http2.upstream.debug.dump; + + if (dump.request_header) { + http2::dump_nv(dump.request_header, nva); + } + + auto content_length = req.fs.header(http2::HD_CONTENT_LENGTH); + if (content_length) { + // libnghttp2 guarantees this can be parsed + req.fs.content_length = util::parse_uint(content_length->value); + } + + // presence of mandatory header fields are guaranteed by libnghttp2. + auto authority = req.fs.header(http2::HD__AUTHORITY); + auto path = req.fs.header(http2::HD__PATH); + auto method = req.fs.header(http2::HD__METHOD); + auto scheme = req.fs.header(http2::HD__SCHEME); + + auto method_token = http2::lookup_method_token(method->value); + if (method_token == -1) { + if (error_reply(downstream, 501) != 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; + } + + auto faddr = handler_->get_upstream_addr(); + + // For HTTP/2 proxy, we require :authority. + if (method_token != HTTP_CONNECT && config->http2_proxy && + faddr->alt_mode == UpstreamAltMode::NONE && !authority) { + rst_stream(downstream, NGHTTP2_PROTOCOL_ERROR); + return 0; + } + + req.method = method_token; + if (scheme) { + req.scheme = scheme->value; + } + + // nghttp2 library guarantees either :authority or host exist + if (!authority) { + req.no_authority = true; + authority = req.fs.header(http2::HD_HOST); + } + + if (authority) { + req.authority = authority->value; + } + + if (path) { + if (method_token == HTTP_OPTIONS && + path->value == StringRef::from_lit("*")) { + // Server-wide OPTIONS request. Path is empty. + } else if (config->http2_proxy && + faddr->alt_mode == UpstreamAltMode::NONE) { + req.path = path->value; + } else { + req.path = http2::rewrite_clean_path(downstream->get_block_allocator(), + path->value); + } + } + + auto connect_proto = req.fs.header(http2::HD__PROTOCOL); + if (connect_proto) { + if (connect_proto->value != "websocket") { + if (error_reply(downstream, 400) != 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; + } + req.connect_proto = ConnectProto::WEBSOCKET; + } + + if (!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) { + req.http2_expect_body = true; + } else if (req.fs.content_length == -1) { + // If END_STREAM flag is set to HEADERS frame, we are sure that + // content-length is 0. + req.fs.content_length = 0; + } + + downstream->inspect_http2_request(); + + downstream->set_request_state(DownstreamState::HEADER_COMPLETE); + + if (config->http.require_http_scheme && + !http::check_http_scheme(req.scheme, handler_->get_ssl() != nullptr)) { + if (error_reply(downstream, 400) != 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; + } + +#ifdef HAVE_MRUBY + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; + } +#endif // HAVE_MRUBY + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + downstream->disable_upstream_rtimer(); + + downstream->set_request_state(DownstreamState::MSG_COMPLETE); + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + + start_downstream(downstream); + + return 0; +} + +void Http2Upstream::start_downstream(Downstream *downstream) { + if (downstream_queue_.can_activate(downstream->request().authority)) { + initiate_downstream(downstream); + return; + } + + downstream_queue_.mark_blocked(downstream); +} + +void Http2Upstream::initiate_downstream(Downstream *downstream) { + int rv; + +#ifdef HAVE_MRUBY + DownstreamConnection *dconn_ptr; +#endif // HAVE_MRUBY + + for (;;) { + auto dconn = handler_->get_downstream_connection(rv, downstream); + if (!dconn) { + if (rv == SHRPX_ERR_TLS_REQUIRED) { + rv = redirect_to_https(downstream); + } else { + rv = error_reply(downstream, 502); + } + if (rv != 0) { + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + + downstream->set_request_state(DownstreamState::CONNECT_FAIL); + downstream_queue_.mark_failure(downstream); + + return; + } + +#ifdef HAVE_MRUBY + dconn_ptr = dconn.get(); +#endif // HAVE_MRUBY + rv = downstream->attach_downstream_connection(std::move(dconn)); + if (rv == 0) { + break; + } + } + +#ifdef HAVE_MRUBY + const auto &group = dconn_ptr->get_downstream_addr_group(); + if (group) { + const auto &mruby_ctx = group->shared_addr->mruby_ctx; + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + + downstream_queue_.mark_failure(downstream); + + return; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return; + } + } +#endif // HAVE_MRUBY + + rv = downstream->push_request_headers(); + if (rv != 0) { + + if (error_reply(downstream, 502) != 0) { + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + + downstream_queue_.mark_failure(downstream); + + return; + } + + downstream_queue_.mark_active(downstream); + + auto &req = downstream->request(); + if (!req.http2_expect_body) { + rv = downstream->end_upload_data(); + if (rv != 0) { + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + } + + return; +} + +namespace { +int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + if (get_config()->http2.upstream.debug.frame_debug) { + verbose_on_frame_recv_callback(session, frame, user_data); + } + auto upstream = static_cast<Http2Upstream *>(user_data); + auto handler = upstream->get_client_handler(); + + switch (frame->hd.type) { + case NGHTTP2_DATA: { + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!downstream) { + return 0; + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + downstream->disable_upstream_rtimer(); + + if (downstream->end_upload_data() != 0) { + if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE) { + upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + } + + downstream->set_request_state(DownstreamState::MSG_COMPLETE); + } + + return 0; + } + case NGHTTP2_HEADERS: { + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (!downstream) { + return 0; + } + + if (frame->headers.cat == NGHTTP2_HCAT_REQUEST) { + downstream->reset_upstream_rtimer(); + + handler->stop_read_timer(); + + return upstream->on_request_headers(downstream, frame); + } + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + downstream->disable_upstream_rtimer(); + + if (downstream->end_upload_data() != 0) { + if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE) { + upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + } + + downstream->set_request_state(DownstreamState::MSG_COMPLETE); + } + + return 0; + } + case NGHTTP2_SETTINGS: + if ((frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { + return 0; + } + upstream->stop_settings_timer(); + return 0; + case NGHTTP2_GOAWAY: + if (LOG_ENABLED(INFO)) { + auto debug_data = util::ascii_dump(frame->goaway.opaque_data, + frame->goaway.opaque_data_len); + + ULOG(INFO, upstream) << "GOAWAY received: last-stream-id=" + << frame->goaway.last_stream_id + << ", error_code=" << frame->goaway.error_code + << ", debug_data=" << debug_data; + } + return 0; + default: + return 0; + } +} +} // namespace + +namespace { +int on_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, + int32_t stream_id, const uint8_t *data, + size_t len, void *user_data) { + auto upstream = static_cast<Http2Upstream *>(user_data); + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + + if (!downstream) { + if (upstream->consume(stream_id, len) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return 0; + } + + downstream->reset_upstream_rtimer(); + + if (downstream->push_upload_data_chunk(data, len) != 0) { + if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE) { + upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + + if (upstream->consume(stream_id, len) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return 0; + } + + return 0; +} +} // namespace + +namespace { +int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + if (get_config()->http2.upstream.debug.frame_debug) { + verbose_on_frame_send_callback(session, frame, user_data); + } + auto upstream = static_cast<Http2Upstream *>(user_data); + auto handler = upstream->get_client_handler(); + + switch (frame->hd.type) { + case NGHTTP2_DATA: + case NGHTTP2_HEADERS: { + if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) == 0) { + return 0; + } + // RST_STREAM if request is still incomplete. + auto stream_id = frame->hd.stream_id; + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, stream_id)); + + if (!downstream) { + return 0; + } + + // For tunneling, issue RST_STREAM to finish the stream. + if (downstream->get_upgraded() || + nghttp2_session_get_stream_remote_close(session, stream_id) == 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) + << "Send RST_STREAM to " + << (downstream->get_upgraded() ? "tunneled " : "") + << "stream stream_id=" << downstream->get_stream_id() + << " to finish off incomplete request"; + } + + upstream->rst_stream(downstream, NGHTTP2_NO_ERROR); + } + + return 0; + } + case NGHTTP2_SETTINGS: + if ((frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { + upstream->start_settings_timer(); + } + return 0; + case NGHTTP2_PUSH_PROMISE: { + auto promised_stream_id = frame->push_promise.promised_stream_id; + + if (nghttp2_session_get_stream_user_data(session, promised_stream_id)) { + // In case of push from backend, downstream object was already + // created. + return 0; + } + + auto promised_downstream = std::make_unique<Downstream>( + upstream, handler->get_mcpool(), promised_stream_id); + auto &req = promised_downstream->request(); + + // As long as we use nghttp2_session_mem_send(), setting stream + // user data here should not fail. This is because this callback + // is called just after frame was serialized. So no worries about + // hanging Downstream. + nghttp2_session_set_stream_user_data(session, promised_stream_id, + promised_downstream.get()); + + promised_downstream->set_assoc_stream_id(frame->hd.stream_id); + promised_downstream->disable_upstream_rtimer(); + + req.http_major = 2; + req.http_minor = 0; + + req.fs.content_length = 0; + req.http2_expect_body = false; + + auto &promised_balloc = promised_downstream->get_block_allocator(); + + for (size_t i = 0; i < frame->push_promise.nvlen; ++i) { + auto &nv = frame->push_promise.nva[i]; + + auto name = + make_string_ref(promised_balloc, StringRef{nv.name, nv.namelen}); + auto value = + make_string_ref(promised_balloc, StringRef{nv.value, nv.valuelen}); + + auto token = http2::lookup_token(nv.name, nv.namelen); + switch (token) { + case http2::HD__METHOD: + req.method = http2::lookup_method_token(value); + break; + case http2::HD__SCHEME: + req.scheme = value; + break; + case http2::HD__AUTHORITY: + req.authority = value; + break; + case http2::HD__PATH: + req.path = http2::rewrite_clean_path(promised_balloc, value); + break; + } + req.fs.add_header_token(name, value, nv.flags & NGHTTP2_NV_FLAG_NO_INDEX, + token); + } + + promised_downstream->inspect_http2_request(); + + promised_downstream->set_request_state(DownstreamState::MSG_COMPLETE); + + // a bit weird but start_downstream() expects that given + // downstream is in pending queue. + auto ptr = promised_downstream.get(); + upstream->add_pending_downstream(std::move(promised_downstream)); + +#ifdef HAVE_MRUBY + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(ptr) != 0) { + if (upstream->error_reply(ptr, 500) != 0) { + upstream->rst_stream(ptr, NGHTTP2_INTERNAL_ERROR); + return 0; + } + return 0; + } +#endif // HAVE_MRUBY + + upstream->start_downstream(ptr); + + return 0; + } + case NGHTTP2_GOAWAY: + if (LOG_ENABLED(INFO)) { + auto debug_data = util::ascii_dump(frame->goaway.opaque_data, + frame->goaway.opaque_data_len); + + ULOG(INFO, upstream) << "Sending GOAWAY: last-stream-id=" + << frame->goaway.last_stream_id + << ", error_code=" << frame->goaway.error_code + << ", debug_data=" << debug_data; + } + return 0; + default: + return 0; + } +} +} // namespace + +namespace { +int on_frame_not_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, int lib_error_code, + void *user_data) { + auto upstream = static_cast<Http2Upstream *>(user_data); + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Failed to send control frame type=" + << static_cast<uint32_t>(frame->hd.type) + << ", lib_error_code=" << lib_error_code << ":" + << nghttp2_strerror(lib_error_code); + } + if (frame->hd.type == NGHTTP2_HEADERS && + lib_error_code != NGHTTP2_ERR_STREAM_CLOSED && + lib_error_code != NGHTTP2_ERR_STREAM_CLOSING) { + // To avoid stream hanging around, issue RST_STREAM. + auto downstream = static_cast<Downstream *>( + nghttp2_session_get_stream_user_data(session, frame->hd.stream_id)); + if (downstream) { + upstream->rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + } + return 0; +} +} // namespace + +namespace { +constexpr auto PADDING = std::array<uint8_t, 256>{}; +} // namespace + +namespace { +int send_data_callback(nghttp2_session *session, nghttp2_frame *frame, + const uint8_t *framehd, size_t length, + nghttp2_data_source *source, void *user_data) { + auto downstream = static_cast<Downstream *>(source->ptr); + auto upstream = static_cast<Http2Upstream *>(downstream->get_upstream()); + auto body = downstream->get_response_buf(); + + auto wb = upstream->get_response_buf(); + + size_t padlen = 0; + + wb->append(framehd, 9); + if (frame->data.padlen > 0) { + padlen = frame->data.padlen - 1; + wb->append(static_cast<uint8_t>(padlen)); + } + + body->remove(*wb, length); + + wb->append(PADDING.data(), padlen); + + if (body->rleft() == 0) { + downstream->disable_upstream_wtimer(); + } else { + downstream->reset_upstream_wtimer(); + } + + if (length > 0 && downstream->resume_read(SHRPX_NO_BUFFER, length) != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + // We have to add length here, so that we can log this amount of + // data transferred. + downstream->response_sent_body_length += length; + + auto max_buffer_size = upstream->get_max_buffer_size(); + + return wb->rleft() >= max_buffer_size ? NGHTTP2_ERR_PAUSE : 0; +} +} // namespace + +namespace { +uint32_t infer_upstream_rst_stream_error_code(uint32_t downstream_error_code) { + // NGHTTP2_REFUSED_STREAM is important because it tells upstream + // client to retry. + switch (downstream_error_code) { + case NGHTTP2_NO_ERROR: + case NGHTTP2_REFUSED_STREAM: + return downstream_error_code; + default: + return NGHTTP2_INTERNAL_ERROR; + } +} +} // namespace + +namespace { +void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto upstream = static_cast<Http2Upstream *>(w->data); + auto handler = upstream->get_client_handler(); + ULOG(INFO, upstream) << "SETTINGS timeout"; + if (upstream->terminate_session(NGHTTP2_SETTINGS_TIMEOUT) != 0) { + delete handler; + return; + } + handler->signal_write(); +} +} // namespace + +namespace { +void shutdown_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto upstream = static_cast<Http2Upstream *>(w->data); + auto handler = upstream->get_client_handler(); + upstream->submit_goaway(); + handler->signal_write(); +} +} // namespace + +namespace { +void prepare_cb(struct ev_loop *loop, ev_prepare *w, int revents) { + auto upstream = static_cast<Http2Upstream *>(w->data); + upstream->check_shutdown(); +} +} // namespace + +void Http2Upstream::submit_goaway() { + auto last_stream_id = nghttp2_session_get_last_proc_stream_id(session_); + nghttp2_submit_goaway(session_, NGHTTP2_FLAG_NONE, last_stream_id, + NGHTTP2_NO_ERROR, nullptr, 0); +} + +void Http2Upstream::check_shutdown() { + auto worker = handler_->get_worker(); + + if (!worker->get_graceful_shutdown()) { + return; + } + + ev_prepare_stop(handler_->get_loop(), &prep_); + + start_graceful_shutdown(); +} + +void Http2Upstream::start_graceful_shutdown() { + int rv; + if (ev_is_active(&shutdown_timer_)) { + return; + } + + rv = nghttp2_submit_shutdown_notice(session_); + if (rv != 0) { + ULOG(FATAL, this) << "nghttp2_submit_shutdown_notice() failed: " + << nghttp2_strerror(rv); + return; + } + + handler_->signal_write(); + + ev_timer_start(handler_->get_loop(), &shutdown_timer_); +} + +nghttp2_session_callbacks *create_http2_upstream_callbacks() { + int rv; + nghttp2_session_callbacks *callbacks; + + rv = nghttp2_session_callbacks_new(&callbacks); + + if (rv != 0) { + return nullptr; + } + + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, on_stream_close_callback); + + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback); + + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, on_data_chunk_recv_callback); + + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, + on_frame_send_callback); + + nghttp2_session_callbacks_set_on_frame_not_send_callback( + callbacks, on_frame_not_send_callback); + + nghttp2_session_callbacks_set_on_header_callback2(callbacks, + on_header_callback2); + + nghttp2_session_callbacks_set_on_invalid_header_callback2( + callbacks, on_invalid_header_callback2); + + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, on_begin_headers_callback); + + nghttp2_session_callbacks_set_send_data_callback(callbacks, + send_data_callback); + + auto config = get_config(); + + if (config->padding) { + nghttp2_session_callbacks_set_select_padding_callback( + callbacks, http::select_padding_callback); + } + + if (config->http2.upstream.debug.frame_debug) { + nghttp2_session_callbacks_set_error_callback2(callbacks, + verbose_error_callback); + } + + return callbacks; +} + +namespace { +size_t downstream_queue_size(Worker *worker) { + auto &downstreamconf = *worker->get_downstream_config(); + + if (get_config()->http2_proxy) { + return downstreamconf.connections_per_host; + } + + return downstreamconf.connections_per_frontend; +} +} // namespace + +Http2Upstream::Http2Upstream(ClientHandler *handler) + : wb_(handler->get_worker()->get_mcpool()), + downstream_queue_(downstream_queue_size(handler->get_worker()), + !get_config()->http2_proxy), + handler_(handler), + session_(nullptr), + max_buffer_size_(MAX_BUFFER_SIZE), + num_requests_(0) { + int rv; + + auto config = get_config(); + auto &http2conf = config->http2; + + auto faddr = handler_->get_upstream_addr(); + + rv = + nghttp2_session_server_new2(&session_, http2conf.upstream.callbacks, this, + faddr->alt_mode != UpstreamAltMode::NONE + ? http2conf.upstream.alt_mode_option + : http2conf.upstream.option); + + assert(rv == 0); + + flow_control_ = true; + + // TODO Maybe call from outside? + std::array<nghttp2_settings_entry, 5> entry; + size_t nentry = 3; + + entry[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; + entry[0].value = http2conf.upstream.max_concurrent_streams; + + entry[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; + if (faddr->alt_mode != UpstreamAltMode::NONE) { + entry[1].value = (1u << 31) - 1; + } else { + entry[1].value = http2conf.upstream.window_size; + } + + entry[2].settings_id = NGHTTP2_SETTINGS_NO_RFC7540_PRIORITIES; + entry[2].value = 1; + + if (!config->http2_proxy) { + entry[nentry].settings_id = NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL; + entry[nentry].value = 1; + ++nentry; + } + + if (http2conf.upstream.decoder_dynamic_table_size != + NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { + entry[nentry].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; + entry[nentry].value = http2conf.upstream.decoder_dynamic_table_size; + ++nentry; + } + + rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry.data(), + nentry); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp2_submit_settings() returned error: " + << nghttp2_strerror(rv); + } + + auto window_size = faddr->alt_mode != UpstreamAltMode::NONE + ? std::numeric_limits<int32_t>::max() + : http2conf.upstream.optimize_window_size + ? std::min(http2conf.upstream.connection_window_size, + NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE) + : http2conf.upstream.connection_window_size; + + rv = nghttp2_session_set_local_window_size(session_, NGHTTP2_FLAG_NONE, 0, + window_size); + + if (rv != 0) { + ULOG(ERROR, this) + << "nghttp2_session_set_local_window_size() returned error: " + << nghttp2_strerror(rv); + } + + // We wait for SETTINGS ACK at least 10 seconds. + ev_timer_init(&settings_timer_, settings_timeout_cb, + http2conf.upstream.timeout.settings, 0.); + + settings_timer_.data = this; + + // timer for 2nd GOAWAY. HTTP/2 spec recommend 1 RTT. We wait for + // 2 seconds. + ev_timer_init(&shutdown_timer_, shutdown_timeout_cb, 2., 0); + shutdown_timer_.data = this; + + ev_prepare_init(&prep_, prepare_cb); + prep_.data = this; + ev_prepare_start(handler_->get_loop(), &prep_); + +#if defined(TCP_INFO) && defined(TCP_NOTSENT_LOWAT) + if (http2conf.upstream.optimize_write_buffer_size) { + auto conn = handler_->get_connection(); + conn->tls_dyn_rec_warmup_threshold = 0; + + uint32_t pollout_thres = 1; + rv = setsockopt(conn->fd, IPPROTO_TCP, TCP_NOTSENT_LOWAT, &pollout_thres, + static_cast<socklen_t>(sizeof(pollout_thres))); + + if (rv != 0) { + if (LOG_ENABLED(INFO)) { + auto error = errno; + LOG(INFO) << "setsockopt(TCP_NOTSENT_LOWAT, " << pollout_thres + << ") failed: errno=" << error; + } + } + } +#endif // defined(TCP_INFO) && defined(TCP_NOTSENT_LOWAT) + + handler_->reset_upstream_read_timeout( + config->conn.upstream.timeout.http2_read); + + handler_->signal_write(); +} + +Http2Upstream::~Http2Upstream() { + nghttp2_session_del(session_); + ev_prepare_stop(handler_->get_loop(), &prep_); + ev_timer_stop(handler_->get_loop(), &shutdown_timer_); + ev_timer_stop(handler_->get_loop(), &settings_timer_); +} + +int Http2Upstream::on_read() { + ssize_t rv = 0; + auto rb = handler_->get_rb(); + auto rlimit = handler_->get_rlimit(); + + if (rb->rleft()) { + rv = nghttp2_session_mem_recv(session_, rb->pos(), rb->rleft()); + if (rv < 0) { + if (rv != NGHTTP2_ERR_BAD_CLIENT_MAGIC) { + ULOG(ERROR, this) << "nghttp2_session_mem_recv() returned error: " + << nghttp2_strerror(rv); + } + return -1; + } + + // nghttp2_session_mem_recv should consume all input bytes on + // success. + assert(static_cast<size_t>(rv) == rb->rleft()); + rb->reset(); + rlimit->startw(); + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "No more read/write for this HTTP2 session"; + } + return -1; + } + + handler_->signal_write(); + return 0; +} + +// After this function call, downstream may be deleted. +int Http2Upstream::on_write() { + int rv; + auto config = get_config(); + auto &http2conf = config->http2; + + if ((http2conf.upstream.optimize_write_buffer_size || + http2conf.upstream.optimize_window_size) && + handler_->get_ssl()) { + auto conn = handler_->get_connection(); + TCPHint hint; + rv = conn->get_tcp_hint(&hint); + if (rv == 0) { + if (http2conf.upstream.optimize_write_buffer_size) { + max_buffer_size_ = std::min(MAX_BUFFER_SIZE, hint.write_buffer_size); + } + + if (http2conf.upstream.optimize_window_size) { + auto faddr = handler_->get_upstream_addr(); + if (faddr->alt_mode == UpstreamAltMode::NONE) { + auto window_size = std::min(http2conf.upstream.connection_window_size, + static_cast<int32_t>(hint.rwin * 2)); + + rv = nghttp2_session_set_local_window_size( + session_, NGHTTP2_FLAG_NONE, 0, window_size); + if (rv != 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) + << "nghttp2_session_set_local_window_size() with window_size=" + << window_size << " failed: " << nghttp2_strerror(rv); + } + } + } + } + } + } + + for (;;) { + if (wb_.rleft() >= max_buffer_size_) { + return 0; + } + + const uint8_t *data; + auto datalen = nghttp2_session_mem_send(session_, &data); + + if (datalen < 0) { + ULOG(ERROR, this) << "nghttp2_session_mem_send() returned error: " + << nghttp2_strerror(datalen); + return -1; + } + if (datalen == 0) { + break; + } + wb_.append(data, datalen); + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "No more read/write for this HTTP2 session"; + } + return -1; + } + + return 0; +} + +ClientHandler *Http2Upstream::get_client_handler() const { return handler_; } + +int Http2Upstream::downstream_read(DownstreamConnection *dconn) { + auto downstream = dconn->get_downstream(); + + if (downstream->get_response_state() == DownstreamState::MSG_RESET) { + // The downstream stream was reset (canceled). In this case, + // RST_STREAM to the upstream and delete downstream connection + // here. Deleting downstream will be taken place at + // on_stream_close_callback. + rst_stream(downstream, + infer_upstream_rst_stream_error_code( + downstream->get_response_rst_stream_error_code())); + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + } else if (downstream->get_response_state() == + DownstreamState::MSG_BAD_HEADER) { + if (error_reply(downstream, 502) != 0) { + return -1; + } + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + } else { + auto rv = downstream->on_read(); + if (rv == SHRPX_ERR_EOF) { + if (downstream->get_request_header_sent()) { + return downstream_eof(dconn); + } + return SHRPX_ERR_RETRY; + } + if (rv == SHRPX_ERR_DCONN_CANCELED) { + downstream->pop_downstream_connection(); + handler_->signal_write(); + return 0; + } + if (rv != 0) { + if (rv != SHRPX_ERR_NETWORK) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "HTTP parser failure"; + } + } + return downstream_error(dconn, Downstream::EVENT_ERROR); + } + + if (downstream->can_detach_downstream_connection()) { + // Keep-alive + downstream->detach_downstream_connection(); + } + } + + handler_->signal_write(); + + // At this point, downstream may be deleted. + + return 0; +} + +int Http2Upstream::downstream_write(DownstreamConnection *dconn) { + int rv; + rv = dconn->on_write(); + if (rv == SHRPX_ERR_NETWORK) { + return downstream_error(dconn, Downstream::EVENT_ERROR); + } + if (rv != 0) { + return rv; + } + return 0; +} + +int Http2Upstream::downstream_eof(DownstreamConnection *dconn) { + auto downstream = dconn->get_downstream(); + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "EOF. stream_id=" << downstream->get_stream_id(); + } + + // Delete downstream connection. If we don't delete it here, it will + // be pooled in on_stream_close_callback. + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + // downstream will be deleted in on_stream_close_callback. + if (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE) { + // Server may indicate the end of the request by EOF + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Downstream body was ended by EOF"; + } + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + // For tunneled connection, MSG_COMPLETE signals + // downstream_data_read_callback to send RST_STREAM after pending + // response body is sent. This is needed to ensure that RST_STREAM + // is sent after all pending data are sent. + on_downstream_body_complete(downstream); + } else if (downstream->get_response_state() != + DownstreamState::MSG_COMPLETE) { + // If stream was not closed, then we set MSG_COMPLETE and let + // on_stream_close_callback delete downstream. + if (error_reply(downstream, 502) != 0) { + return -1; + } + } + handler_->signal_write(); + // At this point, downstream may be deleted. + return 0; +} + +int Http2Upstream::downstream_error(DownstreamConnection *dconn, int events) { + auto downstream = dconn->get_downstream(); + + if (LOG_ENABLED(INFO)) { + if (events & Downstream::EVENT_ERROR) { + DCLOG(INFO, dconn) << "Downstream network/general error"; + } else { + DCLOG(INFO, dconn) << "Timeout"; + } + if (downstream->get_upgraded()) { + DCLOG(INFO, dconn) << "Note: this is tunnel connection"; + } + } + + // Delete downstream connection. If we don't delete it here, it will + // be pooled in on_stream_close_callback. + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + // For SSL tunneling, we issue RST_STREAM. For other types of + // stream, we don't have to do anything since response was + // complete. + if (downstream->get_upgraded()) { + rst_stream(downstream, NGHTTP2_NO_ERROR); + } + } else { + if (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE) { + if (downstream->get_upgraded()) { + on_downstream_body_complete(downstream); + } else { + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + } else { + unsigned int status; + if (events & Downstream::EVENT_TIMEOUT) { + if (downstream->get_request_header_sent()) { + status = 504; + } else { + status = 408; + } + } else { + status = 502; + } + if (error_reply(downstream, status) != 0) { + return -1; + } + } + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + } + handler_->signal_write(); + // At this point, downstream may be deleted. + return 0; +} + +int Http2Upstream::rst_stream(Downstream *downstream, uint32_t error_code) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "RST_STREAM stream_id=" << downstream->get_stream_id() + << " with error_code=" << error_code; + } + int rv; + rv = nghttp2_submit_rst_stream(session_, NGHTTP2_FLAG_NONE, + downstream->get_stream_id(), error_code); + if (rv < NGHTTP2_ERR_FATAL) { + ULOG(FATAL, this) << "nghttp2_submit_rst_stream() failed: " + << nghttp2_strerror(rv); + return -1; + } + return 0; +} + +int Http2Upstream::terminate_session(uint32_t error_code) { + int rv; + rv = nghttp2_session_terminate_session(session_, error_code); + if (rv != 0) { + return -1; + } + return 0; +} + +namespace { +ssize_t downstream_data_read_callback(nghttp2_session *session, + int32_t stream_id, uint8_t *buf, + size_t length, uint32_t *data_flags, + nghttp2_data_source *source, + void *user_data) { + int rv; + auto downstream = static_cast<Downstream *>(source->ptr); + auto body = downstream->get_response_buf(); + assert(body); + auto upstream = static_cast<Http2Upstream *>(user_data); + + const auto &resp = downstream->response(); + + auto nread = std::min(body->rleft(), length); + + auto max_buffer_size = upstream->get_max_buffer_size(); + + auto buffer = upstream->get_response_buf(); + + if (max_buffer_size < + std::min(nread, static_cast<size_t>(256)) + 9 + buffer->rleft()) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Buffer is almost full. Skip write DATA"; + } + return NGHTTP2_ERR_PAUSE; + } + + nread = std::min(nread, max_buffer_size - 9 - buffer->rleft()); + + auto body_empty = body->rleft() == nread; + + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + + if (body_empty && + downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + + if (!downstream->get_upgraded()) { + const auto &trailers = resp.fs.trailers(); + if (!trailers.empty()) { + std::vector<nghttp2_nv> nva; + nva.reserve(trailers.size()); + http2::copy_headers_to_nva_nocopy(nva, trailers, http2::HDOP_STRIP_ALL); + if (!nva.empty()) { + rv = nghttp2_submit_trailer(session, stream_id, nva.data(), + nva.size()); + if (rv != 0) { + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } else { + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + } + } + } + } + } + + if (nread == 0 && ((*data_flags) & NGHTTP2_DATA_FLAG_EOF) == 0) { + downstream->disable_upstream_wtimer(); + return NGHTTP2_ERR_DEFERRED; + } + + return nread; +} +} // namespace + +int Http2Upstream::send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) { + int rv; + + nghttp2_data_provider data_prd, *data_prd_ptr = nullptr; + + if (bodylen) { + data_prd.source.ptr = downstream; + data_prd.read_callback = downstream_data_read_callback; + data_prd_ptr = &data_prd; + } + + const auto &resp = downstream->response(); + auto config = get_config(); + auto &httpconf = config->http; + + auto &balloc = downstream->get_block_allocator(); + + const auto &headers = resp.fs.headers(); + auto nva = std::vector<nghttp2_nv>(); + // 2 for :status and server + nva.reserve(2 + headers.size() + httpconf.add_response_headers.size()); + + auto response_status = http2::stringify_status(balloc, resp.http_status); + + nva.push_back(http2::make_nv_ls_nocopy(":status", response_status)); + + for (auto &kv : headers) { + if (kv.name.empty() || kv.name[0] == ':') { + continue; + } + switch (kv.token) { + case http2::HD_CONNECTION: + case http2::HD_KEEP_ALIVE: + case http2::HD_PROXY_CONNECTION: + case http2::HD_TE: + case http2::HD_TRANSFER_ENCODING: + case http2::HD_UPGRADE: + continue; + } + nva.push_back(http2::make_nv_nocopy(kv.name, kv.value, kv.no_index)); + } + + if (!resp.fs.header(http2::HD_SERVER)) { + nva.push_back(http2::make_nv_ls_nocopy("server", config->http.server_name)); + } + + for (auto &p : httpconf.add_response_headers) { + nva.push_back(http2::make_nv_nocopy(p.name, p.value)); + } + + rv = nghttp2_submit_response(session_, downstream->get_stream_id(), + nva.data(), nva.size(), data_prd_ptr); + if (nghttp2_is_fatal(rv)) { + ULOG(FATAL, this) << "nghttp2_submit_response() failed: " + << nghttp2_strerror(rv); + return -1; + } + + auto buf = downstream->get_response_buf(); + + buf->append(body, bodylen); + + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + if (data_prd_ptr) { + downstream->reset_upstream_wtimer(); + } + + return 0; +} + +int Http2Upstream::error_reply(Downstream *downstream, + unsigned int status_code) { + int rv; + auto &resp = downstream->response(); + + auto &balloc = downstream->get_block_allocator(); + + auto html = http::create_error_html(balloc, status_code); + resp.http_status = status_code; + auto body = downstream->get_response_buf(); + body->append(html); + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + nghttp2_data_provider data_prd; + data_prd.source.ptr = downstream; + data_prd.read_callback = downstream_data_read_callback; + + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + + auto response_status = http2::stringify_status(balloc, status_code); + auto content_length = util::make_string_ref_uint(balloc, html.size()); + auto date = make_string_ref(balloc, lgconf->tstamp->time_http); + + auto nva = std::array<nghttp2_nv, 5>{ + {http2::make_nv_ls_nocopy(":status", response_status), + http2::make_nv_ll("content-type", "text/html; charset=UTF-8"), + http2::make_nv_ls_nocopy("server", get_config()->http.server_name), + http2::make_nv_ls_nocopy("content-length", content_length), + http2::make_nv_ls_nocopy("date", date)}}; + + rv = nghttp2_submit_response(session_, downstream->get_stream_id(), + nva.data(), nva.size(), &data_prd); + if (rv < NGHTTP2_ERR_FATAL) { + ULOG(FATAL, this) << "nghttp2_submit_response() failed: " + << nghttp2_strerror(rv); + return -1; + } + + downstream->reset_upstream_wtimer(); + + return 0; +} + +void Http2Upstream::add_pending_downstream( + std::unique_ptr<Downstream> downstream) { + downstream_queue_.add_pending(std::move(downstream)); +} + +void Http2Upstream::remove_downstream(Downstream *downstream) { + if (downstream->accesslog_ready()) { + handler_->write_accesslog(downstream); + } + + nghttp2_session_set_stream_user_data(session_, downstream->get_stream_id(), + nullptr); + + auto next_downstream = downstream_queue_.remove_and_get_blocked(downstream); + + if (next_downstream) { + initiate_downstream(next_downstream); + } + + if (downstream_queue_.get_downstreams() == nullptr) { + // There is no downstream at the moment. Start idle timer now. + handler_->repeat_read_timer(); + } +} + +// WARNING: Never call directly or indirectly nghttp2_session_send or +// nghttp2_session_recv. These calls may delete downstream. +int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { + int rv; + + const auto &req = downstream->request(); + auto &resp = downstream->response(); + + auto &balloc = downstream->get_block_allocator(); + + if (LOG_ENABLED(INFO)) { + if (downstream->get_non_final_response()) { + DLOG(INFO, downstream) << "HTTP non-final response header"; + } else { + DLOG(INFO, downstream) << "HTTP response header completed"; + } + } + + auto config = get_config(); + auto &httpconf = config->http; + + if (!config->http2_proxy && !httpconf.no_location_rewrite) { + downstream->rewrite_location_response_header(req.scheme); + } + +#ifdef HAVE_MRUBY + if (!downstream->get_non_final_response()) { + auto dconn = downstream->get_downstream_connection(); + const auto &group = dconn->get_downstream_addr_group(); + if (group) { + const auto &dmruby_ctx = group->shared_addr->mruby_ctx; + + if (dmruby_ctx->run_on_response_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return -1; + } + // Returning -1 will signal deletion of dconn. + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return -1; + } + } + + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_response_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return -1; + } + // Returning -1 will signal deletion of dconn. + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return -1; + } + } +#endif // HAVE_MRUBY + + auto &http2conf = config->http2; + + // We need some conditions that must be fulfilled to initiate server + // push. + // + // * Server push is disabled for http2 proxy or client proxy, since + // incoming headers are mixed origins. We don't know how to + // reliably determine the authority yet. + // + // * We need non-final response or 200 response code for associated + // resource. This is too restrictive, we will review this later. + // + // * We requires GET or POST for associated resource. Probably we + // don't want to push for HEAD request. Not sure other methods + // are also eligible for push. + if (!http2conf.no_server_push && + nghttp2_session_get_remote_settings(session_, + NGHTTP2_SETTINGS_ENABLE_PUSH) == 1 && + !config->http2_proxy && (downstream->get_stream_id() % 2) && + resp.fs.header(http2::HD_LINK) && + (downstream->get_non_final_response() || resp.http_status == 200) && + (req.method == HTTP_GET || req.method == HTTP_POST)) { + + if (prepare_push_promise(downstream) != 0) { + // Continue to send response even if push was failed. + } + } + + auto nva = std::vector<nghttp2_nv>(); + // 6 means :status and possible server, via, x-http2-push, alt-svc, + // and set-cookie (for affinity cookie) header field. + nva.reserve(resp.fs.headers().size() + 6 + + httpconf.add_response_headers.size()); + + if (downstream->get_non_final_response()) { + auto response_status = http2::stringify_status(balloc, resp.http_status); + + nva.push_back(http2::make_nv_ls_nocopy(":status", response_status)); + + http2::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), + http2::HDOP_STRIP_ALL); + + if (LOG_ENABLED(INFO)) { + log_response_headers(downstream, nva); + } + + rv = nghttp2_submit_headers(session_, NGHTTP2_FLAG_NONE, + downstream->get_stream_id(), nullptr, + nva.data(), nva.size(), nullptr); + + resp.fs.clear_headers(); + + if (rv != 0) { + ULOG(FATAL, this) << "nghttp2_submit_headers() failed"; + return -1; + } + + return 0; + } + + auto striphd_flags = http2::HDOP_STRIP_ALL & ~http2::HDOP_STRIP_VIA; + StringRef response_status; + + if (req.connect_proto == ConnectProto::WEBSOCKET && resp.http_status == 101) { + response_status = http2::stringify_status(balloc, 200); + striphd_flags |= http2::HDOP_STRIP_SEC_WEBSOCKET_ACCEPT; + } else { + response_status = http2::stringify_status(balloc, resp.http_status); + } + + nva.push_back(http2::make_nv_ls_nocopy(":status", response_status)); + + http2::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), striphd_flags); + + if (!config->http2_proxy && !httpconf.no_server_rewrite) { + nva.push_back(http2::make_nv_ls_nocopy("server", httpconf.server_name)); + } else { + auto server = resp.fs.header(http2::HD_SERVER); + if (server) { + nva.push_back(http2::make_nv_ls_nocopy("server", (*server).value)); + } + } + + if (!req.regular_connect_method() || !downstream->get_upgraded()) { + auto affinity_cookie = downstream->get_affinity_cookie_to_send(); + if (affinity_cookie) { + auto dconn = downstream->get_downstream_connection(); + assert(dconn); + auto &group = dconn->get_downstream_addr_group(); + auto &shared_addr = group->shared_addr; + auto &cookieconf = shared_addr->affinity.cookie; + auto secure = + http::require_cookie_secure_attribute(cookieconf.secure, req.scheme); + auto cookie_str = http::create_affinity_cookie( + balloc, cookieconf.name, affinity_cookie, cookieconf.path, secure); + nva.push_back(http2::make_nv_ls_nocopy("set-cookie", cookie_str)); + } + } + + if (!resp.fs.header(http2::HD_ALT_SVC)) { + // We won't change or alter alt-svc from backend for now + if (!httpconf.http2_altsvc_header_value.empty()) { + nva.push_back(http2::make_nv_ls_nocopy( + "alt-svc", httpconf.http2_altsvc_header_value)); + } + } + + auto via = resp.fs.header(http2::HD_VIA); + if (httpconf.no_via) { + if (via) { + nva.push_back(http2::make_nv_ls_nocopy("via", (*via).value)); + } + } else { + // we don't create more than 16 bytes in + // http::create_via_header_value. + size_t len = 16; + if (via) { + len += via->value.size() + 2; + } + + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + if (via) { + p = std::copy(std::begin(via->value), std::end(via->value), p); + p = util::copy_lit(p, ", "); + } + p = http::create_via_header_value(p, resp.http_major, resp.http_minor); + *p = '\0'; + + nva.push_back(http2::make_nv_ls_nocopy("via", StringRef{iov.base, p})); + } + + for (auto &p : httpconf.add_response_headers) { + nva.push_back(http2::make_nv_nocopy(p.name, p.value)); + } + + if (downstream->get_stream_id() % 2 == 0) { + // This header field is basically for human on client side to + // figure out that the resource is pushed. + nva.push_back(http2::make_nv_ll("x-http2-push", "1")); + } + + if (LOG_ENABLED(INFO)) { + log_response_headers(downstream, nva); + } + + if (http2conf.upstream.debug.dump.response_header) { + http2::dump_nv(http2conf.upstream.debug.dump.response_header, nva.data(), + nva.size()); + } + + auto priority = resp.fs.header(http2::HD_PRIORITY); + if (priority) { + nghttp2_extpri extpri; + + if (nghttp2_session_get_extpri_stream_priority( + session_, &extpri, downstream->get_stream_id()) == 0 && + nghttp2_extpri_parse_priority(&extpri, priority->value.byte(), + priority->value.size()) == 0) { + rv = nghttp2_session_change_extpri_stream_priority( + session_, downstream->get_stream_id(), &extpri, + /* ignore_client_signal = */ 1); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp2_session_change_extpri_stream_priority: " + << nghttp2_strerror(rv); + } + } + } + + nghttp2_data_provider data_prd; + data_prd.source.ptr = downstream; + data_prd.read_callback = downstream_data_read_callback; + + nghttp2_data_provider *data_prdptr; + + if (downstream->expect_response_body() || + downstream->expect_response_trailer()) { + data_prdptr = &data_prd; + } else { + data_prdptr = nullptr; + } + + rv = nghttp2_submit_response(session_, downstream->get_stream_id(), + nva.data(), nva.size(), data_prdptr); + if (rv != 0) { + ULOG(FATAL, this) << "nghttp2_submit_response() failed"; + return -1; + } + + if (data_prdptr) { + downstream->reset_upstream_wtimer(); + } + + return 0; +} + +// WARNING: Never call directly or indirectly nghttp2_session_send or +// nghttp2_session_recv. These calls may delete downstream. +int Http2Upstream::on_downstream_body(Downstream *downstream, + const uint8_t *data, size_t len, + bool flush) { + auto body = downstream->get_response_buf(); + body->append(data, len); + + if (flush) { + nghttp2_session_resume_data(session_, downstream->get_stream_id()); + + downstream->ensure_upstream_wtimer(); + } + + return 0; +} + +// WARNING: Never call directly or indirectly nghttp2_session_send or +// nghttp2_session_recv. These calls may delete downstream. +int Http2Upstream::on_downstream_body_complete(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "HTTP response completed"; + } + + auto &resp = downstream->response(); + + if (!downstream->validate_response_recv_body_length()) { + rst_stream(downstream, NGHTTP2_PROTOCOL_ERROR); + resp.connection_close = true; + return 0; + } + + nghttp2_session_resume_data(session_, downstream->get_stream_id()); + downstream->ensure_upstream_wtimer(); + + return 0; +} + +bool Http2Upstream::get_flow_control() const { return flow_control_; } + +void Http2Upstream::pause_read(IOCtrlReason reason) {} + +int Http2Upstream::resume_read(IOCtrlReason reason, Downstream *downstream, + size_t consumed) { + if (get_flow_control()) { + if (consume(downstream->get_stream_id(), consumed) != 0) { + return -1; + } + + auto &req = downstream->request(); + + req.consume(consumed); + } + + handler_->signal_write(); + return 0; +} + +int Http2Upstream::on_downstream_abort_request(Downstream *downstream, + unsigned int status_code) { + int rv; + + rv = error_reply(downstream, status_code); + + if (rv != 0) { + return -1; + } + + handler_->signal_write(); + return 0; +} + +int Http2Upstream::on_downstream_abort_request_with_https_redirect( + Downstream *downstream) { + int rv; + + rv = redirect_to_https(downstream); + if (rv != 0) { + return -1; + } + + handler_->signal_write(); + return 0; +} + +int Http2Upstream::redirect_to_https(Downstream *downstream) { + auto &req = downstream->request(); + if (req.regular_connect_method() || req.scheme != "http") { + return error_reply(downstream, 400); + } + + auto authority = util::extract_host(req.authority); + if (authority.empty()) { + return error_reply(downstream, 400); + } + + auto &balloc = downstream->get_block_allocator(); + auto config = get_config(); + auto &httpconf = config->http; + + StringRef loc; + if (httpconf.redirect_https_port == StringRef::from_lit("443")) { + loc = concat_string_ref(balloc, StringRef::from_lit("https://"), authority, + req.path); + } else { + loc = concat_string_ref(balloc, StringRef::from_lit("https://"), authority, + StringRef::from_lit(":"), + httpconf.redirect_https_port, req.path); + } + + auto &resp = downstream->response(); + resp.http_status = 308; + resp.fs.add_header_token(StringRef::from_lit("location"), loc, false, + http2::HD_LOCATION); + + return send_reply(downstream, nullptr, 0); +} + +int Http2Upstream::consume(int32_t stream_id, size_t len) { + int rv; + + auto faddr = handler_->get_upstream_addr(); + + if (faddr->alt_mode != UpstreamAltMode::NONE) { + return 0; + } + + rv = nghttp2_session_consume(session_, stream_id, len); + + if (rv != 0) { + ULOG(WARN, this) << "nghttp2_session_consume() returned error: " + << nghttp2_strerror(rv); + return -1; + } + + return 0; +} + +void Http2Upstream::log_response_headers( + Downstream *downstream, const std::vector<nghttp2_nv> &nva) const { + std::stringstream ss; + for (auto &nv : nva) { + ss << TTY_HTTP_HD << StringRef{nv.name, nv.namelen} << TTY_RST << ": " + << StringRef{nv.value, nv.valuelen} << "\n"; + } + ULOG(INFO, this) << "HTTP response headers. stream_id=" + << downstream->get_stream_id() << "\n" + << ss.str(); +} + +int Http2Upstream::on_timeout(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Stream timeout stream_id=" + << downstream->get_stream_id(); + } + + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + handler_->signal_write(); + + return 0; +} + +void Http2Upstream::on_handler_delete() { + for (auto d = downstream_queue_.get_downstreams(); d; d = d->dlnext) { + if (d->get_dispatch_state() == DispatchState::ACTIVE && + d->accesslog_ready()) { + handler_->write_accesslog(d); + } + } +} + +int Http2Upstream::on_downstream_reset(Downstream *downstream, bool no_retry) { + int rv; + + if (downstream->get_dispatch_state() != DispatchState::ACTIVE) { + // This is error condition when we failed push_request_headers() + // in initiate_downstream(). Otherwise, we have + // DispatchState::ACTIVE state, or we did not set + // DownstreamConnection. + downstream->pop_downstream_connection(); + handler_->signal_write(); + + return 0; + } + + if (!downstream->request_submission_ready()) { + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + // We have got all response body already. Send it off. + downstream->pop_downstream_connection(); + return 0; + } + // pushed stream is handled here + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + downstream->pop_downstream_connection(); + + handler_->signal_write(); + + return 0; + } + + downstream->pop_downstream_connection(); + + downstream->add_retry(); + + std::unique_ptr<DownstreamConnection> dconn; + + rv = 0; + + if (no_retry || downstream->no_more_retry()) { + goto fail; + } + + // downstream connection is clean; we can retry with new + // downstream connection. + + for (;;) { + auto dconn = handler_->get_downstream_connection(rv, downstream); + if (!dconn) { + goto fail; + } + + rv = downstream->attach_downstream_connection(std::move(dconn)); + if (rv == 0) { + break; + } + } + + rv = downstream->push_request_headers(); + if (rv != 0) { + goto fail; + } + + return 0; + +fail: + if (rv == SHRPX_ERR_TLS_REQUIRED) { + rv = on_downstream_abort_request_with_https_redirect(downstream); + } else { + rv = on_downstream_abort_request(downstream, 502); + } + if (rv != 0) { + rst_stream(downstream, NGHTTP2_INTERNAL_ERROR); + } + downstream->pop_downstream_connection(); + + handler_->signal_write(); + + return 0; +} + +int Http2Upstream::prepare_push_promise(Downstream *downstream) { + int rv; + + const auto &req = downstream->request(); + auto &resp = downstream->response(); + + auto base = http2::get_pure_path_component(req.path); + if (base.empty()) { + return 0; + } + + auto &balloc = downstream->get_block_allocator(); + + for (auto &kv : resp.fs.headers()) { + if (kv.token != http2::HD_LINK) { + continue; + } + for (auto &link : http2::parse_link_header(kv.value)) { + StringRef scheme, authority, path; + + rv = http2::construct_push_component(balloc, scheme, authority, path, + base, link.uri); + if (rv != 0) { + continue; + } + + if (scheme.empty()) { + scheme = req.scheme; + } + + if (authority.empty()) { + authority = req.authority; + } + + if (resp.is_resource_pushed(scheme, authority, path)) { + continue; + } + + rv = submit_push_promise(scheme, authority, path, downstream); + if (rv != 0) { + return -1; + } + + resp.resource_pushed(scheme, authority, path); + } + } + return 0; +} + +int Http2Upstream::submit_push_promise(const StringRef &scheme, + const StringRef &authority, + const StringRef &path, + Downstream *downstream) { + const auto &req = downstream->request(); + + std::vector<nghttp2_nv> nva; + // 4 for :method, :scheme, :path and :authority + nva.reserve(4 + req.fs.headers().size()); + + // just use "GET" for now + nva.push_back(http2::make_nv_ll(":method", "GET")); + nva.push_back(http2::make_nv_ls_nocopy(":scheme", scheme)); + nva.push_back(http2::make_nv_ls_nocopy(":path", path)); + nva.push_back(http2::make_nv_ls_nocopy(":authority", authority)); + + for (auto &kv : req.fs.headers()) { + switch (kv.token) { + // TODO generate referer + case http2::HD__AUTHORITY: + case http2::HD__SCHEME: + case http2::HD__METHOD: + case http2::HD__PATH: + continue; + case http2::HD_ACCEPT_ENCODING: + case http2::HD_ACCEPT_LANGUAGE: + case http2::HD_CACHE_CONTROL: + case http2::HD_HOST: + case http2::HD_USER_AGENT: + nva.push_back(http2::make_nv_nocopy(kv.name, kv.value, kv.no_index)); + break; + } + } + + auto promised_stream_id = nghttp2_submit_push_promise( + session_, NGHTTP2_FLAG_NONE, downstream->get_stream_id(), nva.data(), + nva.size(), nullptr); + + if (promised_stream_id < 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "nghttp2_submit_push_promise() failed: " + << nghttp2_strerror(promised_stream_id); + } + if (nghttp2_is_fatal(promised_stream_id)) { + return -1; + } + return 0; + } + + if (LOG_ENABLED(INFO)) { + std::stringstream ss; + for (auto &nv : nva) { + ss << TTY_HTTP_HD << StringRef{nv.name, nv.namelen} << TTY_RST << ": " + << StringRef{nv.value, nv.valuelen} << "\n"; + } + ULOG(INFO, this) << "HTTP push request headers. promised_stream_id=" + << promised_stream_id << "\n" + << ss.str(); + } + + return 0; +} + +bool Http2Upstream::push_enabled() const { + auto config = get_config(); + return !(config->http2.no_server_push || + nghttp2_session_get_remote_settings( + session_, NGHTTP2_SETTINGS_ENABLE_PUSH) == 0 || + config->http2_proxy); +} + +int Http2Upstream::initiate_push(Downstream *downstream, const StringRef &uri) { + int rv; + + if (uri.empty() || !push_enabled() || + (downstream->get_stream_id() % 2) == 0) { + return 0; + } + + const auto &req = downstream->request(); + + auto base = http2::get_pure_path_component(req.path); + if (base.empty()) { + return -1; + } + + auto &balloc = downstream->get_block_allocator(); + + StringRef scheme, authority, path; + + rv = http2::construct_push_component(balloc, scheme, authority, path, base, + uri); + if (rv != 0) { + return -1; + } + + if (scheme.empty()) { + scheme = req.scheme; + } + + if (authority.empty()) { + authority = req.authority; + } + + auto &resp = downstream->response(); + + if (resp.is_resource_pushed(scheme, authority, path)) { + return 0; + } + + rv = submit_push_promise(scheme, authority, path, downstream); + + if (rv != 0) { + return -1; + } + + resp.resource_pushed(scheme, authority, path); + + return 0; +} + +int Http2Upstream::response_riovec(struct iovec *iov, int iovcnt) const { + if (iovcnt == 0 || wb_.rleft() == 0) { + return 0; + } + + return wb_.riovec(iov, iovcnt); +} + +void Http2Upstream::response_drain(size_t n) { wb_.drain(n); } + +bool Http2Upstream::response_empty() const { return wb_.rleft() == 0; } + +DefaultMemchunks *Http2Upstream::get_response_buf() { return &wb_; } + +Downstream * +Http2Upstream::on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + // promised_stream_id is for backend HTTP/2 session, not for + // frontend. + auto promised_downstream = + std::make_unique<Downstream>(this, handler_->get_mcpool(), 0); + auto &promised_req = promised_downstream->request(); + + promised_downstream->set_downstream_stream_id(promised_stream_id); + // Set associated stream in frontend + promised_downstream->set_assoc_stream_id(downstream->get_stream_id()); + + promised_downstream->disable_upstream_rtimer(); + + promised_req.http_major = 2; + promised_req.http_minor = 0; + + promised_req.fs.content_length = 0; + promised_req.http2_expect_body = false; + + auto ptr = promised_downstream.get(); + add_pending_downstream(std::move(promised_downstream)); + downstream_queue_.mark_active(ptr); + + return ptr; +} + +int Http2Upstream::on_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + std::vector<nghttp2_nv> nva; + + const auto &promised_req = promised_downstream->request(); + const auto &headers = promised_req.fs.headers(); + + nva.reserve(headers.size()); + + for (auto &kv : headers) { + nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); + } + + auto promised_stream_id = nghttp2_submit_push_promise( + session_, NGHTTP2_FLAG_NONE, downstream->get_stream_id(), nva.data(), + nva.size(), promised_downstream); + if (promised_stream_id < 0) { + return -1; + } + + promised_downstream->set_stream_id(promised_stream_id); + + return 0; +} + +void Http2Upstream::cancel_premature_downstream( + Downstream *promised_downstream) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Remove premature promised stream " + << promised_downstream; + } + downstream_queue_.remove_and_get_blocked(promised_downstream, false); +} + +size_t Http2Upstream::get_max_buffer_size() const { return max_buffer_size_; } + +} // namespace shrpx diff --git a/src/shrpx_http2_upstream.h b/src/shrpx_http2_upstream.h new file mode 100644 index 0000000..739c6ff --- /dev/null +++ b/src/shrpx_http2_upstream.h @@ -0,0 +1,150 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP2_UPSTREAM_H +#define SHRPX_HTTP2_UPSTREAM_H + +#include "shrpx.h" + +#include <memory> + +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#include "shrpx_upstream.h" +#include "shrpx_downstream_queue.h" +#include "memchunk.h" +#include "buffer.h" + +using namespace nghttp2; + +namespace shrpx { + +class ClientHandler; +class HttpsUpstream; + +class Http2Upstream : public Upstream { +public: + Http2Upstream(ClientHandler *handler); + virtual ~Http2Upstream(); + virtual int on_read(); + virtual int on_write(); + virtual int on_timeout(Downstream *downstream); + virtual int on_downstream_abort_request(Downstream *downstream, + unsigned int status_code); + virtual int + on_downstream_abort_request_with_https_redirect(Downstream *downstream); + virtual ClientHandler *get_client_handler() const; + + virtual int downstream_read(DownstreamConnection *dconn); + virtual int downstream_write(DownstreamConnection *dconn); + virtual int downstream_eof(DownstreamConnection *dconn); + virtual int downstream_error(DownstreamConnection *dconn, int events); + + void add_pending_downstream(std::unique_ptr<Downstream> downstream); + void remove_downstream(Downstream *downstream); + + int rst_stream(Downstream *downstream, uint32_t error_code); + int terminate_session(uint32_t error_code); + int error_reply(Downstream *downstream, unsigned int status_code); + + virtual void pause_read(IOCtrlReason reason); + virtual int resume_read(IOCtrlReason reason, Downstream *downstream, + size_t consumed); + + virtual int on_downstream_header_complete(Downstream *downstream); + virtual int on_downstream_body(Downstream *downstream, const uint8_t *data, + size_t len, bool flush); + virtual int on_downstream_body_complete(Downstream *downstream); + + virtual void on_handler_delete(); + virtual int on_downstream_reset(Downstream *downstream, bool no_retry); + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen); + virtual int initiate_push(Downstream *downstream, const StringRef &uri); + virtual int response_riovec(struct iovec *iov, int iovcnt) const; + virtual void response_drain(size_t n); + virtual bool response_empty() const; + + virtual Downstream *on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + virtual bool push_enabled() const; + virtual void cancel_premature_downstream(Downstream *promised_downstream); + + bool get_flow_control() const; + // Perform HTTP/2 upgrade from |upstream|. On success, this object + // takes ownership of the |upstream|. This function returns 0 if it + // succeeds, or -1. + int upgrade_upstream(HttpsUpstream *upstream); + void start_settings_timer(); + void stop_settings_timer(); + int consume(int32_t stream_id, size_t len); + void log_response_headers(Downstream *downstream, + const std::vector<nghttp2_nv> &nva) const; + void start_downstream(Downstream *downstream); + void initiate_downstream(Downstream *downstream); + + void submit_goaway(); + void check_shutdown(); + // Starts graceful shutdown period. + void start_graceful_shutdown(); + + int prepare_push_promise(Downstream *downstream); + int submit_push_promise(const StringRef &scheme, const StringRef &authority, + const StringRef &path, Downstream *downstream); + + // Called when new request has started. + void on_start_request(const nghttp2_frame *frame); + int on_request_headers(Downstream *downstream, const nghttp2_frame *frame); + + DefaultMemchunks *get_response_buf(); + + size_t get_max_buffer_size() const; + + int redirect_to_https(Downstream *downstream); + +private: + DefaultMemchunks wb_; + std::unique_ptr<HttpsUpstream> pre_upstream_; + DownstreamQueue downstream_queue_; + ev_timer settings_timer_; + ev_timer shutdown_timer_; + ev_prepare prep_; + ClientHandler *handler_; + nghttp2_session *session_; + size_t max_buffer_size_; + // The number of requests seen so far. + size_t num_requests_; + bool flow_control_; +}; + +nghttp2_session_callbacks *create_http2_upstream_callbacks(); + +} // namespace shrpx + +#endif // SHRPX_HTTP2_UPSTREAM_H diff --git a/src/shrpx_http3_upstream.cc b/src/shrpx_http3_upstream.cc new file mode 100644 index 0000000..f8ae1ce --- /dev/null +++ b/src/shrpx_http3_upstream.cc @@ -0,0 +1,2906 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_http3_upstream.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <netinet/udp.h> + +#include <cstdio> + +#include <ngtcp2/ngtcp2_crypto.h> + +#include "shrpx_client_handler.h" +#include "shrpx_downstream.h" +#include "shrpx_downstream_connection.h" +#include "shrpx_log.h" +#include "shrpx_quic.h" +#include "shrpx_worker.h" +#include "shrpx_http.h" +#include "shrpx_connection_handler.h" +#ifdef HAVE_MRUBY +# include "shrpx_mruby.h" +#endif // HAVE_MRUBY +#include "http3.h" +#include "util.h" +#include "ssl_compat.h" + +namespace shrpx { + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto upstream = static_cast<Http3Upstream *>(w->data); + + if (upstream->handle_expiry() != 0 || upstream->on_write() != 0) { + goto fail; + } + + return; + +fail: + auto handler = upstream->get_client_handler(); + + delete handler; +} +} // namespace + +namespace { +void shutdown_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto upstream = static_cast<Http3Upstream *>(w->data); + auto handler = upstream->get_client_handler(); + + if (upstream->submit_goaway() != 0) { + delete handler; + } +} +} // namespace + +namespace { +void prepare_cb(struct ev_loop *loop, ev_prepare *w, int revent) { + auto upstream = static_cast<Http3Upstream *>(w->data); + auto handler = upstream->get_client_handler(); + + if (upstream->check_shutdown() != 0) { + delete handler; + } +} +} // namespace + +namespace { +size_t downstream_queue_size(Worker *worker) { + auto &downstreamconf = *worker->get_downstream_config(); + + if (get_config()->http2_proxy) { + return downstreamconf.connections_per_host; + } + + return downstreamconf.connections_per_frontend; +} +} // namespace + +namespace { +ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref) { + auto conn = static_cast<Connection *>(conn_ref->user_data); + auto handler = static_cast<ClientHandler *>(conn->data); + auto upstream = static_cast<Http3Upstream *>(handler->get_upstream()); + return upstream->get_conn(); +} +} // namespace + +Http3Upstream::Http3Upstream(ClientHandler *handler) + : handler_{handler}, + qlog_fd_{-1}, + hashed_scid_{}, + conn_{nullptr}, + httpconn_{nullptr}, + downstream_queue_{downstream_queue_size(handler->get_worker()), + !get_config()->http2_proxy}, + retry_close_{false}, + tx_{ + .data = std::unique_ptr<uint8_t[]>(new uint8_t[64_k]), + } { + auto conn = handler_->get_connection(); + conn->conn_ref.get_conn = shrpx::get_conn; + + ev_timer_init(&timer_, timeoutcb, 0., 0.); + timer_.data = this; + + ngtcp2_ccerr_default(&last_error_); + + ev_timer_init(&shutdown_timer_, shutdown_timeout_cb, 0., 0.); + shutdown_timer_.data = this; + + ev_prepare_init(&prep_, prepare_cb); + prep_.data = this; + ev_prepare_start(handler_->get_loop(), &prep_); +} + +Http3Upstream::~Http3Upstream() { + auto loop = handler_->get_loop(); + + ev_prepare_stop(loop, &prep_); + ev_timer_stop(loop, &shutdown_timer_); + ev_timer_stop(loop, &timer_); + + nghttp3_conn_del(httpconn_); + + ngtcp2_conn_del(conn_); + + if (qlog_fd_ != -1) { + close(qlog_fd_); + } +} + +namespace { +void log_printf(void *user_data, const char *fmt, ...) { + va_list ap; + std::array<char, 4096> buf; + + va_start(ap, fmt); + auto nwrite = vsnprintf(buf.data(), buf.size(), fmt, ap); + va_end(ap); + + if (static_cast<size_t>(nwrite) >= buf.size()) { + nwrite = buf.size() - 1; + } + + buf[nwrite++] = '\n'; + + while (write(fileno(stderr), buf.data(), nwrite) == -1 && errno == EINTR) + ; +} +} // namespace + +namespace { +void qlog_write(void *user_data, uint32_t flags, const void *data, + size_t datalen) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + upstream->qlog_write(data, datalen, flags & NGTCP2_QLOG_WRITE_FLAG_FIN); +} +} // namespace + +void Http3Upstream::qlog_write(const void *data, size_t datalen, bool fin) { + assert(qlog_fd_ != -1); + + while (write(qlog_fd_, data, datalen) == -1 && errno == EINTR) + ; + + if (fin) { + close(qlog_fd_); + qlog_fd_ = -1; + } +} + +namespace { +void rand(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx) { + util::random_bytes(dest, dest + destlen, + *static_cast<std::mt19937 *>(rand_ctx->native_handle)); +} +} // namespace + +namespace { +int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token, + size_t cidlen, void *user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto handler = upstream->get_client_handler(); + auto worker = handler->get_worker(); + auto conn_handler = worker->get_connection_handler(); + auto &qkms = conn_handler->get_quic_keying_materials(); + auto &qkm = qkms->keying_materials.front(); + + if (generate_quic_connection_id(*cid, cidlen, worker->get_cid_prefix(), + qkm.id, qkm.cid_encryption_key.data()) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + if (generate_quic_stateless_reset_token(token, *cid, qkm.secret.data(), + qkm.secret.size()) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + auto quic_connection_handler = worker->get_quic_connection_handler(); + + quic_connection_handler->add_connection_id(*cid, handler); + + return 0; +} +} // namespace + +namespace { +int remove_connection_id(ngtcp2_conn *conn, const ngtcp2_cid *cid, + void *user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto handler = upstream->get_client_handler(); + auto worker = handler->get_worker(); + auto quic_conn_handler = worker->get_quic_connection_handler(); + + quic_conn_handler->remove_connection_id(*cid); + + return 0; +} +} // namespace + +void Http3Upstream::http_begin_request_headers(int64_t stream_id) { + auto downstream = + std::make_unique<Downstream>(this, handler_->get_mcpool(), stream_id); + nghttp3_conn_set_stream_user_data(httpconn_, stream_id, downstream.get()); + + downstream->reset_upstream_rtimer(); + + handler_->repeat_read_timer(); + + auto &req = downstream->request(); + req.http_major = 3; + req.http_minor = 0; + + add_pending_downstream(std::move(downstream)); +} + +void Http3Upstream::add_pending_downstream( + std::unique_ptr<Downstream> downstream) { + downstream_queue_.add_pending(std::move(downstream)); +} + +namespace { +int recv_stream_data(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t offset, const uint8_t *data, size_t datalen, + void *user_data, void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->recv_stream_data(flags, stream_id, data, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::recv_stream_data(uint32_t flags, int64_t stream_id, + const uint8_t *data, size_t datalen) { + assert(httpconn_); + + auto nconsumed = nghttp3_conn_read_stream( + httpconn_, stream_id, data, datalen, flags & NGTCP2_STREAM_DATA_FLAG_FIN); + if (nconsumed < 0) { + ULOG(ERROR, this) << "nghttp3_conn_read_stream: " + << nghttp3_strerror(nconsumed); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(nconsumed), nullptr, + 0); + return -1; + } + + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); + + return 0; +} + +namespace { +int stream_close(ngtcp2_conn *conn, uint32_t flags, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (!(flags & NGTCP2_STREAM_CLOSE_FLAG_APP_ERROR_CODE_SET)) { + app_error_code = NGHTTP3_H3_NO_ERROR; + } + + if (upstream->stream_close(stream_id, app_error_code) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::stream_close(int64_t stream_id, uint64_t app_error_code) { + if (!httpconn_) { + return 0; + } + + auto rv = nghttp3_conn_close_stream(httpconn_, stream_id, app_error_code); + switch (rv) { + case 0: + break; + case NGHTTP3_ERR_STREAM_NOT_FOUND: + if (ngtcp2_is_bidi_stream(stream_id)) { + ngtcp2_conn_extend_max_streams_bidi(conn_, 1); + } + break; + default: + ULOG(ERROR, this) << "nghttp3_conn_close_stream: " << nghttp3_strerror(rv); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, 0); + return -1; + } + + return 0; +} + +namespace { +int acked_stream_data_offset(ngtcp2_conn *conn, int64_t stream_id, + uint64_t offset, uint64_t datalen, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->acked_stream_data_offset(stream_id, datalen) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::acked_stream_data_offset(int64_t stream_id, + uint64_t datalen) { + if (!httpconn_) { + return 0; + } + + auto rv = nghttp3_conn_add_ack_offset(httpconn_, stream_id, datalen); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_add_ack_offset: " + << nghttp3_strerror(rv); + return -1; + } + + return 0; +} + +namespace { +int extend_max_stream_data(ngtcp2_conn *conn, int64_t stream_id, + uint64_t max_data, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->extend_max_stream_data(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::extend_max_stream_data(int64_t stream_id) { + if (!httpconn_) { + return 0; + } + + auto rv = nghttp3_conn_unblock_stream(httpconn_, stream_id); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_unblock_stream: " + << nghttp3_strerror(rv); + return -1; + } + + return 0; +} + +namespace { +int extend_max_remote_streams_bidi(ngtcp2_conn *conn, uint64_t max_streams, + void *user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + upstream->extend_max_remote_streams_bidi(max_streams); + + return 0; +} +} // namespace + +void Http3Upstream::extend_max_remote_streams_bidi(uint64_t max_streams) { + nghttp3_conn_set_max_client_streams_bidi(httpconn_, max_streams); +} + +namespace { +int stream_reset(ngtcp2_conn *conn, int64_t stream_id, uint64_t final_size, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->http_shutdown_stream_read(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_shutdown_stream_read(int64_t stream_id) { + if (!httpconn_) { + return 0; + } + + auto rv = nghttp3_conn_shutdown_stream_read(httpconn_, stream_id); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_shutdown_stream_read: " + << nghttp3_strerror(rv); + return -1; + } + + return 0; +} + +namespace { +int stream_stop_sending(ngtcp2_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->http_shutdown_stream_read(stream_id) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int handshake_completed(ngtcp2_conn *conn, void *user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->handshake_completed() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::handshake_completed() { + handler_->set_alpn_from_conn(); + + auto alpn = handler_->get_alpn(); + if (alpn.empty()) { + ULOG(ERROR, this) << "NO ALPN was negotiated"; + return -1; + } + + auto path = ngtcp2_conn_get_path(conn_); + + return send_new_token(&path->remote); +} + +namespace { +int path_validation(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, + const ngtcp2_path *old_path, + ngtcp2_path_validation_result res, void *user_data) { + if (res != NGTCP2_PATH_VALIDATION_RESULT_SUCCESS || + !(flags & NGTCP2_PATH_VALIDATION_FLAG_NEW_TOKEN)) { + return 0; + } + + auto upstream = static_cast<Http3Upstream *>(user_data); + if (upstream->send_new_token(&path->remote) != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::send_new_token(const ngtcp2_addr *remote_addr) { + std::array<uint8_t, NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN + 1> token; + size_t tokenlen; + + auto worker = handler_->get_worker(); + auto conn_handler = worker->get_connection_handler(); + auto &qkms = conn_handler->get_quic_keying_materials(); + auto &qkm = qkms->keying_materials.front(); + + if (generate_token(token.data(), tokenlen, remote_addr->addr, + remote_addr->addrlen, qkm.secret.data(), + qkm.secret.size()) != 0) { + return -1; + } + + assert(tokenlen == NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN); + + token[tokenlen++] = qkm.id; + + auto rv = ngtcp2_conn_submit_new_token(conn_, token.data(), tokenlen); + if (rv != 0) { + ULOG(ERROR, this) << "ngtcp2_conn_submit_new_token: " + << ngtcp2_strerror(rv); + return -1; + } + + return 0; +} + +namespace { +int recv_tx_key(ngtcp2_conn *conn, ngtcp2_encryption_level level, + void *user_data) { + if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) { + return 0; + } + + auto upstream = static_cast<Http3Upstream *>(user_data); + if (upstream->setup_httpconn() != 0) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::init(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, + const ngtcp2_pkt_hd &initial_hd, + const ngtcp2_cid *odcid, const uint8_t *token, + size_t tokenlen, ngtcp2_token_type token_type) { + int rv; + + auto worker = handler_->get_worker(); + auto conn_handler = worker->get_connection_handler(); + + auto callbacks = ngtcp2_callbacks{ + nullptr, // client_initial + ngtcp2_crypto_recv_client_initial_cb, + ngtcp2_crypto_recv_crypto_data_cb, + shrpx::handshake_completed, + nullptr, // recv_version_negotiation + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + shrpx::recv_stream_data, + shrpx::acked_stream_data_offset, + nullptr, // stream_open + shrpx::stream_close, + nullptr, // recv_stateless_reset + nullptr, // recv_retry + nullptr, // extend_max_local_streams_bidi + nullptr, // extend_max_local_streams_uni + rand, + get_new_connection_id, + remove_connection_id, + ngtcp2_crypto_update_key_cb, + shrpx::path_validation, + nullptr, // select_preferred_addr + shrpx::stream_reset, + shrpx::extend_max_remote_streams_bidi, + nullptr, // extend_max_remote_streams_uni + shrpx::extend_max_stream_data, + nullptr, // dcid_status + nullptr, // handshake_confirmed + nullptr, // recv_new_token + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + nullptr, // recv_datagram + nullptr, // ack_datagram + nullptr, // lost_datagram + ngtcp2_crypto_get_path_challenge_data_cb, + shrpx::stream_stop_sending, + nullptr, // version_negotiation + nullptr, // recv_rx_key + shrpx::recv_tx_key, + }; + + auto config = get_config(); + auto &quicconf = config->quic; + auto &http3conf = config->http3; + + auto &qkms = conn_handler->get_quic_keying_materials(); + auto &qkm = qkms->keying_materials.front(); + + ngtcp2_cid scid; + + if (generate_quic_connection_id(scid, SHRPX_QUIC_SCIDLEN, + worker->get_cid_prefix(), qkm.id, + qkm.cid_encryption_key.data()) != 0) { + return -1; + } + + ngtcp2_settings settings; + ngtcp2_settings_default(&settings); + if (quicconf.upstream.debug.log) { + settings.log_printf = log_printf; + } + + if (!quicconf.upstream.qlog.dir.empty()) { + auto fd = open_qlog_file(quicconf.upstream.qlog.dir, scid); + if (fd != -1) { + qlog_fd_ = fd; + settings.qlog_write = shrpx::qlog_write; + } + } + + settings.initial_ts = quic_timestamp(); + settings.initial_rtt = static_cast<ngtcp2_tstamp>( + quicconf.upstream.initial_rtt * NGTCP2_SECONDS); + settings.cc_algo = quicconf.upstream.congestion_controller; + settings.max_window = http3conf.upstream.max_connection_window_size; + settings.max_stream_window = http3conf.upstream.max_window_size; + settings.max_tx_udp_payload_size = SHRPX_QUIC_MAX_UDP_PAYLOAD_SIZE; + settings.rand_ctx.native_handle = &worker->get_randgen(); + settings.token = token; + settings.tokenlen = tokenlen; + settings.token_type = token_type; + settings.initial_pkt_num = std::uniform_int_distribution<uint32_t>( + 0, std::numeric_limits<int32_t>::max())(worker->get_randgen()); + + ngtcp2_transport_params params; + ngtcp2_transport_params_default(¶ms); + params.initial_max_streams_bidi = http3conf.upstream.max_concurrent_streams; + // The minimum number of unidirectional streams required for HTTP/3. + params.initial_max_streams_uni = 3; + params.initial_max_data = http3conf.upstream.connection_window_size; + params.initial_max_stream_data_bidi_remote = http3conf.upstream.window_size; + params.initial_max_stream_data_uni = http3conf.upstream.window_size; + params.max_idle_timeout = static_cast<ngtcp2_tstamp>( + quicconf.upstream.timeout.idle * NGTCP2_SECONDS); + +#ifdef NGHTTP2_OPENSSL_IS_BORINGSSL + if (quicconf.upstream.early_data) { + ngtcp2_transport_params early_data_params; + + ngtcp2_transport_params_default(&early_data_params); + + early_data_params.initial_max_stream_data_bidi_local = + params.initial_max_stream_data_bidi_local; + early_data_params.initial_max_stream_data_bidi_remote = + params.initial_max_stream_data_bidi_remote; + early_data_params.initial_max_stream_data_uni = + params.initial_max_stream_data_uni; + early_data_params.initial_max_data = params.initial_max_data; + early_data_params.initial_max_streams_bidi = + params.initial_max_streams_bidi; + early_data_params.initial_max_streams_uni = params.initial_max_streams_uni; + + // TODO include HTTP/3 SETTINGS + + std::array<uint8_t, 128> quic_early_data_ctx; + + auto quic_early_data_ctxlen = ngtcp2_transport_params_encode( + quic_early_data_ctx.data(), quic_early_data_ctx.size(), + &early_data_params); + + assert(quic_early_data_ctxlen > 0); + assert(static_cast<size_t>(quic_early_data_ctxlen) <= + quic_early_data_ctx.size()); + + if (SSL_set_quic_early_data_context(handler_->get_ssl(), + quic_early_data_ctx.data(), + quic_early_data_ctxlen) != 1) { + ULOG(ERROR, this) << "SSL_set_quic_early_data_context failed"; + return -1; + } + } +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + + if (odcid) { + params.original_dcid = *odcid; + params.retry_scid = initial_hd.dcid; + params.retry_scid_present = 1; + } else { + params.original_dcid = initial_hd.dcid; + } + + params.original_dcid_present = 1; + + rv = generate_quic_stateless_reset_token( + params.stateless_reset_token, scid, qkm.secret.data(), qkm.secret.size()); + if (rv != 0) { + ULOG(ERROR, this) << "generate_quic_stateless_reset_token failed"; + return -1; + } + params.stateless_reset_token_present = 1; + + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&local_addr.su.sa), + static_cast<socklen_t>(local_addr.len), + }, + { + const_cast<sockaddr *>(&remote_addr.su.sa), + static_cast<socklen_t>(remote_addr.len), + }, + const_cast<UpstreamAddr *>(faddr), + }; + + rv = ngtcp2_conn_server_new(&conn_, &initial_hd.scid, &scid, &path, + initial_hd.version, &callbacks, &settings, + ¶ms, nullptr, this); + if (rv != 0) { + ULOG(ERROR, this) << "ngtcp2_conn_server_new: " << ngtcp2_strerror(rv); + return -1; + } + + ngtcp2_conn_set_tls_native_handle(conn_, handler_->get_ssl()); + + auto quic_connection_handler = worker->get_quic_connection_handler(); + + if (generate_quic_hashed_connection_id(hashed_scid_, remote_addr, local_addr, + initial_hd.dcid) != 0) { + return -1; + } + + quic_connection_handler->add_connection_id(hashed_scid_, handler_); + quic_connection_handler->add_connection_id(scid, handler_); + + return 0; +} + +int Http3Upstream::on_read() { return 0; } + +int Http3Upstream::on_write() { + int rv; + + if (tx_.send_blocked) { + rv = send_blocked_packet(); + if (rv != 0) { + return -1; + } + + if (tx_.send_blocked) { + return 0; + } + } + + handler_->get_connection()->wlimit.stopw(); + + if (write_streams() != 0) { + return -1; + } + + if (httpconn_ && nghttp3_conn_is_drained(httpconn_)) { + return -1; + } + + reset_timer(); + + return 0; +} + +int Http3Upstream::write_streams() { + std::array<nghttp3_vec, 16> vec; + auto max_udp_payload_size = ngtcp2_conn_get_max_tx_udp_payload_size(conn_); +#ifdef UDP_SEGMENT + auto path_max_udp_payload_size = + ngtcp2_conn_get_path_max_tx_udp_payload_size(conn_); +#endif // UDP_SEGMENT + auto max_pktcnt = ngtcp2_conn_get_send_quantum(conn_) / max_udp_payload_size; + ngtcp2_pkt_info pi, prev_pi; + uint8_t *bufpos = tx_.data.get(); + ngtcp2_path_storage ps, prev_ps; + size_t pktcnt = 0; + int rv; + size_t gso_size = 0; + auto ts = quic_timestamp(); + + ngtcp2_path_storage_zero(&ps); + ngtcp2_path_storage_zero(&prev_ps); + + for (;;) { + int64_t stream_id = -1; + int fin = 0; + nghttp3_ssize sveccnt = 0; + + if (httpconn_ && ngtcp2_conn_get_max_data_left(conn_)) { + sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin, + vec.data(), vec.size()); + if (sveccnt < 0) { + ULOG(ERROR, this) << "nghttp3_conn_writev_stream: " + << nghttp3_strerror(sveccnt); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(sveccnt), + nullptr, 0); + return handle_error(); + } + } + + ngtcp2_ssize ndatalen; + auto v = vec.data(); + auto vcnt = static_cast<size_t>(sveccnt); + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE; + if (fin) { + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + } + + auto nwrite = ngtcp2_conn_writev_stream( + conn_, &ps.path, &pi, bufpos, max_udp_payload_size, &ndatalen, flags, + stream_id, reinterpret_cast<const ngtcp2_vec *>(v), vcnt, ts); + if (nwrite < 0) { + switch (nwrite) { + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + assert(ndatalen == -1); + nghttp3_conn_block_stream(httpconn_, stream_id); + continue; + case NGTCP2_ERR_STREAM_SHUT_WR: + assert(ndatalen == -1); + nghttp3_conn_shutdown_stream_write(httpconn_, stream_id); + continue; + case NGTCP2_ERR_WRITE_MORE: + assert(ndatalen >= 0); + rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen); + if (rv != 0) { + ULOG(ERROR, this) + << "nghttp3_conn_add_write_offset: " << nghttp3_strerror(rv); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + return handle_error(); + } + continue; + } + + assert(ndatalen == -1); + + ULOG(ERROR, this) << "ngtcp2_conn_writev_stream: " + << ngtcp2_strerror(nwrite); + + ngtcp2_ccerr_set_liberr(&last_error_, nwrite, nullptr, 0); + + return handle_error(); + } else if (ndatalen >= 0) { + rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_add_write_offset: " + << nghttp3_strerror(rv); + ngtcp2_ccerr_set_application_error( + &last_error_, nghttp3_err_infer_quic_app_error_code(rv), nullptr, + 0); + return handle_error(); + } + } + + if (nwrite == 0) { + if (bufpos - tx_.data.get()) { + auto faddr = static_cast<UpstreamAddr *>(prev_ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data; + + rv = send_packet(faddr, prev_ps.path.remote.addr, + prev_ps.path.remote.addrlen, prev_ps.path.local.addr, + prev_ps.path.local.addrlen, prev_pi, data, datalen, + gso_size); + if (rv == SHRPX_ERR_SEND_BLOCKED) { + on_send_blocked(faddr, prev_ps.path.remote, prev_ps.path.local, + prev_pi, data, datalen, gso_size); + + signal_write_upstream_addr(faddr); + } + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + + return 0; + } + + bufpos += nwrite; + +#ifdef UDP_SEGMENT + if (pktcnt == 0) { + ngtcp2_path_copy(&prev_ps.path, &ps.path); + prev_pi = pi; + gso_size = nwrite; + } else if (!ngtcp2_path_eq(&prev_ps.path, &ps.path) || + prev_pi.ecn != pi.ecn || + static_cast<size_t>(nwrite) > gso_size || + (gso_size > path_max_udp_payload_size && + static_cast<size_t>(nwrite) != gso_size)) { + auto faddr = static_cast<UpstreamAddr *>(prev_ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data - nwrite; + + rv = send_packet(faddr, prev_ps.path.remote.addr, + prev_ps.path.remote.addrlen, prev_ps.path.local.addr, + prev_ps.path.local.addrlen, prev_pi, data, datalen, + gso_size); + switch (rv) { + case SHRPX_ERR_SEND_BLOCKED: + on_send_blocked(faddr, prev_ps.path.remote, prev_ps.path.local, prev_pi, + data, datalen, gso_size); + + on_send_blocked(static_cast<UpstreamAddr *>(ps.path.user_data), + ps.path.remote, ps.path.local, pi, bufpos - nwrite, + nwrite, 0); + + signal_write_upstream_addr(faddr); + + break; + default: { + auto faddr = static_cast<UpstreamAddr *>(ps.path.user_data); + auto data = bufpos - nwrite; + + rv = send_packet(faddr, ps.path.remote.addr, ps.path.remote.addrlen, + ps.path.local.addr, ps.path.local.addrlen, pi, data, + nwrite, 0); + if (rv == SHRPX_ERR_SEND_BLOCKED) { + on_send_blocked(faddr, ps.path.remote, ps.path.local, pi, data, + nwrite, 0); + + signal_write_upstream_addr(faddr); + } + } + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + + return 0; + } + + if (++pktcnt == max_pktcnt || static_cast<size_t>(nwrite) < gso_size) { + auto faddr = static_cast<UpstreamAddr *>(ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data; + + rv = send_packet(faddr, ps.path.remote.addr, ps.path.remote.addrlen, + ps.path.local.addr, ps.path.local.addrlen, pi, data, + datalen, gso_size); + if (rv == SHRPX_ERR_SEND_BLOCKED) { + on_send_blocked(faddr, ps.path.remote, ps.path.local, pi, data, datalen, + gso_size); + + signal_write_upstream_addr(faddr); + } + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + + return 0; + } +#else // !UDP_SEGMENT + auto faddr = static_cast<UpstreamAddr *>(ps.path.user_data); + auto data = tx_.data.get(); + auto datalen = bufpos - data; + + rv = send_packet(faddr, ps.path.remote.addr, ps.path.remote.addrlen, + ps.path.local.addr, ps.path.local.addrlen, pi, data, + datalen, 0); + if (rv == SHRPX_ERR_SEND_BLOCKED) { + on_send_blocked(faddr, ps.path.remote, ps.path.local, pi, data, datalen, + 0); + + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + + signal_write_upstream_addr(faddr); + + return 0; + } + + if (++pktcnt == max_pktcnt) { + ngtcp2_conn_update_pkt_tx_time(conn_, ts); + + return 0; + } + + bufpos = tx_.data.get(); +#endif // !UDP_SEGMENT + } + + return 0; +} + +int Http3Upstream::on_timeout(Downstream *downstream) { return 0; } + +int Http3Upstream::on_downstream_abort_request(Downstream *downstream, + unsigned int status_code) { + int rv; + + rv = error_reply(downstream, status_code); + + if (rv != 0) { + return -1; + } + + handler_->signal_write(); + + return 0; +} + +int Http3Upstream::on_downstream_abort_request_with_https_redirect( + Downstream *downstream) { + assert(0); + abort(); +} + +namespace { +uint64_t +infer_upstream_shutdown_stream_error_code(uint32_t downstream_error_code) { + // NGHTTP2_REFUSED_STREAM is important because it tells upstream + // client to retry. + switch (downstream_error_code) { + case NGHTTP2_NO_ERROR: + return NGHTTP3_H3_NO_ERROR; + case NGHTTP2_REFUSED_STREAM: + return NGHTTP3_H3_REQUEST_REJECTED; + default: + return NGHTTP3_H3_INTERNAL_ERROR; + } +} +} // namespace + +int Http3Upstream::downstream_read(DownstreamConnection *dconn) { + auto downstream = dconn->get_downstream(); + + if (downstream->get_response_state() == DownstreamState::MSG_RESET) { + // The downstream stream was reset (canceled). In this case, + // RST_STREAM to the upstream and delete downstream connection + // here. Deleting downstream will be taken place at + // on_stream_close_callback. + shutdown_stream(downstream, + infer_upstream_shutdown_stream_error_code( + downstream->get_response_rst_stream_error_code())); + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + } else if (downstream->get_response_state() == + DownstreamState::MSG_BAD_HEADER) { + if (error_reply(downstream, 502) != 0) { + return -1; + } + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + } else { + auto rv = downstream->on_read(); + if (rv == SHRPX_ERR_EOF) { + if (downstream->get_request_header_sent()) { + return downstream_eof(dconn); + } + return SHRPX_ERR_RETRY; + } + if (rv == SHRPX_ERR_DCONN_CANCELED) { + downstream->pop_downstream_connection(); + handler_->signal_write(); + return 0; + } + if (rv != 0) { + if (rv != SHRPX_ERR_NETWORK) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "HTTP parser failure"; + } + } + return downstream_error(dconn, Downstream::EVENT_ERROR); + } + + if (downstream->can_detach_downstream_connection()) { + // Keep-alive + downstream->detach_downstream_connection(); + } + } + + handler_->signal_write(); + + // At this point, downstream may be deleted. + + return 0; +} + +int Http3Upstream::downstream_write(DownstreamConnection *dconn) { + int rv; + rv = dconn->on_write(); + if (rv == SHRPX_ERR_NETWORK) { + return downstream_error(dconn, Downstream::EVENT_ERROR); + } + if (rv != 0) { + return rv; + } + return 0; +} + +int Http3Upstream::downstream_eof(DownstreamConnection *dconn) { + auto downstream = dconn->get_downstream(); + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "EOF. stream_id=" << downstream->get_stream_id(); + } + + // Delete downstream connection. If we don't delete it here, it will + // be pooled in on_stream_close_callback. + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + // downstream will be deleted in on_stream_close_callback. + if (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE) { + // Server may indicate the end of the request by EOF + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Downstream body was ended by EOF"; + } + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + // For tunneled connection, MSG_COMPLETE signals + // downstream_read_data_callback to send RST_STREAM after pending + // response body is sent. This is needed to ensure that RST_STREAM + // is sent after all pending data are sent. + if (on_downstream_body_complete(downstream) != 0) { + return -1; + } + } else if (downstream->get_response_state() != + DownstreamState::MSG_COMPLETE) { + // If stream was not closed, then we set MSG_COMPLETE and let + // on_stream_close_callback delete downstream. + if (error_reply(downstream, 502) != 0) { + return -1; + } + } + handler_->signal_write(); + // At this point, downstream may be deleted. + return 0; +} + +int Http3Upstream::downstream_error(DownstreamConnection *dconn, int events) { + auto downstream = dconn->get_downstream(); + + if (LOG_ENABLED(INFO)) { + if (events & Downstream::EVENT_ERROR) { + DCLOG(INFO, dconn) << "Downstream network/general error"; + } else { + DCLOG(INFO, dconn) << "Timeout"; + } + if (downstream->get_upgraded()) { + DCLOG(INFO, dconn) << "Note: this is tunnel connection"; + } + } + + // Delete downstream connection. If we don't delete it here, it will + // be pooled in on_stream_close_callback. + downstream->pop_downstream_connection(); + // dconn was deleted + dconn = nullptr; + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + // For SSL tunneling, we issue RST_STREAM. For other types of + // stream, we don't have to do anything since response was + // complete. + if (downstream->get_upgraded()) { + shutdown_stream(downstream, NGHTTP3_H3_NO_ERROR); + } + } else { + if (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE) { + if (downstream->get_upgraded()) { + if (on_downstream_body_complete(downstream) != 0) { + return -1; + } + } else { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + } else { + unsigned int status; + if (events & Downstream::EVENT_TIMEOUT) { + if (downstream->get_request_header_sent()) { + status = 504; + } else { + status = 408; + } + } else { + status = 502; + } + if (error_reply(downstream, status) != 0) { + return -1; + } + } + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + } + handler_->signal_write(); + // At this point, downstream may be deleted. + return 0; +} + +ClientHandler *Http3Upstream::get_client_handler() const { return handler_; } + +namespace { +nghttp3_ssize downstream_read_data_callback(nghttp3_conn *conn, + int64_t stream_id, nghttp3_vec *vec, + size_t veccnt, uint32_t *pflags, + void *conn_user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(conn_user_data); + auto downstream = static_cast<Downstream *>(stream_user_data); + + assert(downstream); + + auto body = downstream->get_response_buf(); + + assert(body); + + if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE && + body->rleft_mark() == 0) { + downstream->disable_upstream_wtimer(); + return NGHTTP3_ERR_WOULDBLOCK; + } + + downstream->reset_upstream_wtimer(); + + veccnt = body->riovec_mark(reinterpret_cast<struct iovec *>(vec), veccnt); + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE && + body->rleft_mark() == 0) { + *pflags |= NGHTTP3_DATA_FLAG_EOF; + } + + assert((*pflags & NGHTTP3_DATA_FLAG_EOF) || veccnt); + + downstream->response_sent_body_length += nghttp3_vec_len(vec, veccnt); + + if ((*pflags & NGHTTP3_DATA_FLAG_EOF) && + upstream->shutdown_stream_read(stream_id, NGHTTP3_H3_NO_ERROR) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return veccnt; +} +} // namespace + +int Http3Upstream::on_downstream_header_complete(Downstream *downstream) { + int rv; + + const auto &req = downstream->request(); + auto &resp = downstream->response(); + + auto &balloc = downstream->get_block_allocator(); + + if (LOG_ENABLED(INFO)) { + if (downstream->get_non_final_response()) { + DLOG(INFO, downstream) << "HTTP non-final response header"; + } else { + DLOG(INFO, downstream) << "HTTP response header completed"; + } + } + + auto config = get_config(); + auto &httpconf = config->http; + + if (!config->http2_proxy && !httpconf.no_location_rewrite) { + downstream->rewrite_location_response_header(req.scheme); + } + +#ifdef HAVE_MRUBY + if (!downstream->get_non_final_response()) { + auto dconn = downstream->get_downstream_connection(); + const auto &group = dconn->get_downstream_addr_group(); + if (group) { + const auto &dmruby_ctx = group->shared_addr->mruby_ctx; + + if (dmruby_ctx->run_on_response_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return -1; + } + // Returning -1 will signal deletion of dconn. + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return -1; + } + } + + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_response_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return -1; + } + // Returning -1 will signal deletion of dconn. + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return -1; + } + } +#endif // HAVE_MRUBY + + auto nva = std::vector<nghttp3_nv>(); + // 4 means :status and possible server, via, and set-cookie (for + // affinity cookie) header field. + nva.reserve(resp.fs.headers().size() + 4 + + httpconf.add_response_headers.size()); + + if (downstream->get_non_final_response()) { + auto response_status = http2::stringify_status(balloc, resp.http_status); + + nva.push_back(http3::make_nv_ls_nocopy(":status", response_status)); + + http3::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), + http2::HDOP_STRIP_ALL); + + if (LOG_ENABLED(INFO)) { + log_response_headers(downstream, nva); + } + + rv = nghttp3_conn_submit_info(httpconn_, downstream->get_stream_id(), + nva.data(), nva.size()); + + resp.fs.clear_headers(); + + if (rv != 0) { + ULOG(FATAL, this) << "nghttp3_conn_submit_info() failed"; + return -1; + } + + return 0; + } + + auto striphd_flags = http2::HDOP_STRIP_ALL & ~http2::HDOP_STRIP_VIA; + StringRef response_status; + + if (req.connect_proto == ConnectProto::WEBSOCKET && resp.http_status == 101) { + response_status = http2::stringify_status(balloc, 200); + striphd_flags |= http2::HDOP_STRIP_SEC_WEBSOCKET_ACCEPT; + } else { + response_status = http2::stringify_status(balloc, resp.http_status); + } + + nva.push_back(http3::make_nv_ls_nocopy(":status", response_status)); + + http3::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), striphd_flags); + + if (!config->http2_proxy && !httpconf.no_server_rewrite) { + nva.push_back(http3::make_nv_ls_nocopy("server", httpconf.server_name)); + } else { + auto server = resp.fs.header(http2::HD_SERVER); + if (server) { + nva.push_back(http3::make_nv_ls_nocopy("server", (*server).value)); + } + } + + if (!req.regular_connect_method() || !downstream->get_upgraded()) { + auto affinity_cookie = downstream->get_affinity_cookie_to_send(); + if (affinity_cookie) { + auto dconn = downstream->get_downstream_connection(); + assert(dconn); + auto &group = dconn->get_downstream_addr_group(); + auto &shared_addr = group->shared_addr; + auto &cookieconf = shared_addr->affinity.cookie; + auto secure = + http::require_cookie_secure_attribute(cookieconf.secure, req.scheme); + auto cookie_str = http::create_affinity_cookie( + balloc, cookieconf.name, affinity_cookie, cookieconf.path, secure); + nva.push_back(http3::make_nv_ls_nocopy("set-cookie", cookie_str)); + } + } + + auto via = resp.fs.header(http2::HD_VIA); + if (httpconf.no_via) { + if (via) { + nva.push_back(http3::make_nv_ls_nocopy("via", (*via).value)); + } + } else { + // we don't create more than 16 bytes in + // http::create_via_header_value. + size_t len = 16; + if (via) { + len += via->value.size() + 2; + } + + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + if (via) { + p = std::copy(std::begin(via->value), std::end(via->value), p); + p = util::copy_lit(p, ", "); + } + p = http::create_via_header_value(p, resp.http_major, resp.http_minor); + *p = '\0'; + + nva.push_back(http3::make_nv_ls_nocopy("via", StringRef{iov.base, p})); + } + + for (auto &p : httpconf.add_response_headers) { + nva.push_back(http3::make_nv_nocopy(p.name, p.value)); + } + + if (LOG_ENABLED(INFO)) { + log_response_headers(downstream, nva); + } + + auto priority = resp.fs.header(http2::HD_PRIORITY); + if (priority) { + nghttp3_pri pri; + + if (nghttp3_conn_get_stream_priority(httpconn_, &pri, + downstream->get_stream_id()) == 0 && + nghttp3_pri_parse_priority(&pri, priority->value.byte(), + priority->value.size()) == 0) { + rv = nghttp3_conn_set_server_stream_priority( + httpconn_, downstream->get_stream_id(), &pri); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_set_server_stream_priority: " + << nghttp3_strerror(rv); + } + } + } + + nghttp3_data_reader data_read; + data_read.read_data = downstream_read_data_callback; + + nghttp3_data_reader *data_readptr; + + if (downstream->expect_response_body() || + downstream->expect_response_trailer()) { + data_readptr = &data_read; + } else { + data_readptr = nullptr; + } + + rv = nghttp3_conn_submit_response(httpconn_, downstream->get_stream_id(), + nva.data(), nva.size(), data_readptr); + if (rv != 0) { + ULOG(FATAL, this) << "nghttp3_conn_submit_response() failed"; + return -1; + } + + if (data_readptr) { + downstream->reset_upstream_wtimer(); + } else if (shutdown_stream_read(downstream->get_stream_id(), + NGHTTP3_H3_NO_ERROR) != 0) { + return -1; + } + + return 0; +} + +int Http3Upstream::on_downstream_body(Downstream *downstream, + const uint8_t *data, size_t len, + bool flush) { + auto body = downstream->get_response_buf(); + body->append(data, len); + + if (flush) { + nghttp3_conn_resume_stream(httpconn_, downstream->get_stream_id()); + + downstream->ensure_upstream_wtimer(); + } + + return 0; +} + +int Http3Upstream::on_downstream_body_complete(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "HTTP response completed"; + } + + auto &resp = downstream->response(); + + if (!downstream->validate_response_recv_body_length()) { + shutdown_stream(downstream, NGHTTP3_H3_GENERAL_PROTOCOL_ERROR); + resp.connection_close = true; + return 0; + } + + if (!downstream->get_upgraded()) { + const auto &trailers = resp.fs.trailers(); + if (!trailers.empty()) { + std::vector<nghttp3_nv> nva; + nva.reserve(trailers.size()); + http3::copy_headers_to_nva_nocopy(nva, trailers, http2::HDOP_STRIP_ALL); + if (!nva.empty()) { + auto rv = nghttp3_conn_submit_trailers( + httpconn_, downstream->get_stream_id(), nva.data(), nva.size()); + if (rv != 0) { + ULOG(FATAL, this) << "nghttp3_conn_submit_trailers() failed: " + << nghttp3_strerror(rv); + return -1; + } + } + } + } + + nghttp3_conn_resume_stream(httpconn_, downstream->get_stream_id()); + downstream->ensure_upstream_wtimer(); + + return 0; +} + +void Http3Upstream::on_handler_delete() { + for (auto d = downstream_queue_.get_downstreams(); d; d = d->dlnext) { + if (d->get_dispatch_state() == DispatchState::ACTIVE && + d->accesslog_ready()) { + handler_->write_accesslog(d); + } + } + + auto worker = handler_->get_worker(); + auto quic_conn_handler = worker->get_quic_connection_handler(); + + std::vector<ngtcp2_cid> scids(ngtcp2_conn_get_scid(conn_, nullptr) + 1); + ngtcp2_conn_get_scid(conn_, scids.data()); + scids.back() = hashed_scid_; + + for (auto &cid : scids) { + quic_conn_handler->remove_connection_id(cid); + } + + if (retry_close_ || last_error_.type == NGTCP2_CCERR_TYPE_IDLE_CLOSE) { + return; + } + + // If this is not idle close, send CONNECTION_CLOSE. + if (!ngtcp2_conn_in_closing_period(conn_) && + !ngtcp2_conn_in_draining_period(conn_)) { + ngtcp2_path_storage ps; + ngtcp2_pkt_info pi; + conn_close_.resize(SHRPX_QUIC_CONN_CLOSE_PKTLEN); + + ngtcp2_path_storage_zero(&ps); + + ngtcp2_ccerr ccerr; + ngtcp2_ccerr_default(&ccerr); + + if (worker->get_graceful_shutdown() && + !ngtcp2_conn_get_handshake_completed(conn_)) { + ccerr.error_code = NGTCP2_CONNECTION_REFUSED; + } + + auto nwrite = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, conn_close_.data(), conn_close_.size(), &ccerr, + quic_timestamp()); + if (nwrite < 0) { + if (nwrite != NGTCP2_ERR_INVALID_STATE) { + ULOG(ERROR, this) << "ngtcp2_conn_write_connection_close: " + << ngtcp2_strerror(nwrite); + } + + return; + } + + conn_close_.resize(nwrite); + + send_packet(static_cast<UpstreamAddr *>(ps.path.user_data), + ps.path.remote.addr, ps.path.remote.addrlen, ps.path.local.addr, + ps.path.local.addrlen, pi, conn_close_.data(), nwrite, 0); + } + + auto d = + static_cast<ev_tstamp>(ngtcp2_conn_get_pto(conn_) * 3) / NGTCP2_SECONDS; + + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Enter close-wait period " << d << "s with " + << conn_close_.size() << " bytes sentinel packet"; + } + + auto cw = std::make_unique<CloseWait>(worker, std::move(scids), + std::move(conn_close_), d); + + quic_conn_handler->add_close_wait(cw.release()); +} + +int Http3Upstream::on_downstream_reset(Downstream *downstream, bool no_retry) { + int rv; + + if (downstream->get_dispatch_state() != DispatchState::ACTIVE) { + // This is error condition when we failed push_request_headers() + // in initiate_downstream(). Otherwise, we have + // DispatchState::ACTIVE state, or we did not set + // DownstreamConnection. + downstream->pop_downstream_connection(); + handler_->signal_write(); + + return 0; + } + + if (!downstream->request_submission_ready()) { + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + // We have got all response body already. Send it off. + downstream->pop_downstream_connection(); + return 0; + } + // pushed stream is handled here + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + downstream->pop_downstream_connection(); + + handler_->signal_write(); + + return 0; + } + + downstream->pop_downstream_connection(); + + downstream->add_retry(); + + std::unique_ptr<DownstreamConnection> dconn; + + rv = 0; + + if (no_retry || downstream->no_more_retry()) { + goto fail; + } + + // downstream connection is clean; we can retry with new + // downstream connection. + + for (;;) { + auto dconn = handler_->get_downstream_connection(rv, downstream); + if (!dconn) { + goto fail; + } + + rv = downstream->attach_downstream_connection(std::move(dconn)); + if (rv == 0) { + break; + } + } + + rv = downstream->push_request_headers(); + if (rv != 0) { + goto fail; + } + + return 0; + +fail: + if (rv == SHRPX_ERR_TLS_REQUIRED) { + assert(0); + abort(); + } + + rv = on_downstream_abort_request(downstream, 502); + if (rv != 0) { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + downstream->pop_downstream_connection(); + + handler_->signal_write(); + + return 0; +} + +void Http3Upstream::pause_read(IOCtrlReason reason) {} + +int Http3Upstream::resume_read(IOCtrlReason reason, Downstream *downstream, + size_t consumed) { + consume(downstream->get_stream_id(), consumed); + + auto &req = downstream->request(); + + req.consume(consumed); + + handler_->signal_write(); + + return 0; +} + +int Http3Upstream::send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) { + int rv; + + nghttp3_data_reader data_read, *data_read_ptr = nullptr; + + if (bodylen) { + data_read.read_data = downstream_read_data_callback; + data_read_ptr = &data_read; + } + + const auto &resp = downstream->response(); + auto config = get_config(); + auto &httpconf = config->http; + + auto &balloc = downstream->get_block_allocator(); + + const auto &headers = resp.fs.headers(); + auto nva = std::vector<nghttp3_nv>(); + // 2 for :status and server + nva.reserve(2 + headers.size() + httpconf.add_response_headers.size()); + + auto response_status = http2::stringify_status(balloc, resp.http_status); + + nva.push_back(http3::make_nv_ls_nocopy(":status", response_status)); + + for (auto &kv : headers) { + if (kv.name.empty() || kv.name[0] == ':') { + continue; + } + switch (kv.token) { + case http2::HD_CONNECTION: + case http2::HD_KEEP_ALIVE: + case http2::HD_PROXY_CONNECTION: + case http2::HD_TE: + case http2::HD_TRANSFER_ENCODING: + case http2::HD_UPGRADE: + continue; + } + nva.push_back(http3::make_nv_nocopy(kv.name, kv.value, kv.no_index)); + } + + if (!resp.fs.header(http2::HD_SERVER)) { + nva.push_back(http3::make_nv_ls_nocopy("server", config->http.server_name)); + } + + for (auto &p : httpconf.add_response_headers) { + nva.push_back(http3::make_nv_nocopy(p.name, p.value)); + } + + rv = nghttp3_conn_submit_response(httpconn_, downstream->get_stream_id(), + nva.data(), nva.size(), data_read_ptr); + if (nghttp3_err_is_fatal(rv)) { + ULOG(FATAL, this) << "nghttp3_conn_submit_response() failed: " + << nghttp3_strerror(rv); + return -1; + } + + auto buf = downstream->get_response_buf(); + + buf->append(body, bodylen); + + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + if (data_read_ptr) { + downstream->reset_upstream_wtimer(); + } + + if (shutdown_stream_read(downstream->get_stream_id(), NGHTTP3_H3_NO_ERROR) != + 0) { + return -1; + } + + return 0; +} + +int Http3Upstream::initiate_push(Downstream *downstream, const StringRef &uri) { + return 0; +} + +int Http3Upstream::response_riovec(struct iovec *iov, int iovcnt) const { + return 0; +} + +void Http3Upstream::response_drain(size_t n) {} + +bool Http3Upstream::response_empty() const { return false; } + +Downstream * +Http3Upstream::on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + return nullptr; +} + +int Http3Upstream::on_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + return 0; +} + +bool Http3Upstream::push_enabled() const { return false; } + +void Http3Upstream::cancel_premature_downstream( + Downstream *promised_downstream) {} + +int Http3Upstream::on_read(const UpstreamAddr *faddr, + const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen) { + int rv; + + auto path = ngtcp2_path{ + { + const_cast<sockaddr *>(&local_addr.su.sa), + static_cast<socklen_t>(local_addr.len), + }, + { + const_cast<sockaddr *>(&remote_addr.su.sa), + static_cast<socklen_t>(remote_addr.len), + }, + const_cast<UpstreamAddr *>(faddr), + }; + + rv = ngtcp2_conn_read_pkt(conn_, &path, &pi, data, datalen, quic_timestamp()); + if (rv != 0) { + switch (rv) { + case NGTCP2_ERR_DRAINING: + return -1; + case NGTCP2_ERR_RETRY: { + auto worker = handler_->get_worker(); + auto quic_conn_handler = worker->get_quic_connection_handler(); + + if (worker->get_graceful_shutdown()) { + ngtcp2_ccerr_set_transport_error(&last_error_, + NGTCP2_CONNECTION_REFUSED, nullptr, 0); + + return handle_error(); + } + + ngtcp2_version_cid vc; + + rv = + ngtcp2_pkt_decode_version_cid(&vc, data, datalen, SHRPX_QUIC_SCIDLEN); + if (rv != 0) { + return -1; + } + + retry_close_ = true; + + quic_conn_handler->send_retry(handler_->get_upstream_addr(), vc.version, + vc.dcid, vc.dcidlen, vc.scid, vc.scidlen, + remote_addr, local_addr, datalen * 3); + + return -1; + } + case NGTCP2_ERR_CRYPTO: + if (!last_error_.error_code) { + ngtcp2_ccerr_set_tls_alert( + &last_error_, ngtcp2_conn_get_tls_alert(conn_), nullptr, 0); + } + break; + case NGTCP2_ERR_DROP_CONN: + return -1; + default: + if (!last_error_.error_code) { + ngtcp2_ccerr_set_liberr(&last_error_, rv, nullptr, 0); + } + } + + ULOG(ERROR, this) << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv); + + return handle_error(); + } + + return 0; +} + +int Http3Upstream::send_packet(const UpstreamAddr *faddr, + const sockaddr *remote_sa, size_t remote_salen, + const sockaddr *local_sa, size_t local_salen, + const ngtcp2_pkt_info &pi, const uint8_t *data, + size_t datalen, size_t gso_size) { + auto rv = quic_send_packet(faddr, remote_sa, remote_salen, local_sa, + local_salen, pi, data, datalen, gso_size); + switch (rv) { + case 0: + return 0; + // With GSO, sendmsg may fail with EINVAL if UDP payload is too + // large. + case -EINVAL: + case -EMSGSIZE: + // Let the packet lost. + break; + case -EAGAIN: +#if EAGAIN != EWOULDBLOCK + case -EWOULDBLOCK: +#endif // EAGAIN != EWOULDBLOCK + return SHRPX_ERR_SEND_BLOCKED; + default: + break; + } + + return -1; +} + +void Http3Upstream::on_send_blocked(const UpstreamAddr *faddr, + const ngtcp2_addr &remote_addr, + const ngtcp2_addr &local_addr, + const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen, + size_t gso_size) { + assert(tx_.num_blocked || !tx_.send_blocked); + assert(tx_.num_blocked < 2); + + tx_.send_blocked = true; + + auto &p = tx_.blocked[tx_.num_blocked++]; + + memcpy(&p.local_addr.su, local_addr.addr, local_addr.addrlen); + memcpy(&p.remote_addr.su, remote_addr.addr, remote_addr.addrlen); + + p.local_addr.len = local_addr.addrlen; + p.remote_addr.len = remote_addr.addrlen; + p.faddr = faddr; + p.pi = pi; + p.data = data; + p.datalen = datalen; + p.gso_size = gso_size; +} + +int Http3Upstream::send_blocked_packet() { + int rv; + + assert(tx_.send_blocked); + + for (; tx_.num_blocked_sent < tx_.num_blocked; ++tx_.num_blocked_sent) { + auto &p = tx_.blocked[tx_.num_blocked_sent]; + + rv = send_packet(p.faddr, &p.remote_addr.su.sa, p.remote_addr.len, + &p.local_addr.su.sa, p.local_addr.len, p.pi, p.data, + p.datalen, p.gso_size); + if (rv == SHRPX_ERR_SEND_BLOCKED) { + signal_write_upstream_addr(p.faddr); + + return 0; + } + } + + tx_.send_blocked = false; + tx_.num_blocked = 0; + tx_.num_blocked_sent = 0; + + return 0; +} + +void Http3Upstream::signal_write_upstream_addr(const UpstreamAddr *faddr) { + auto conn = handler_->get_connection(); + + if (faddr->fd != conn->wev.fd) { + if (ev_is_active(&conn->wev)) { + ev_io_stop(handler_->get_loop(), &conn->wev); + } + + ev_io_set(&conn->wev, faddr->fd, EV_WRITE); + } + + conn->wlimit.startw(); +} + +int Http3Upstream::handle_error() { + if (ngtcp2_conn_in_closing_period(conn_) || + ngtcp2_conn_in_draining_period(conn_)) { + return -1; + } + + ngtcp2_path_storage ps; + ngtcp2_pkt_info pi; + + ngtcp2_path_storage_zero(&ps); + + auto ts = quic_timestamp(); + + conn_close_.resize(SHRPX_QUIC_CONN_CLOSE_PKTLEN); + + auto nwrite = ngtcp2_conn_write_connection_close( + conn_, &ps.path, &pi, conn_close_.data(), conn_close_.size(), + &last_error_, ts); + if (nwrite < 0) { + ULOG(ERROR, this) << "ngtcp2_conn_write_connection_close: " + << ngtcp2_strerror(nwrite); + return -1; + } + + conn_close_.resize(nwrite); + + if (nwrite == 0) { + return -1; + } + + send_packet(static_cast<UpstreamAddr *>(ps.path.user_data), + ps.path.remote.addr, ps.path.remote.addrlen, ps.path.local.addr, + ps.path.local.addrlen, pi, conn_close_.data(), nwrite, 0); + + return -1; +} + +int Http3Upstream::handle_expiry() { + int rv; + + auto ts = quic_timestamp(); + + rv = ngtcp2_conn_handle_expiry(conn_, ts); + if (rv != 0) { + if (rv == NGTCP2_ERR_IDLE_CLOSE) { + ULOG(INFO, this) << "Idle connection timeout"; + } else { + ULOG(ERROR, this) << "ngtcp2_conn_handle_expiry: " << ngtcp2_strerror(rv); + } + ngtcp2_ccerr_set_liberr(&last_error_, rv, nullptr, 0); + return handle_error(); + } + + return 0; +} + +void Http3Upstream::reset_timer() { + auto ts = quic_timestamp(); + auto expiry_ts = ngtcp2_conn_get_expiry(conn_); + auto loop = handler_->get_loop(); + + if (expiry_ts <= ts) { + ev_feed_event(loop, &timer_, EV_TIMER); + return; + } + + timer_.repeat = static_cast<ev_tstamp>(expiry_ts - ts) / NGTCP2_SECONDS; + + ev_timer_again(loop, &timer_); +} + +namespace { +int http_deferred_consume(nghttp3_conn *conn, int64_t stream_id, + size_t nconsumed, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + upstream->consume(stream_id, nconsumed); + + return 0; +} +} // namespace + +namespace { +int http_acked_stream_data(nghttp3_conn *conn, int64_t stream_id, + uint64_t datalen, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto downstream = static_cast<Downstream *>(stream_user_data); + + assert(downstream); + + if (upstream->http_acked_stream_data(downstream, datalen) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_acked_stream_data(Downstream *downstream, + uint64_t datalen) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Stream " << downstream->get_stream_id() << " " + << datalen << " bytes acknowledged"; + } + + auto body = downstream->get_response_buf(); + auto drained = body->drain_mark(datalen); + (void)drained; + + assert(datalen == drained); + + if (downstream->resume_read(SHRPX_NO_BUFFER, datalen) != 0) { + return -1; + } + + return 0; +} + +namespace { +int http_begin_request_headers(nghttp3_conn *conn, int64_t stream_id, + void *user_data, void *stream_user_data) { + if (!ngtcp2_is_bidi_stream(stream_id)) { + return 0; + } + + auto upstream = static_cast<Http3Upstream *>(user_data); + upstream->http_begin_request_headers(stream_id); + + return 0; +} +} // namespace + +namespace { +int http_recv_request_header(nghttp3_conn *conn, int64_t stream_id, + int32_t token, nghttp3_rcbuf *name, + nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto downstream = static_cast<Downstream *>(stream_user_data); + + if (!downstream || downstream->get_stop_reading()) { + return 0; + } + + if (upstream->http_recv_request_header(downstream, token, name, value, flags, + /* trailer = */ false) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +namespace { +int http_recv_request_trailer(nghttp3_conn *conn, int64_t stream_id, + int32_t token, nghttp3_rcbuf *name, + nghttp3_rcbuf *value, uint8_t flags, + void *user_data, void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto downstream = static_cast<Downstream *>(stream_user_data); + + if (!downstream || downstream->get_stop_reading()) { + return 0; + } + + if (upstream->http_recv_request_header(downstream, token, name, value, flags, + /* trailer = */ true) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_recv_request_header(Downstream *downstream, + int32_t h3token, + nghttp3_rcbuf *name, + nghttp3_rcbuf *value, uint8_t flags, + bool trailer) { + auto namebuf = nghttp3_rcbuf_get_buf(name); + auto valuebuf = nghttp3_rcbuf_get_buf(value); + auto &req = downstream->request(); + auto config = get_config(); + auto &httpconf = config->http; + + if (req.fs.buffer_size() + namebuf.len + valuebuf.len > + httpconf.request_header_field_buffer || + req.fs.num_fields() >= httpconf.max_request_header_fields) { + downstream->set_stop_reading(true); + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Too large or many header field size=" + << req.fs.buffer_size() + namebuf.len + valuebuf.len + << ", num=" << req.fs.num_fields() + 1; + } + + // just ignore if this is a trailer part. + if (trailer) { + return 0; + } + + if (error_reply(downstream, 431) != 0) { + return -1; + } + + return 0; + } + + auto token = http2::lookup_token(namebuf.base, namebuf.len); + auto no_index = flags & NGHTTP3_NV_FLAG_NEVER_INDEX; + + downstream->add_rcbuf(name); + downstream->add_rcbuf(value); + + if (trailer) { + req.fs.add_trailer_token(StringRef{namebuf.base, namebuf.len}, + StringRef{valuebuf.base, valuebuf.len}, no_index, + token); + return 0; + } + + req.fs.add_header_token(StringRef{namebuf.base, namebuf.len}, + StringRef{valuebuf.base, valuebuf.len}, no_index, + token); + return 0; +} + +namespace { +int http_end_request_headers(nghttp3_conn *conn, int64_t stream_id, int fin, + void *user_data, void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto handler = upstream->get_client_handler(); + auto downstream = static_cast<Downstream *>(stream_user_data); + + if (!downstream || downstream->get_stop_reading()) { + return 0; + } + + if (upstream->http_end_request_headers(downstream, fin) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + downstream->reset_upstream_rtimer(); + handler->stop_read_timer(); + + return 0; +} +} // namespace + +int Http3Upstream::http_end_request_headers(Downstream *downstream, int fin) { + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + auto &req = downstream->request(); + req.tstamp = lgconf->tstamp; + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + + auto &nva = req.fs.headers(); + + if (LOG_ENABLED(INFO)) { + std::stringstream ss; + for (auto &nv : nva) { + if (nv.name == "authorization") { + ss << TTY_HTTP_HD << nv.name << TTY_RST << ": <redacted>\n"; + continue; + } + ss << TTY_HTTP_HD << nv.name << TTY_RST << ": " << nv.value << "\n"; + } + ULOG(INFO, this) << "HTTP request headers. stream_id=" + << downstream->get_stream_id() << "\n" + << ss.str(); + } + + auto content_length = req.fs.header(http2::HD_CONTENT_LENGTH); + if (content_length) { + // libnghttp3 guarantees this can be parsed + req.fs.content_length = util::parse_uint(content_length->value); + } + + // presence of mandatory header fields are guaranteed by libnghttp3. + auto authority = req.fs.header(http2::HD__AUTHORITY); + auto path = req.fs.header(http2::HD__PATH); + auto method = req.fs.header(http2::HD__METHOD); + auto scheme = req.fs.header(http2::HD__SCHEME); + + auto method_token = http2::lookup_method_token(method->value); + if (method_token == -1) { + if (error_reply(downstream, 501) != 0) { + return -1; + } + return 0; + } + + auto faddr = handler_->get_upstream_addr(); + + auto config = get_config(); + + // For HTTP/2 proxy, we require :authority. + if (method_token != HTTP_CONNECT && config->http2_proxy && + faddr->alt_mode == UpstreamAltMode::NONE && !authority) { + shutdown_stream(downstream, NGHTTP3_H3_GENERAL_PROTOCOL_ERROR); + return 0; + } + + req.method = method_token; + if (scheme) { + req.scheme = scheme->value; + } + + // nghttp2 library guarantees either :authority or host exist + if (!authority) { + req.no_authority = true; + authority = req.fs.header(http2::HD_HOST); + } + + if (authority) { + req.authority = authority->value; + } + + if (path) { + if (method_token == HTTP_OPTIONS && + path->value == StringRef::from_lit("*")) { + // Server-wide OPTIONS request. Path is empty. + } else if (config->http2_proxy && + faddr->alt_mode == UpstreamAltMode::NONE) { + req.path = path->value; + } else { + req.path = http2::rewrite_clean_path(downstream->get_block_allocator(), + path->value); + } + } + + auto connect_proto = req.fs.header(http2::HD__PROTOCOL); + if (connect_proto) { + if (connect_proto->value != "websocket") { + if (error_reply(downstream, 400) != 0) { + return -1; + } + return 0; + } + req.connect_proto = ConnectProto::WEBSOCKET; + } + + if (!fin) { + req.http2_expect_body = true; + } else if (req.fs.content_length == -1) { + req.fs.content_length = 0; + } + + downstream->inspect_http2_request(); + + downstream->set_request_state(DownstreamState::HEADER_COMPLETE); + + if (config->http.require_http_scheme && + !http::check_http_scheme(req.scheme, /* encrypted = */ true)) { + if (error_reply(downstream, 400) != 0) { + return -1; + } + } + +#ifdef HAVE_MRUBY + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + return -1; + } + return 0; + } +#endif // HAVE_MRUBY + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + + start_downstream(downstream); + + return 0; +} + +void Http3Upstream::start_downstream(Downstream *downstream) { + if (downstream_queue_.can_activate(downstream->request().authority)) { + initiate_downstream(downstream); + return; + } + + downstream_queue_.mark_blocked(downstream); +} + +void Http3Upstream::initiate_downstream(Downstream *downstream) { + int rv; + +#ifdef HAVE_MRUBY + DownstreamConnection *dconn_ptr; +#endif // HAVE_MRUBY + + for (;;) { + auto dconn = handler_->get_downstream_connection(rv, downstream); + if (!dconn) { + if (rv == SHRPX_ERR_TLS_REQUIRED) { + assert(0); + abort(); + } + + rv = error_reply(downstream, 502); + if (rv != 0) { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + + downstream->set_request_state(DownstreamState::CONNECT_FAIL); + downstream_queue_.mark_failure(downstream); + + return; + } + +#ifdef HAVE_MRUBY + dconn_ptr = dconn.get(); +#endif // HAVE_MRUBY + rv = downstream->attach_downstream_connection(std::move(dconn)); + if (rv == 0) { + break; + } + } + +#ifdef HAVE_MRUBY + const auto &group = dconn_ptr->get_downstream_addr_group(); + if (group) { + const auto &mruby_ctx = group->shared_addr->mruby_ctx; + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + if (error_reply(downstream, 500) != 0) { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + + downstream_queue_.mark_failure(downstream); + + return; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return; + } + } +#endif // HAVE_MRUBY + + rv = downstream->push_request_headers(); + if (rv != 0) { + + if (error_reply(downstream, 502) != 0) { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + + downstream_queue_.mark_failure(downstream); + + return; + } + + downstream_queue_.mark_active(downstream); + + auto &req = downstream->request(); + if (!req.http2_expect_body) { + rv = downstream->end_upload_data(); + if (rv != 0) { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + } +} + +namespace { +int http_recv_data(nghttp3_conn *conn, int64_t stream_id, const uint8_t *data, + size_t datalen, void *user_data, void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto downstream = static_cast<Downstream *>(stream_user_data); + + if (upstream->http_recv_data(downstream, data, datalen) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_recv_data(Downstream *downstream, const uint8_t *data, + size_t datalen) { + downstream->reset_upstream_rtimer(); + + if (downstream->push_upload_data_chunk(data, datalen) != 0) { + if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE) { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + + consume(downstream->get_stream_id(), datalen); + + return 0; + } + + return 0; +} + +namespace { +int http_end_stream(nghttp3_conn *conn, int64_t stream_id, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + auto downstream = static_cast<Downstream *>(stream_user_data); + + if (!downstream || downstream->get_stop_reading()) { + return 0; + } + + if (upstream->http_end_stream(downstream) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_end_stream(Downstream *downstream) { + downstream->disable_upstream_rtimer(); + + if (downstream->end_upload_data() != 0) { + if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE) { + shutdown_stream(downstream, NGHTTP3_H3_INTERNAL_ERROR); + } + } + + downstream->set_request_state(DownstreamState::MSG_COMPLETE); + + return 0; +} + +namespace { +int http_stream_close(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *conn_user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(conn_user_data); + auto downstream = static_cast<Downstream *>(stream_user_data); + + if (!downstream) { + return 0; + } + + if (upstream->http_stream_close(downstream, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_stream_close(Downstream *downstream, + uint64_t app_error_code) { + auto stream_id = downstream->get_stream_id(); + + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Stream stream_id=" << stream_id + << " is being closed with app_error_code=" + << app_error_code; + + auto body = downstream->get_response_buf(); + + ULOG(INFO, this) << "response unacked_left=" << body->rleft() + << " not_sent=" << body->rleft_mark(); + } + + auto &req = downstream->request(); + + consume(stream_id, req.unconsumed_body_length); + + req.unconsumed_body_length = 0; + + ngtcp2_conn_extend_max_streams_bidi(conn_, 1); + + if (downstream->get_request_state() == DownstreamState::CONNECT_FAIL) { + remove_downstream(downstream); + // downstream was deleted + + return 0; + } + + if (downstream->can_detach_downstream_connection()) { + // Keep-alive + downstream->detach_downstream_connection(); + } + + downstream->set_request_state(DownstreamState::STREAM_CLOSED); + + // At this point, downstream read may be paused. + + // If shrpx_downstream::push_request_headers() failed, the + // error is handled here. + remove_downstream(downstream); + // downstream was deleted + + return 0; +} + +namespace { +int http_stop_sending(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->http_stop_sending(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_stop_sending(int64_t stream_id, + uint64_t app_error_code) { + auto rv = + ngtcp2_conn_shutdown_stream_read(conn_, 0, stream_id, app_error_code); + if (ngtcp2_err_is_fatal(rv)) { + ULOG(ERROR, this) << "ngtcp2_conn_shutdown_stream_read: " + << ngtcp2_strerror(rv); + return -1; + } + + return 0; +} + +namespace { +int http_reset_stream(nghttp3_conn *conn, int64_t stream_id, + uint64_t app_error_code, void *user_data, + void *stream_user_data) { + auto upstream = static_cast<Http3Upstream *>(user_data); + + if (upstream->http_reset_stream(stream_id, app_error_code) != 0) { + return NGHTTP3_ERR_CALLBACK_FAILURE; + } + + return 0; +} +} // namespace + +int Http3Upstream::http_reset_stream(int64_t stream_id, + uint64_t app_error_code) { + auto rv = + ngtcp2_conn_shutdown_stream_write(conn_, 0, stream_id, app_error_code); + if (ngtcp2_err_is_fatal(rv)) { + ULOG(ERROR, this) << "ngtcp2_conn_shutdown_stream_write: " + << ngtcp2_strerror(rv); + return -1; + } + + return 0; +} + +int Http3Upstream::setup_httpconn() { + int rv; + + if (ngtcp2_conn_get_streams_uni_left(conn_) < 3) { + return -1; + } + + nghttp3_callbacks callbacks{ + shrpx::http_acked_stream_data, + shrpx::http_stream_close, + shrpx::http_recv_data, + http_deferred_consume, + shrpx::http_begin_request_headers, + shrpx::http_recv_request_header, + shrpx::http_end_request_headers, + nullptr, // begin_trailers + shrpx::http_recv_request_trailer, + nullptr, // end_trailers + shrpx::http_stop_sending, + shrpx::http_end_stream, + shrpx::http_reset_stream, + }; + + auto config = get_config(); + + nghttp3_settings settings; + nghttp3_settings_default(&settings); + settings.qpack_max_dtable_capacity = 4_k; + + if (!config->http2_proxy) { + settings.enable_connect_protocol = 1; + } + + auto mem = nghttp3_mem_default(); + + rv = nghttp3_conn_server_new(&httpconn_, &callbacks, &settings, mem, this); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_server_new: " << nghttp3_strerror(rv); + return -1; + } + + auto params = ngtcp2_conn_get_local_transport_params(conn_); + + nghttp3_conn_set_max_client_streams_bidi(httpconn_, + params->initial_max_streams_bidi); + + int64_t ctrl_stream_id; + + rv = ngtcp2_conn_open_uni_stream(conn_, &ctrl_stream_id, nullptr); + if (rv != 0) { + ULOG(ERROR, this) << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv); + return -1; + } + + rv = nghttp3_conn_bind_control_stream(httpconn_, ctrl_stream_id); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_bind_control_stream: " + << nghttp3_strerror(rv); + return -1; + } + + int64_t qpack_enc_stream_id, qpack_dec_stream_id; + + rv = ngtcp2_conn_open_uni_stream(conn_, &qpack_enc_stream_id, nullptr); + if (rv != 0) { + ULOG(ERROR, this) << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv); + return -1; + } + + rv = ngtcp2_conn_open_uni_stream(conn_, &qpack_dec_stream_id, nullptr); + if (rv != 0) { + ULOG(ERROR, this) << "ngtcp2_conn_open_uni_stream: " << ngtcp2_strerror(rv); + return -1; + } + + rv = nghttp3_conn_bind_qpack_streams(httpconn_, qpack_enc_stream_id, + qpack_dec_stream_id); + if (rv != 0) { + ULOG(ERROR, this) << "nghttp3_conn_bind_qpack_streams: " + << nghttp3_strerror(rv); + return -1; + } + + return 0; +} + +int Http3Upstream::error_reply(Downstream *downstream, + unsigned int status_code) { + int rv; + auto &resp = downstream->response(); + + auto &balloc = downstream->get_block_allocator(); + + auto html = http::create_error_html(balloc, status_code); + resp.http_status = status_code; + auto body = downstream->get_response_buf(); + body->append(html); + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + nghttp3_data_reader data_read; + data_read.read_data = downstream_read_data_callback; + + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + + auto response_status = http2::stringify_status(balloc, status_code); + auto content_length = util::make_string_ref_uint(balloc, html.size()); + auto date = make_string_ref(balloc, lgconf->tstamp->time_http); + + auto nva = std::array<nghttp3_nv, 5>{ + {http3::make_nv_ls_nocopy(":status", response_status), + http3::make_nv_ll("content-type", "text/html; charset=UTF-8"), + http3::make_nv_ls_nocopy("server", get_config()->http.server_name), + http3::make_nv_ls_nocopy("content-length", content_length), + http3::make_nv_ls_nocopy("date", date)}}; + + rv = nghttp3_conn_submit_response(httpconn_, downstream->get_stream_id(), + nva.data(), nva.size(), &data_read); + if (nghttp3_err_is_fatal(rv)) { + ULOG(FATAL, this) << "nghttp3_conn_submit_response() failed: " + << nghttp3_strerror(rv); + return -1; + } + + downstream->reset_upstream_wtimer(); + + if (shutdown_stream_read(downstream->get_stream_id(), NGHTTP3_H3_NO_ERROR) != + 0) { + return -1; + } + + return 0; +} + +int Http3Upstream::shutdown_stream(Downstream *downstream, + uint64_t app_error_code) { + auto stream_id = downstream->get_stream_id(); + + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Shutdown stream_id=" << stream_id + << " with app_error_code=" << app_error_code; + } + + auto rv = ngtcp2_conn_shutdown_stream(conn_, 0, stream_id, app_error_code); + if (rv != 0) { + ULOG(FATAL, this) << "ngtcp2_conn_shutdown_stream() failed: " + << ngtcp2_strerror(rv); + return -1; + } + + return 0; +} + +int Http3Upstream::shutdown_stream_read(int64_t stream_id, + uint64_t app_error_code) { + auto rv = ngtcp2_conn_shutdown_stream_read(conn_, 0, stream_id, + NGHTTP3_H3_NO_ERROR); + if (ngtcp2_err_is_fatal(rv)) { + ULOG(FATAL, this) << "ngtcp2_conn_shutdown_stream_read: " + << ngtcp2_strerror(rv); + return -1; + } + + return 0; +} + +void Http3Upstream::consume(int64_t stream_id, size_t nconsumed) { + ngtcp2_conn_extend_max_stream_offset(conn_, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(conn_, nconsumed); +} + +void Http3Upstream::remove_downstream(Downstream *downstream) { + if (downstream->accesslog_ready()) { + handler_->write_accesslog(downstream); + } + + nghttp3_conn_set_stream_user_data(httpconn_, downstream->get_stream_id(), + nullptr); + + auto next_downstream = downstream_queue_.remove_and_get_blocked(downstream); + + if (next_downstream) { + initiate_downstream(next_downstream); + } + + if (downstream_queue_.get_downstreams() == nullptr) { + // There is no downstream at the moment. Start idle timer now. + handler_->repeat_read_timer(); + } +} + +void Http3Upstream::log_response_headers( + Downstream *downstream, const std::vector<nghttp3_nv> &nva) const { + std::stringstream ss; + for (auto &nv : nva) { + ss << TTY_HTTP_HD << StringRef{nv.name, nv.namelen} << TTY_RST << ": " + << StringRef{nv.value, nv.valuelen} << "\n"; + } + ULOG(INFO, this) << "HTTP response headers. stream_id=" + << downstream->get_stream_id() << "\n" + << ss.str(); +} + +int Http3Upstream::check_shutdown() { + auto worker = handler_->get_worker(); + + if (!worker->get_graceful_shutdown()) { + return 0; + } + + ev_prepare_stop(handler_->get_loop(), &prep_); + + return start_graceful_shutdown(); +} + +int Http3Upstream::start_graceful_shutdown() { + int rv; + + if (ev_is_active(&shutdown_timer_)) { + return 0; + } + + if (!httpconn_) { + return -1; + } + + rv = nghttp3_conn_submit_shutdown_notice(httpconn_); + if (rv != 0) { + ULOG(FATAL, this) << "nghttp3_conn_submit_shutdown_notice: " + << nghttp3_strerror(rv); + return -1; + } + + handler_->signal_write(); + + auto t = ngtcp2_conn_get_pto(conn_); + + ev_timer_set(&shutdown_timer_, static_cast<ev_tstamp>(t * 3) / NGTCP2_SECONDS, + 0.); + ev_timer_start(handler_->get_loop(), &shutdown_timer_); + + return 0; +} + +int Http3Upstream::submit_goaway() { + int rv; + + rv = nghttp3_conn_shutdown(httpconn_); + if (rv != 0) { + ULOG(FATAL, this) << "nghttp3_conn_shutdown: " << nghttp3_strerror(rv); + return -1; + } + + handler_->signal_write(); + + return 0; +} + +int Http3Upstream::open_qlog_file(const StringRef &dir, + const ngtcp2_cid &scid) const { + std::array<char, sizeof("20141115T125824.741+0900")> buf; + + auto path = dir.str(); + path += '/'; + path += + util::format_iso8601_basic(buf.data(), std::chrono::system_clock::now()); + path += '-'; + path += util::format_hex(scid.data, scid.datalen); + path += ".sqlog"; + + int fd; + +#ifdef O_CLOEXEC + while ((fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, + S_IRUSR | S_IWUSR | S_IRGRP)) == -1 && + errno == EINTR) + ; +#else // !O_CLOEXEC + while ((fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP)) == -1 && + errno == EINTR) + ; + + if (fd != -1) { + util::make_socket_closeonexec(fd); + } +#endif // !O_CLOEXEC + + if (fd == -1) { + auto error = errno; + ULOG(ERROR, this) << "Failed to open qlog file " << path + << ": errno=" << error; + return -1; + } + + return fd; +} + +ngtcp2_conn *Http3Upstream::get_conn() const { return conn_; } + +} // namespace shrpx diff --git a/src/shrpx_http3_upstream.h b/src/shrpx_http3_upstream.h new file mode 100644 index 0000000..89dfc17 --- /dev/null +++ b/src/shrpx_http3_upstream.h @@ -0,0 +1,193 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP3_UPSTREAM_H +#define SHRPX_HTTP3_UPSTREAM_H + +#include "shrpx.h" + +#include <ngtcp2/ngtcp2.h> +#include <nghttp3/nghttp3.h> + +#include "shrpx_upstream.h" +#include "shrpx_downstream_queue.h" +#include "quic.h" +#include "network.h" + +using namespace nghttp2; + +namespace shrpx { + +struct UpstreamAddr; + +class Http3Upstream : public Upstream { +public: + Http3Upstream(ClientHandler *handler); + virtual ~Http3Upstream(); + + virtual int on_read(); + virtual int on_write(); + virtual int on_timeout(Downstream *downstream); + virtual int on_downstream_abort_request(Downstream *downstream, + unsigned int status_code); + virtual int + on_downstream_abort_request_with_https_redirect(Downstream *downstream); + virtual int downstream_read(DownstreamConnection *dconn); + virtual int downstream_write(DownstreamConnection *dconn); + virtual int downstream_eof(DownstreamConnection *dconn); + virtual int downstream_error(DownstreamConnection *dconn, int events); + virtual ClientHandler *get_client_handler() const; + + virtual int on_downstream_header_complete(Downstream *downstream); + virtual int on_downstream_body(Downstream *downstream, const uint8_t *data, + size_t len, bool flush); + virtual int on_downstream_body_complete(Downstream *downstream); + + virtual void on_handler_delete(); + virtual int on_downstream_reset(Downstream *downstream, bool no_retry); + + virtual void pause_read(IOCtrlReason reason); + virtual int resume_read(IOCtrlReason reason, Downstream *downstream, + size_t consumed); + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen); + + virtual int initiate_push(Downstream *downstream, const StringRef &uri); + + virtual int response_riovec(struct iovec *iov, int iovcnt) const; + virtual void response_drain(size_t n); + virtual bool response_empty() const; + + virtual Downstream *on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + virtual bool push_enabled() const; + virtual void cancel_premature_downstream(Downstream *promised_downstream); + + int init(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_hd &initial_hd, + const ngtcp2_cid *odcid, const uint8_t *token, size_t tokenlen, + ngtcp2_token_type token_type); + + int on_read(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen); + + int write_streams(); + + int handle_error(); + + int handle_expiry(); + void reset_timer(); + + int setup_httpconn(); + void add_pending_downstream(std::unique_ptr<Downstream> downstream); + int recv_stream_data(uint32_t flags, int64_t stream_id, const uint8_t *data, + size_t datalen); + int acked_stream_data_offset(int64_t stream_id, uint64_t datalen); + int extend_max_stream_data(int64_t stream_id); + void extend_max_remote_streams_bidi(uint64_t max_streams); + int error_reply(Downstream *downstream, unsigned int status_code); + void http_begin_request_headers(int64_t stream_id); + int http_recv_request_header(Downstream *downstream, int32_t token, + nghttp3_rcbuf *name, nghttp3_rcbuf *value, + uint8_t flags, bool trailer); + int http_end_request_headers(Downstream *downstream, int fin); + int http_end_stream(Downstream *downstream); + void start_downstream(Downstream *downstream); + void initiate_downstream(Downstream *downstream); + int shutdown_stream(Downstream *downstream, uint64_t app_error_code); + int shutdown_stream_read(int64_t stream_id, uint64_t app_error_code); + int http_stream_close(Downstream *downstream, uint64_t app_error_code); + void consume(int64_t stream_id, size_t nconsumed); + void remove_downstream(Downstream *downstream); + int stream_close(int64_t stream_id, uint64_t app_error_code); + void log_response_headers(Downstream *downstream, + const std::vector<nghttp3_nv> &nva) const; + int http_acked_stream_data(Downstream *downstream, uint64_t datalen); + int http_shutdown_stream_read(int64_t stream_id); + int http_reset_stream(int64_t stream_id, uint64_t app_error_code); + int http_stop_sending(int64_t stream_id, uint64_t app_error_code); + int http_recv_data(Downstream *downstream, const uint8_t *data, + size_t datalen); + int handshake_completed(); + int check_shutdown(); + int start_graceful_shutdown(); + int submit_goaway(); + int send_packet(const UpstreamAddr *faddr, const sockaddr *remote_sa, + size_t remote_salen, const sockaddr *local_sa, + size_t local_salen, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen, size_t gso_size); + + void qlog_write(const void *data, size_t datalen, bool fin); + int open_qlog_file(const StringRef &dir, const ngtcp2_cid &scid) const; + + void on_send_blocked(const UpstreamAddr *faddr, + const ngtcp2_addr &remote_addr, + const ngtcp2_addr &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen, size_t gso_size); + int send_blocked_packet(); + void signal_write_upstream_addr(const UpstreamAddr *faddr); + + ngtcp2_conn *get_conn() const; + + int send_new_token(const ngtcp2_addr *remote_addr); + +private: + ClientHandler *handler_; + ev_timer timer_; + ev_timer shutdown_timer_; + ev_prepare prep_; + int qlog_fd_; + ngtcp2_cid hashed_scid_; + ngtcp2_conn *conn_; + ngtcp2_ccerr last_error_; + nghttp3_conn *httpconn_; + DownstreamQueue downstream_queue_; + bool retry_close_; + std::vector<uint8_t> conn_close_; + + struct { + bool send_blocked; + size_t num_blocked; + size_t num_blocked_sent; + // blocked field is effective only when send_blocked is true. + struct { + const UpstreamAddr *faddr; + Address local_addr; + Address remote_addr; + ngtcp2_pkt_info pi; + const uint8_t *data; + size_t datalen; + size_t gso_size; + } blocked[2]; + std::unique_ptr<uint8_t[]> data; + } tx_; +}; + +} // namespace shrpx + +#endif // SHRPX_HTTP3_UPSTREAM_H diff --git a/src/shrpx_http_downstream_connection.cc b/src/shrpx_http_downstream_connection.cc new file mode 100644 index 0000000..4164b8b --- /dev/null +++ b/src/shrpx_http_downstream_connection.cc @@ -0,0 +1,1617 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_http_downstream_connection.h" + +#include <openssl/rand.h> + +#include "shrpx_client_handler.h" +#include "shrpx_upstream.h" +#include "shrpx_downstream.h" +#include "shrpx_config.h" +#include "shrpx_error.h" +#include "shrpx_http.h" +#include "shrpx_log_config.h" +#include "shrpx_connect_blocker.h" +#include "shrpx_downstream_connection_pool.h" +#include "shrpx_worker.h" +#include "shrpx_http2_session.h" +#include "shrpx_tls.h" +#include "shrpx_log.h" +#include "http2.h" +#include "util.h" +#include "ssl_compat.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto dconn = static_cast<HttpDownstreamConnection *>(conn->data); + + if (w == &conn->rt && !conn->expired_rt()) { + return; + } + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "Time out"; + } + + auto downstream = dconn->get_downstream(); + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto &resp = downstream->response(); + + // Do this so that dconn is not pooled + resp.connection_close = true; + + if (upstream->downstream_error(dconn, Downstream::EVENT_TIMEOUT) != 0) { + delete handler; + } +} +} // namespace + +namespace { +void retry_downstream_connection(Downstream *downstream, + unsigned int status_code) { + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + + assert(!downstream->get_request_header_sent()); + + downstream->add_retry(); + + if (downstream->no_more_retry()) { + delete handler; + return; + } + + downstream->pop_downstream_connection(); + auto buf = downstream->get_request_buf(); + buf->reset(); + + int rv; + + for (;;) { + auto ndconn = handler->get_downstream_connection(rv, downstream); + if (!ndconn) { + break; + } + if (downstream->attach_downstream_connection(std::move(ndconn)) != 0) { + continue; + } + if (downstream->push_request_headers() == 0) { + return; + } + } + + downstream->set_request_state(DownstreamState::CONNECT_FAIL); + + if (rv == SHRPX_ERR_TLS_REQUIRED) { + rv = upstream->on_downstream_abort_request_with_https_redirect(downstream); + } else { + rv = upstream->on_downstream_abort_request(downstream, status_code); + } + + if (rv != 0) { + delete handler; + } +} +} // namespace + +namespace { +void connect_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto dconn = static_cast<HttpDownstreamConnection *>(conn->data); + auto addr = dconn->get_addr(); + auto raddr = dconn->get_raddr(); + + DCLOG(WARN, dconn) << "Connect time out; addr=" + << util::to_numeric_addr(raddr); + + downstream_failure(addr, raddr); + + auto downstream = dconn->get_downstream(); + + retry_downstream_connection(downstream, 504); +} +} // namespace + +namespace { +void backend_retry(Downstream *downstream) { + retry_downstream_connection(downstream, 502); +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto conn = static_cast<Connection *>(w->data); + auto dconn = static_cast<HttpDownstreamConnection *>(conn->data); + auto downstream = dconn->get_downstream(); + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + + rv = upstream->downstream_read(dconn); + if (rv != 0) { + if (rv == SHRPX_ERR_RETRY) { + backend_retry(downstream); + return; + } + + delete handler; + } +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto conn = static_cast<Connection *>(w->data); + auto dconn = static_cast<HttpDownstreamConnection *>(conn->data); + auto downstream = dconn->get_downstream(); + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + + rv = upstream->downstream_write(dconn); + if (rv == SHRPX_ERR_RETRY) { + backend_retry(downstream); + return; + } + + if (rv != 0) { + delete handler; + } +} +} // namespace + +namespace { +void connectcb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto dconn = static_cast<HttpDownstreamConnection *>(conn->data); + auto downstream = dconn->get_downstream(); + if (dconn->connected() != 0) { + backend_retry(downstream); + return; + } + writecb(loop, w, revents); +} +} // namespace + +HttpDownstreamConnection::HttpDownstreamConnection( + const std::shared_ptr<DownstreamAddrGroup> &group, DownstreamAddr *addr, + struct ev_loop *loop, Worker *worker) + : conn_(loop, -1, nullptr, worker->get_mcpool(), + group->shared_addr->timeout.write, group->shared_addr->timeout.read, + {}, {}, connectcb, readcb, connect_timeoutcb, this, + get_config()->tls.dyn_rec.warmup_threshold, + get_config()->tls.dyn_rec.idle_timeout, Proto::HTTP1), + on_read_(&HttpDownstreamConnection::noop), + on_write_(&HttpDownstreamConnection::noop), + signal_write_(&HttpDownstreamConnection::noop), + worker_(worker), + ssl_ctx_(worker->get_cl_ssl_ctx()), + group_(group), + addr_(addr), + raddr_(nullptr), + ioctrl_(&conn_.rlimit), + response_htp_{0}, + first_write_done_(false), + reusable_(true), + request_header_written_(false) {} + +HttpDownstreamConnection::~HttpDownstreamConnection() { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Deleted"; + } + + if (dns_query_) { + auto dns_tracker = worker_->get_dns_tracker(); + dns_tracker->cancel(dns_query_.get()); + } +} + +int HttpDownstreamConnection::attach_downstream(Downstream *downstream) { + int rv; + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Attaching to DOWNSTREAM:" << downstream; + } + + downstream_ = downstream; + + rv = initiate_connection(); + if (rv != 0) { + downstream_ = nullptr; + return rv; + } + + return 0; +} + +namespace { +int htp_msg_begincb(llhttp_t *htp); +int htp_hdr_keycb(llhttp_t *htp, const char *data, size_t len); +int htp_hdr_valcb(llhttp_t *htp, const char *data, size_t len); +int htp_hdrs_completecb(llhttp_t *htp); +int htp_bodycb(llhttp_t *htp, const char *data, size_t len); +int htp_msg_completecb(llhttp_t *htp); +} // namespace + +namespace { +constexpr llhttp_settings_t htp_hooks = { + htp_msg_begincb, // llhttp_cb on_message_begin; + nullptr, // llhttp_data_cb on_url; + nullptr, // llhttp_data_cb on_status; + nullptr, // llhttp_data_cb on_method; + nullptr, // llhttp_data_cb on_version; + htp_hdr_keycb, // llhttp_data_cb on_header_field; + htp_hdr_valcb, // llhttp_data_cb on_header_value; + nullptr, // llhttp_data_cb on_chunk_extension_name; + nullptr, // llhttp_data_cb on_chunk_extension_value; + htp_hdrs_completecb, // llhttp_cb on_headers_complete; + htp_bodycb, // llhttp_data_cb on_body; + htp_msg_completecb, // llhttp_cb on_message_complete; + nullptr, // llhttp_cb on_url_complete; + nullptr, // llhttp_cb on_status_complete; + nullptr, // llhttp_cb on_method_complete; + nullptr, // llhttp_cb on_version_complete; + nullptr, // llhttp_cb on_header_field_complete; + nullptr, // llhttp_cb on_header_value_complete; + nullptr, // llhttp_cb on_chunk_extension_name_complete; + nullptr, // llhttp_cb on_chunk_extension_value_complete; + nullptr, // llhttp_cb on_chunk_header; + nullptr, // llhttp_cb on_chunk_complete; + nullptr, // llhttp_cb on_reset; +}; +} // namespace + +int HttpDownstreamConnection::initiate_connection() { + int rv; + + auto worker_blocker = worker_->get_connect_blocker(); + if (worker_blocker->blocked()) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) + << "Worker wide backend connection was blocked temporarily"; + } + return SHRPX_ERR_NETWORK; + } + + auto &downstreamconf = *worker_->get_downstream_config(); + + if (conn_.fd == -1) { + auto check_dns_result = dns_query_.get() != nullptr; + + if (check_dns_result) { + assert(addr_->dns); + } + + auto &connect_blocker = addr_->connect_blocker; + + if (connect_blocker->blocked()) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Backend server " << addr_->host << ":" + << addr_->port << " was not available temporarily"; + } + + return SHRPX_ERR_NETWORK; + } + + Address *raddr; + + if (addr_->dns) { + if (!check_dns_result) { + auto dns_query = std::make_unique<DNSQuery>( + addr_->host, + [this](DNSResolverStatus status, const Address *result) { + int rv; + + if (status == DNSResolverStatus::OK) { + *this->resolved_addr_ = *result; + } + + rv = this->initiate_connection(); + if (rv != 0) { + // This callback destroys |this|. + auto downstream = this->downstream_; + backend_retry(downstream); + } + }); + + auto dns_tracker = worker_->get_dns_tracker(); + + if (!resolved_addr_) { + resolved_addr_ = std::make_unique<Address>(); + } + switch (dns_tracker->resolve(resolved_addr_.get(), dns_query.get())) { + case DNSResolverStatus::ERROR: + downstream_failure(addr_, nullptr); + return SHRPX_ERR_NETWORK; + case DNSResolverStatus::RUNNING: + dns_query_ = std::move(dns_query); + return 0; + case DNSResolverStatus::OK: + break; + default: + assert(0); + } + } else { + switch (dns_query_->status) { + case DNSResolverStatus::ERROR: + dns_query_.reset(); + downstream_failure(addr_, nullptr); + return SHRPX_ERR_NETWORK; + case DNSResolverStatus::OK: + dns_query_.reset(); + break; + default: + assert(0); + } + } + + raddr = resolved_addr_.get(); + util::set_port(*resolved_addr_, addr_->port); + } else { + raddr = &addr_->addr; + } + + conn_.fd = util::create_nonblock_socket(raddr->su.storage.ss_family); + + if (conn_.fd == -1) { + auto error = errno; + DCLOG(WARN, this) << "socket() failed; addr=" + << util::to_numeric_addr(raddr) << ", errno=" << error; + + worker_blocker->on_failure(); + + return SHRPX_ERR_NETWORK; + } + + worker_blocker->on_success(); + + rv = connect(conn_.fd, &raddr->su.sa, raddr->len); + if (rv != 0 && errno != EINPROGRESS) { + auto error = errno; + DCLOG(WARN, this) << "connect() failed; addr=" + << util::to_numeric_addr(raddr) << ", errno=" << error; + + downstream_failure(addr_, raddr); + + return SHRPX_ERR_NETWORK; + } + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Connecting to downstream server"; + } + + raddr_ = raddr; + + if (addr_->tls) { + assert(ssl_ctx_); + + auto ssl = tls::create_ssl(ssl_ctx_); + if (!ssl) { + return -1; + } + + tls::setup_downstream_http1_alpn(ssl); + + conn_.set_ssl(ssl); + conn_.tls.client_session_cache = &addr_->tls_session_cache; + + auto sni_name = + addr_->sni.empty() ? StringRef{addr_->host} : StringRef{addr_->sni}; + if (!util::numeric_host(sni_name.c_str())) { + SSL_set_tlsext_host_name(conn_.tls.ssl, sni_name.c_str()); + } + + auto session = tls::reuse_tls_session(addr_->tls_session_cache); + if (session) { + SSL_set_session(conn_.tls.ssl, session); + SSL_SESSION_free(session); + } + + conn_.prepare_client_handshake(); + } + + ev_io_set(&conn_.wev, conn_.fd, EV_WRITE); + ev_io_set(&conn_.rev, conn_.fd, EV_READ); + + conn_.wlimit.startw(); + + conn_.wt.repeat = downstreamconf.timeout.connect; + ev_timer_again(conn_.loop, &conn_.wt); + } else { + // we may set read timer cb to idle_timeoutcb. Reset again. + ev_set_cb(&conn_.rt, timeoutcb); + if (conn_.read_timeout < group_->shared_addr->timeout.read) { + conn_.read_timeout = group_->shared_addr->timeout.read; + conn_.last_read = std::chrono::steady_clock::now(); + } else { + conn_.again_rt(group_->shared_addr->timeout.read); + } + + ev_set_cb(&conn_.rev, readcb); + + on_write_ = &HttpDownstreamConnection::write_first; + first_write_done_ = false; + request_header_written_ = false; + } + + llhttp_init(&response_htp_, HTTP_RESPONSE, &htp_hooks); + response_htp_.data = downstream_; + + return 0; +} + +int HttpDownstreamConnection::push_request_headers() { + if (request_header_written_) { + signal_write(); + return 0; + } + + const auto &downstream_hostport = addr_->hostport; + const auto &req = downstream_->request(); + + auto &balloc = downstream_->get_block_allocator(); + + auto connect_method = req.regular_connect_method(); + + auto config = get_config(); + auto &httpconf = config->http; + + request_header_written_ = true; + + // For HTTP/1.0 request, there is no authority in request. In that + // case, we use backend server's host nonetheless. + auto authority = StringRef(downstream_hostport); + auto no_host_rewrite = + httpconf.no_host_rewrite || config->http2_proxy || connect_method; + + if (no_host_rewrite && !req.authority.empty()) { + authority = req.authority; + } + + downstream_->set_request_downstream_host(authority); + + auto buf = downstream_->get_request_buf(); + + // Assume that method and request path do not contain \r\n. + auto meth = http2::to_method_string( + req.connect_proto == ConnectProto::WEBSOCKET ? HTTP_GET : req.method); + buf->append(meth); + buf->append(' '); + + if (connect_method) { + buf->append(authority); + } else if (config->http2_proxy) { + // Construct absolute-form request target because we are going to + // send a request to a HTTP/1 proxy. + assert(!req.scheme.empty()); + buf->append(req.scheme); + buf->append("://"); + buf->append(authority); + buf->append(req.path); + } else if (req.method == HTTP_OPTIONS && req.path.empty()) { + // Server-wide OPTIONS + buf->append("*"); + } else { + buf->append(req.path); + } + buf->append(" HTTP/1.1\r\nHost: "); + buf->append(authority); + buf->append("\r\n"); + + auto &fwdconf = httpconf.forwarded; + auto &xffconf = httpconf.xff; + auto &xfpconf = httpconf.xfp; + auto &earlydataconf = httpconf.early_data; + + uint32_t build_flags = + (fwdconf.strip_incoming ? http2::HDOP_STRIP_FORWARDED : 0) | + (xffconf.strip_incoming ? http2::HDOP_STRIP_X_FORWARDED_FOR : 0) | + (xfpconf.strip_incoming ? http2::HDOP_STRIP_X_FORWARDED_PROTO : 0) | + (earlydataconf.strip_incoming ? http2::HDOP_STRIP_EARLY_DATA : 0) | + ((req.http_major == 3 || req.http_major == 2) + ? http2::HDOP_STRIP_SEC_WEBSOCKET_KEY + : 0); + + http2::build_http1_headers_from_headers(buf, req.fs.headers(), build_flags); + + auto cookie = downstream_->assemble_request_cookie(); + if (!cookie.empty()) { + buf->append("Cookie: "); + buf->append(cookie); + buf->append("\r\n"); + } + + // set transfer-encoding only when content-length is unknown and + // request body is expected. + if (req.method != HTTP_CONNECT && req.http2_expect_body && + req.fs.content_length == -1) { + downstream_->set_chunked_request(true); + buf->append("Transfer-Encoding: chunked\r\n"); + } + + if (req.connect_proto == ConnectProto::WEBSOCKET) { + if (req.http_major == 3 || req.http_major == 2) { + std::array<uint8_t, 16> nonce; + if (RAND_bytes(nonce.data(), nonce.size()) != 1) { + return -1; + } + auto iov = make_byte_ref(balloc, base64::encode_length(nonce.size()) + 1); + auto p = base64::encode(std::begin(nonce), std::end(nonce), iov.base); + *p = '\0'; + auto key = StringRef{iov.base, p}; + downstream_->set_ws_key(key); + + buf->append("Sec-Websocket-Key: "); + buf->append(key); + buf->append("\r\n"); + } + + buf->append("Upgrade: websocket\r\nConnection: Upgrade\r\n"); + } else if (!connect_method && req.upgrade_request) { + auto connection = req.fs.header(http2::HD_CONNECTION); + if (connection) { + buf->append("Connection: "); + buf->append((*connection).value); + buf->append("\r\n"); + } + + auto upgrade = req.fs.header(http2::HD_UPGRADE); + if (upgrade) { + buf->append("Upgrade: "); + buf->append((*upgrade).value); + buf->append("\r\n"); + } + } else if (req.connection_close) { + buf->append("Connection: close\r\n"); + } + + auto upstream = downstream_->get_upstream(); + auto handler = upstream->get_client_handler(); + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + auto conn = handler->get_connection(); + + if (conn->tls.ssl && !SSL_is_init_finished(conn->tls.ssl)) { + buf->append("Early-Data: 1\r\n"); + } +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_BORINGSSL + + auto fwd = + fwdconf.strip_incoming ? nullptr : req.fs.header(http2::HD_FORWARDED); + + if (fwdconf.params) { + auto params = fwdconf.params; + + if (config->http2_proxy || connect_method) { + params &= ~FORWARDED_PROTO; + } + + auto value = http::create_forwarded( + balloc, params, handler->get_forwarded_by(), + handler->get_forwarded_for(), req.authority, req.scheme); + + if (fwd || !value.empty()) { + buf->append("Forwarded: "); + if (fwd) { + buf->append(fwd->value); + + if (!value.empty()) { + buf->append(", "); + } + } + buf->append(value); + buf->append("\r\n"); + } + } else if (fwd) { + buf->append("Forwarded: "); + buf->append(fwd->value); + buf->append("\r\n"); + } + + auto xff = xffconf.strip_incoming ? nullptr + : req.fs.header(http2::HD_X_FORWARDED_FOR); + + if (xffconf.add) { + buf->append("X-Forwarded-For: "); + if (xff) { + buf->append((*xff).value); + buf->append(", "); + } + buf->append(client_handler_->get_ipaddr()); + buf->append("\r\n"); + } else if (xff) { + buf->append("X-Forwarded-For: "); + buf->append((*xff).value); + buf->append("\r\n"); + } + if (!config->http2_proxy && !connect_method) { + auto xfp = xfpconf.strip_incoming + ? nullptr + : req.fs.header(http2::HD_X_FORWARDED_PROTO); + + if (xfpconf.add) { + buf->append("X-Forwarded-Proto: "); + if (xfp) { + buf->append((*xfp).value); + buf->append(", "); + } + assert(!req.scheme.empty()); + buf->append(req.scheme); + buf->append("\r\n"); + } else if (xfp) { + buf->append("X-Forwarded-Proto: "); + buf->append((*xfp).value); + buf->append("\r\n"); + } + } + auto via = req.fs.header(http2::HD_VIA); + if (httpconf.no_via) { + if (via) { + buf->append("Via: "); + buf->append((*via).value); + buf->append("\r\n"); + } + } else { + buf->append("Via: "); + if (via) { + buf->append((*via).value); + buf->append(", "); + } + std::array<char, 16> viabuf; + auto end = http::create_via_header_value(viabuf.data(), req.http_major, + req.http_minor); + buf->append(viabuf.data(), end - viabuf.data()); + buf->append("\r\n"); + } + + for (auto &p : httpconf.add_request_headers) { + buf->append(p.name); + buf->append(": "); + buf->append(p.value); + buf->append("\r\n"); + } + + buf->append("\r\n"); + + if (LOG_ENABLED(INFO)) { + std::string nhdrs; + for (auto chunk = buf->head; chunk; chunk = chunk->next) { + nhdrs.append(chunk->pos, chunk->last); + } + if (log_config()->errorlog_tty) { + nhdrs = http::colorizeHeaders(nhdrs.c_str()); + } + DCLOG(INFO, this) << "HTTP request headers. stream_id=" + << downstream_->get_stream_id() << "\n" + << nhdrs; + } + + // Don't call signal_write() if we anticipate request body. We call + // signal_write() when we received request body chunk, and it + // enables us to send headers and data in one writev system call. + if (req.method == HTTP_CONNECT || + downstream_->get_blocked_request_buf()->rleft() || + (!req.http2_expect_body && req.fs.content_length == 0) || + downstream_->get_expect_100_continue()) { + signal_write(); + } + + return 0; +} + +int HttpDownstreamConnection::process_blocked_request_buf() { + auto src = downstream_->get_blocked_request_buf(); + + if (src->rleft()) { + auto dest = downstream_->get_request_buf(); + auto chunked = downstream_->get_chunked_request(); + if (chunked) { + auto chunk_size_hex = util::utox(src->rleft()); + dest->append(chunk_size_hex); + dest->append("\r\n"); + } + + src->copy(*dest); + + if (chunked) { + dest->append("\r\n"); + } + } + + if (downstream_->get_blocked_request_data_eof() && + downstream_->get_chunked_request()) { + end_upload_data_chunk(); + } + + return 0; +} + +int HttpDownstreamConnection::push_upload_data_chunk(const uint8_t *data, + size_t datalen) { + if (!downstream_->get_request_header_sent()) { + auto output = downstream_->get_blocked_request_buf(); + auto &req = downstream_->request(); + output->append(data, datalen); + req.unconsumed_body_length += datalen; + if (request_header_written_) { + signal_write(); + } + return 0; + } + + auto chunked = downstream_->get_chunked_request(); + auto output = downstream_->get_request_buf(); + + if (chunked) { + auto chunk_size_hex = util::utox(datalen); + output->append(chunk_size_hex); + output->append("\r\n"); + } + + output->append(data, datalen); + + if (chunked) { + output->append("\r\n"); + } + + signal_write(); + + return 0; +} + +int HttpDownstreamConnection::end_upload_data() { + if (!downstream_->get_request_header_sent()) { + downstream_->set_blocked_request_data_eof(true); + if (request_header_written_) { + signal_write(); + } + return 0; + } + + signal_write(); + + if (!downstream_->get_chunked_request()) { + return 0; + } + + end_upload_data_chunk(); + + return 0; +} + +void HttpDownstreamConnection::end_upload_data_chunk() { + const auto &req = downstream_->request(); + + auto output = downstream_->get_request_buf(); + const auto &trailers = req.fs.trailers(); + if (trailers.empty()) { + output->append("0\r\n\r\n"); + } else { + output->append("0\r\n"); + http2::build_http1_headers_from_headers(output, trailers, + http2::HDOP_STRIP_ALL); + output->append("\r\n"); + } +} + +namespace { +void remove_from_pool(HttpDownstreamConnection *dconn) { + auto addr = dconn->get_addr(); + auto &dconn_pool = addr->dconn_pool; + dconn_pool->remove_downstream_connection(dconn); +} +} // namespace + +namespace { +void idle_readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto dconn = static_cast<HttpDownstreamConnection *>(conn->data); + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "Idle connection EOF"; + } + + remove_from_pool(dconn); + // dconn was deleted +} +} // namespace + +namespace { +void idle_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto dconn = static_cast<HttpDownstreamConnection *>(conn->data); + + if (w == &conn->rt && !conn->expired_rt()) { + return; + } + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "Idle connection timeout"; + } + + remove_from_pool(dconn); + // dconn was deleted +} +} // namespace + +void HttpDownstreamConnection::detach_downstream(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Detaching from DOWNSTREAM:" << downstream; + } + downstream_ = nullptr; + + ev_set_cb(&conn_.rev, idle_readcb); + ioctrl_.force_resume_read(); + + auto &downstreamconf = *worker_->get_downstream_config(); + + ev_set_cb(&conn_.rt, idle_timeoutcb); + if (conn_.read_timeout < downstreamconf.timeout.idle_read) { + conn_.read_timeout = downstreamconf.timeout.idle_read; + conn_.last_read = std::chrono::steady_clock::now(); + } else { + conn_.again_rt(downstreamconf.timeout.idle_read); + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); +} + +void HttpDownstreamConnection::pause_read(IOCtrlReason reason) { + ioctrl_.pause_read(reason); +} + +int HttpDownstreamConnection::resume_read(IOCtrlReason reason, + size_t consumed) { + auto &downstreamconf = *worker_->get_downstream_config(); + + if (downstream_->get_response_buf()->rleft() <= + downstreamconf.request_buffer_size / 2) { + ioctrl_.resume_read(reason); + } + + return 0; +} + +void HttpDownstreamConnection::force_resume_read() { + ioctrl_.force_resume_read(); +} + +namespace { +int htp_msg_begincb(llhttp_t *htp) { + auto downstream = static_cast<Downstream *>(htp->data); + + if (downstream->get_response_state() != DownstreamState::INITIAL) { + return -1; + } + + return 0; +} +} // namespace + +namespace { +int htp_hdrs_completecb(llhttp_t *htp) { + auto downstream = static_cast<Downstream *>(htp->data); + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + const auto &req = downstream->request(); + auto &resp = downstream->response(); + int rv; + + auto &balloc = downstream->get_block_allocator(); + + for (auto &kv : resp.fs.headers()) { + kv.value = util::rstrip(balloc, kv.value); + + if (kv.token == http2::HD_TRANSFER_ENCODING && + !http2::check_transfer_encoding(kv.value)) { + return -1; + } + } + + auto config = get_config(); + auto &loggingconf = config->logging; + + resp.http_status = htp->status_code; + resp.http_major = htp->http_major; + resp.http_minor = htp->http_minor; + + if (resp.http_major > 1 || req.http_minor > 1) { + resp.http_major = 1; + resp.http_minor = 1; + return -1; + } + + auto dconn = downstream->get_downstream_connection(); + + downstream->set_downstream_addr_group(dconn->get_downstream_addr_group()); + downstream->set_addr(dconn->get_addr()); + + // Server MUST NOT send Transfer-Encoding with a status code 1xx or + // 204. Also server MUST NOT send Transfer-Encoding with a status + // code 2xx to a CONNECT request. Same holds true with + // Content-Length. + if (resp.http_status == 204) { + if (resp.fs.header(http2::HD_TRANSFER_ENCODING)) { + return -1; + } + // Some server send content-length: 0 for 204. Until they get + // fixed, we accept, but ignore it. + + // Calling parse_content_length() detects duplicated + // content-length header fields. + if (resp.fs.parse_content_length() != 0) { + return -1; + } + if (resp.fs.content_length == 0) { + resp.fs.erase_content_length_and_transfer_encoding(); + } else if (resp.fs.content_length != -1) { + return -1; + } + } else if (resp.http_status / 100 == 1 || + (resp.http_status / 100 == 2 && req.method == HTTP_CONNECT)) { + // Server MUST NOT send Content-Length and Transfer-Encoding in + // these responses. + resp.fs.erase_content_length_and_transfer_encoding(); + } else if (resp.fs.parse_content_length() != 0) { + downstream->set_response_state(DownstreamState::MSG_BAD_HEADER); + return -1; + } + + // Check upgrade before processing non-final response, since if + // upgrade succeeded, 101 response is treated as final in nghttpx. + downstream->check_upgrade_fulfilled_http1(); + + if (downstream->get_non_final_response()) { + // Reset content-length because we reuse same Downstream for the + // next response. + resp.fs.content_length = -1; + // For non-final response code, we just call + // on_downstream_header_complete() without changing response + // state. + rv = upstream->on_downstream_header_complete(downstream); + + if (rv != 0) { + return -1; + } + + // Ignore response body for non-final response. + return 1; + } + + resp.connection_close = !llhttp_should_keep_alive(htp); + downstream->set_response_state(DownstreamState::HEADER_COMPLETE); + downstream->inspect_http1_response(); + + if (htp->flags & F_CHUNKED) { + downstream->set_chunked_response(true); + } + + auto transfer_encoding = resp.fs.header(http2::HD_TRANSFER_ENCODING); + if (transfer_encoding && !downstream->get_chunked_response()) { + resp.connection_close = true; + } + + if (downstream->get_upgraded()) { + // content-length must be ignored for upgraded connection. + resp.fs.content_length = -1; + resp.connection_close = true; + // transfer-encoding not applied to upgraded connection + downstream->set_chunked_response(false); + } else if (http2::legacy_http1(req.http_major, req.http_minor)) { + if (resp.fs.content_length == -1) { + resp.connection_close = true; + } + downstream->set_chunked_response(false); + } else if (!downstream->expect_response_body()) { + downstream->set_chunked_response(false); + } + + if (loggingconf.access.write_early && downstream->accesslog_ready()) { + handler->write_accesslog(downstream); + downstream->set_accesslog_written(true); + } + + if (upstream->on_downstream_header_complete(downstream) != 0) { + return -1; + } + + if (downstream->get_upgraded()) { + // Upgrade complete, read until EOF in both ends + if (upstream->resume_read(SHRPX_NO_BUFFER, downstream, 0) != 0) { + return -1; + } + downstream->set_request_state(DownstreamState::HEADER_COMPLETE); + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "HTTP upgrade success. stream_id=" + << downstream->get_stream_id(); + } + } + + // Ignore the response body. HEAD response may contain + // Content-Length or Transfer-Encoding: chunked. Some server send + // 304 status code with nonzero Content-Length, but without response + // body. See + // https://tools.ietf.org/html/rfc7230#section-3.3 + + // TODO It seems that the cases other than HEAD are handled by + // llhttp. Need test. + return !http2::expect_response_body(req.method, resp.http_status); +} +} // namespace + +namespace { +int ensure_header_field_buffer(const Downstream *downstream, + const HttpConfig &httpconf, size_t len) { + auto &resp = downstream->response(); + + if (resp.fs.buffer_size() + len > httpconf.response_header_field_buffer) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "Too large header header field size=" + << resp.fs.buffer_size() + len; + } + return -1; + } + + return 0; +} +} // namespace + +namespace { +int ensure_max_header_fields(const Downstream *downstream, + const HttpConfig &httpconf) { + auto &resp = downstream->response(); + + if (resp.fs.num_fields() >= httpconf.max_response_header_fields) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) + << "Too many header field num=" << resp.fs.num_fields() + 1; + } + return -1; + } + + return 0; +} +} // namespace + +namespace { +int htp_hdr_keycb(llhttp_t *htp, const char *data, size_t len) { + auto downstream = static_cast<Downstream *>(htp->data); + auto &resp = downstream->response(); + auto &httpconf = get_config()->http; + + if (ensure_header_field_buffer(downstream, httpconf, len) != 0) { + return -1; + } + + if (downstream->get_response_state() == DownstreamState::INITIAL) { + if (resp.fs.header_key_prev()) { + resp.fs.append_last_header_key(data, len); + } else { + if (ensure_max_header_fields(downstream, httpconf) != 0) { + return -1; + } + resp.fs.alloc_add_header_name(StringRef{data, len}); + } + } else { + // trailer part + if (resp.fs.trailer_key_prev()) { + resp.fs.append_last_trailer_key(data, len); + } else { + if (ensure_max_header_fields(downstream, httpconf) != 0) { + // Could not ignore this trailer field easily, since we may + // get its value in htp_hdr_valcb, and it will be added to + // wrong place or crash if trailer fields are currently empty. + return -1; + } + resp.fs.alloc_add_trailer_name(StringRef{data, len}); + } + } + return 0; +} +} // namespace + +namespace { +int htp_hdr_valcb(llhttp_t *htp, const char *data, size_t len) { + auto downstream = static_cast<Downstream *>(htp->data); + auto &resp = downstream->response(); + auto &httpconf = get_config()->http; + + if (ensure_header_field_buffer(downstream, httpconf, len) != 0) { + return -1; + } + + if (downstream->get_response_state() == DownstreamState::INITIAL) { + resp.fs.append_last_header_value(data, len); + } else { + resp.fs.append_last_trailer_value(data, len); + } + return 0; +} +} // namespace + +namespace { +int htp_bodycb(llhttp_t *htp, const char *data, size_t len) { + auto downstream = static_cast<Downstream *>(htp->data); + auto &resp = downstream->response(); + + resp.recv_body_length += len; + + return downstream->get_upstream()->on_downstream_body( + downstream, reinterpret_cast<const uint8_t *>(data), len, true); +} +} // namespace + +namespace { +int htp_msg_completecb(llhttp_t *htp) { + auto downstream = static_cast<Downstream *>(htp->data); + auto &resp = downstream->response(); + auto &balloc = downstream->get_block_allocator(); + + for (auto &kv : resp.fs.trailers()) { + kv.value = util::rstrip(balloc, kv.value); + } + + // llhttp does not treat "200 connection established" response + // against CONNECT request, and in that case, this function is not + // called. But if HTTP Upgrade is made (e.g., WebSocket), this + // function is called, and llhttp_execute() returns just after that. + if (downstream->get_upgraded()) { + return 0; + } + + if (downstream->get_non_final_response()) { + downstream->reset_response(); + + return 0; + } + + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + // Block reading another response message from (broken?) + // server. This callback is not called if the connection is + // tunneled. + downstream->pause_read(SHRPX_MSG_BLOCK); + return downstream->get_upstream()->on_downstream_body_complete(downstream); +} +} // namespace + +int HttpDownstreamConnection::write_first() { + int rv; + + process_blocked_request_buf(); + + if (conn_.tls.ssl) { + rv = write_tls(); + } else { + rv = write_clear(); + } + + if (rv != 0) { + return SHRPX_ERR_RETRY; + } + + if (conn_.tls.ssl) { + on_write_ = &HttpDownstreamConnection::write_tls; + } else { + on_write_ = &HttpDownstreamConnection::write_clear; + } + + first_write_done_ = true; + downstream_->set_request_header_sent(true); + + auto buf = downstream_->get_blocked_request_buf(); + buf->reset(); + + // upstream->resume_read() might be called in + // write_tls()/write_clear(), but before blocked_request_buf_ is + // reset. So upstream read might still be blocked. Let's do it + // again here. + auto input = downstream_->get_request_buf(); + if (input->rleft() == 0) { + auto upstream = downstream_->get_upstream(); + auto &req = downstream_->request(); + + upstream->resume_read(SHRPX_NO_BUFFER, downstream_, + req.unconsumed_body_length); + } + + return 0; +} + +int HttpDownstreamConnection::read_clear() { + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<uint8_t, 16_k> buf; + int rv; + + for (;;) { + auto nread = conn_.read_clear(buf.data(), buf.size()); + if (nread == 0) { + return 0; + } + + if (nread < 0) { + if (nread == SHRPX_ERR_EOF && !downstream_->get_upgraded()) { + auto htperr = llhttp_finish(&response_htp_); + if (htperr != HPE_OK) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "HTTP response ended prematurely: " + << llhttp_errno_name(htperr); + } + + return -1; + } + } + + return nread; + } + + rv = process_input(buf.data(), nread); + if (rv != 0) { + return rv; + } + + if (!ev_is_active(&conn_.rev)) { + return 0; + } + } +} + +int HttpDownstreamConnection::write_clear() { + conn_.last_read = std::chrono::steady_clock::now(); + + auto upstream = downstream_->get_upstream(); + auto input = downstream_->get_request_buf(); + + std::array<struct iovec, MAX_WR_IOVCNT> iov; + + while (input->rleft() > 0) { + auto iovcnt = input->riovec(iov.data(), iov.size()); + + auto nwrite = conn_.writev_clear(iov.data(), iovcnt); + + if (nwrite == 0) { + return 0; + } + + if (nwrite < 0) { + if (!first_write_done_) { + return nwrite; + } + // We may have pending data in receive buffer which may contain + // part of response body. So keep reading. Invoke read event + // to get read(2) error just in case. + ev_feed_event(conn_.loop, &conn_.rev, EV_READ); + on_write_ = &HttpDownstreamConnection::noop; + reusable_ = false; + break; + } + + input->drain(nwrite); + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + if (input->rleft() == 0) { + auto &req = downstream_->request(); + + upstream->resume_read(SHRPX_NO_BUFFER, downstream_, + req.unconsumed_body_length); + } + + return 0; +} + +int HttpDownstreamConnection::tls_handshake() { + ERR_clear_error(); + + conn_.last_read = std::chrono::steady_clock::now(); + + auto rv = conn_.tls_handshake(); + if (rv == SHRPX_ERR_INPROGRESS) { + return 0; + } + + if (rv < 0) { + downstream_failure(addr_, raddr_); + + return rv; + } + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "SSL/TLS handshake completed"; + } + + if (!get_config()->tls.insecure && + tls::check_cert(conn_.tls.ssl, addr_, raddr_) != 0) { + downstream_failure(addr_, raddr_); + + return -1; + } + + auto &connect_blocker = addr_->connect_blocker; + + signal_write_ = &HttpDownstreamConnection::actual_signal_write; + + connect_blocker->on_success(); + + ev_set_cb(&conn_.rt, timeoutcb); + ev_set_cb(&conn_.wt, timeoutcb); + + on_read_ = &HttpDownstreamConnection::read_tls; + on_write_ = &HttpDownstreamConnection::write_first; + + // TODO Check negotiated ALPN + + return on_write(); +} + +int HttpDownstreamConnection::read_tls() { + conn_.last_read = std::chrono::steady_clock::now(); + + ERR_clear_error(); + + std::array<uint8_t, 16_k> buf; + int rv; + + for (;;) { + auto nread = conn_.read_tls(buf.data(), buf.size()); + if (nread == 0) { + return 0; + } + + if (nread < 0) { + if (nread == SHRPX_ERR_EOF && !downstream_->get_upgraded()) { + auto htperr = llhttp_finish(&response_htp_); + if (htperr != HPE_OK) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "HTTP response ended prematurely: " + << llhttp_errno_name(htperr); + } + + return -1; + } + } + + return nread; + } + + rv = process_input(buf.data(), nread); + if (rv != 0) { + return rv; + } + + if (!ev_is_active(&conn_.rev)) { + return 0; + } + } +} + +int HttpDownstreamConnection::write_tls() { + conn_.last_read = std::chrono::steady_clock::now(); + + ERR_clear_error(); + + auto upstream = downstream_->get_upstream(); + auto input = downstream_->get_request_buf(); + + struct iovec iov; + + while (input->rleft() > 0) { + auto iovcnt = input->riovec(&iov, 1); + if (iovcnt != 1) { + assert(0); + return -1; + } + auto nwrite = conn_.write_tls(iov.iov_base, iov.iov_len); + + if (nwrite == 0) { + return 0; + } + + if (nwrite < 0) { + if (!first_write_done_) { + return nwrite; + } + // We may have pending data in receive buffer which may contain + // part of response body. So keep reading. Invoke read event + // to get read(2) error just in case. + ev_feed_event(conn_.loop, &conn_.rev, EV_READ); + on_write_ = &HttpDownstreamConnection::noop; + reusable_ = false; + break; + } + + input->drain(nwrite); + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + if (input->rleft() == 0) { + auto &req = downstream_->request(); + + upstream->resume_read(SHRPX_NO_BUFFER, downstream_, + req.unconsumed_body_length); + } + + return 0; +} + +int HttpDownstreamConnection::process_input(const uint8_t *data, + size_t datalen) { + int rv; + + if (downstream_->get_upgraded()) { + // For upgraded connection, just pass data to the upstream. + rv = downstream_->get_upstream()->on_downstream_body(downstream_, data, + datalen, true); + if (rv != 0) { + return rv; + } + + if (downstream_->response_buf_full()) { + downstream_->pause_read(SHRPX_NO_BUFFER); + return 0; + } + + return 0; + } + + auto htperr = llhttp_execute(&response_htp_, + reinterpret_cast<const char *>(data), datalen); + auto nproc = + htperr == HPE_OK + ? datalen + : static_cast<size_t>(reinterpret_cast<const uint8_t *>( + llhttp_get_error_pos(&response_htp_)) - + data); + + if (htperr != HPE_OK && + (!downstream_->get_upgraded() || htperr != HPE_PAUSED_UPGRADE)) { + // Handling early return (in other words, response was hijacked by + // mruby scripting). + if (downstream_->get_response_state() == DownstreamState::MSG_COMPLETE) { + return SHRPX_ERR_DCONN_CANCELED; + } + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "HTTP parser failure: " + << "(" << llhttp_errno_name(htperr) << ") " + << llhttp_get_error_reason(&response_htp_); + } + + return -1; + } + + if (downstream_->get_upgraded()) { + if (nproc < datalen) { + // Data from data + nproc are for upgraded protocol. + rv = downstream_->get_upstream()->on_downstream_body( + downstream_, data + nproc, datalen - nproc, true); + if (rv != 0) { + return rv; + } + + if (downstream_->response_buf_full()) { + downstream_->pause_read(SHRPX_NO_BUFFER); + return 0; + } + } + return 0; + } + + if (downstream_->response_buf_full()) { + downstream_->pause_read(SHRPX_NO_BUFFER); + return 0; + } + + return 0; +} + +int HttpDownstreamConnection::connected() { + auto &connect_blocker = addr_->connect_blocker; + + auto sock_error = util::get_socket_error(conn_.fd); + if (sock_error != 0) { + conn_.wlimit.stopw(); + + DCLOG(WARN, this) << "Backend connect failed; addr=" + << util::to_numeric_addr(raddr_) + << ": errno=" << sock_error; + + downstream_failure(addr_, raddr_); + + return -1; + } + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Connected to downstream host"; + } + + // Reset timeout for write. Previously, we set timeout for connect. + conn_.wt.repeat = group_->shared_addr->timeout.write; + ev_timer_again(conn_.loop, &conn_.wt); + + conn_.rlimit.startw(); + conn_.again_rt(); + + ev_set_cb(&conn_.wev, writecb); + + if (conn_.tls.ssl) { + on_read_ = &HttpDownstreamConnection::tls_handshake; + on_write_ = &HttpDownstreamConnection::tls_handshake; + + return 0; + } + + signal_write_ = &HttpDownstreamConnection::actual_signal_write; + + connect_blocker->on_success(); + + ev_set_cb(&conn_.rt, timeoutcb); + ev_set_cb(&conn_.wt, timeoutcb); + + on_read_ = &HttpDownstreamConnection::read_clear; + on_write_ = &HttpDownstreamConnection::write_first; + + return 0; +} + +int HttpDownstreamConnection::on_read() { return on_read_(*this); } + +int HttpDownstreamConnection::on_write() { return on_write_(*this); } + +void HttpDownstreamConnection::on_upstream_change(Upstream *upstream) {} + +void HttpDownstreamConnection::signal_write() { signal_write_(*this); } + +int HttpDownstreamConnection::actual_signal_write() { + ev_feed_event(conn_.loop, &conn_.wev, EV_WRITE); + return 0; +} + +int HttpDownstreamConnection::noop() { return 0; } + +const std::shared_ptr<DownstreamAddrGroup> & +HttpDownstreamConnection::get_downstream_addr_group() const { + return group_; +} + +DownstreamAddr *HttpDownstreamConnection::get_addr() const { return addr_; } + +bool HttpDownstreamConnection::poolable() const { + return !group_->retired && reusable_; +} + +const Address *HttpDownstreamConnection::get_raddr() const { return raddr_; } + +} // namespace shrpx diff --git a/src/shrpx_http_downstream_connection.h b/src/shrpx_http_downstream_connection.h new file mode 100644 index 0000000..a453f0d --- /dev/null +++ b/src/shrpx_http_downstream_connection.h @@ -0,0 +1,124 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP_DOWNSTREAM_CONNECTION_H +#define SHRPX_HTTP_DOWNSTREAM_CONNECTION_H + +#include "shrpx.h" + +#include "llhttp.h" + +#include "shrpx_downstream_connection.h" +#include "shrpx_io_control.h" +#include "shrpx_connection.h" + +namespace shrpx { + +class DownstreamConnectionPool; +class Worker; +struct DownstreamAddrGroup; +struct DownstreamAddr; +struct DNSQuery; + +class HttpDownstreamConnection : public DownstreamConnection { +public: + HttpDownstreamConnection(const std::shared_ptr<DownstreamAddrGroup> &group, + DownstreamAddr *addr, struct ev_loop *loop, + Worker *worker); + virtual ~HttpDownstreamConnection(); + virtual int attach_downstream(Downstream *downstream); + virtual void detach_downstream(Downstream *downstream); + + virtual int push_request_headers(); + virtual int push_upload_data_chunk(const uint8_t *data, size_t datalen); + virtual int end_upload_data(); + void end_upload_data_chunk(); + + virtual void pause_read(IOCtrlReason reason); + virtual int resume_read(IOCtrlReason reason, size_t consumed); + virtual void force_resume_read(); + + virtual int on_read(); + virtual int on_write(); + + virtual void on_upstream_change(Upstream *upstream); + + virtual bool poolable() const; + + virtual const std::shared_ptr<DownstreamAddrGroup> & + get_downstream_addr_group() const; + virtual DownstreamAddr *get_addr() const; + + int initiate_connection(); + + int write_first(); + int read_clear(); + int write_clear(); + int read_tls(); + int write_tls(); + + int process_input(const uint8_t *data, size_t datalen); + int tls_handshake(); + + int connected(); + void signal_write(); + int actual_signal_write(); + + // Returns address used to connect to backend. Could be nullptr. + const Address *get_raddr() const; + + int noop(); + + int process_blocked_request_buf(); + +private: + Connection conn_; + std::function<int(HttpDownstreamConnection &)> on_read_, on_write_, + signal_write_; + Worker *worker_; + // nullptr if TLS is not used. + SSL_CTX *ssl_ctx_; + std::shared_ptr<DownstreamAddrGroup> group_; + // Address of remote endpoint + DownstreamAddr *addr_; + // Actual remote address used to contact backend. This is initially + // nullptr, and may point to either &addr_->addr, or + // resolved_addr_.get(). + const Address *raddr_; + // Resolved IP address if dns parameter is used + std::unique_ptr<Address> resolved_addr_; + std::unique_ptr<DNSQuery> dns_query_; + IOControl ioctrl_; + llhttp_t response_htp_; + // true if first write succeeded. + bool first_write_done_; + // true if this object can be reused + bool reusable_; + // true if request header is written to request buffer. + bool request_header_written_; +}; + +} // namespace shrpx + +#endif // SHRPX_HTTP_DOWNSTREAM_CONNECTION_H diff --git a/src/shrpx_http_test.cc b/src/shrpx_http_test.cc new file mode 100644 index 0000000..3ace870 --- /dev/null +++ b/src/shrpx_http_test.cc @@ -0,0 +1,168 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_http_test.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H + +#include <cstdlib> + +#include <CUnit/CUnit.h> + +#include "shrpx_http.h" +#include "shrpx_config.h" +#include "shrpx_log.h" + +namespace shrpx { + +void test_shrpx_http_create_forwarded(void) { + BlockAllocator balloc(1024, 1024); + + CU_ASSERT("by=\"example.com:3000\";for=\"[::1]\";host=\"www.example.com\";" + "proto=https" == + http::create_forwarded(balloc, + FORWARDED_BY | FORWARDED_FOR | + FORWARDED_HOST | FORWARDED_PROTO, + StringRef::from_lit("example.com:3000"), + StringRef::from_lit("[::1]"), + StringRef::from_lit("www.example.com"), + StringRef::from_lit("https"))); + + CU_ASSERT("for=192.168.0.1" == + http::create_forwarded( + balloc, FORWARDED_FOR, StringRef::from_lit("alpha"), + StringRef::from_lit("192.168.0.1"), + StringRef::from_lit("bravo"), StringRef::from_lit("charlie"))); + + CU_ASSERT("by=_hidden;for=\"[::1]\"" == + http::create_forwarded( + balloc, FORWARDED_BY | FORWARDED_FOR, + StringRef::from_lit("_hidden"), StringRef::from_lit("[::1]"), + StringRef::from_lit(""), StringRef::from_lit(""))); + + CU_ASSERT("by=\"[::1]\";for=_hidden" == + http::create_forwarded( + balloc, FORWARDED_BY | FORWARDED_FOR, + StringRef::from_lit("[::1]"), StringRef::from_lit("_hidden"), + StringRef::from_lit(""), StringRef::from_lit(""))); + + CU_ASSERT("" == + http::create_forwarded( + balloc, + FORWARDED_BY | FORWARDED_FOR | FORWARDED_HOST | FORWARDED_PROTO, + StringRef::from_lit(""), StringRef::from_lit(""), + StringRef::from_lit(""), StringRef::from_lit(""))); +} + +void test_shrpx_http_create_via_header_value(void) { + std::array<char, 16> buf; + + auto end = http::create_via_header_value(std::begin(buf), 1, 1); + + CU_ASSERT(("1.1 nghttpx" == StringRef{std::begin(buf), end})); + + std::fill(std::begin(buf), std::end(buf), '\0'); + + end = http::create_via_header_value(std::begin(buf), 2, 0); + + CU_ASSERT(("2 nghttpx" == StringRef{std::begin(buf), end})); +} + +void test_shrpx_http_create_affinity_cookie(void) { + BlockAllocator balloc(1024, 1024); + StringRef c; + + c = http::create_affinity_cookie(balloc, StringRef::from_lit("cookie-val"), + 0xf1e2d3c4u, StringRef{}, false); + + CU_ASSERT("cookie-val=f1e2d3c4" == c); + + c = http::create_affinity_cookie(balloc, StringRef::from_lit("alpha"), + 0x00000000u, StringRef{}, true); + + CU_ASSERT("alpha=00000000; Secure" == c); + + c = http::create_affinity_cookie(balloc, StringRef::from_lit("bravo"), + 0x01111111u, StringRef::from_lit("bar"), + false); + + CU_ASSERT("bravo=01111111; Path=bar" == c); + + c = http::create_affinity_cookie(balloc, StringRef::from_lit("charlie"), + 0x01111111u, StringRef::from_lit("bar"), + true); + + CU_ASSERT("charlie=01111111; Path=bar; Secure" == c); +} + +void test_shrpx_http_create_altsvc_header_value(void) { + { + BlockAllocator balloc(1024, 1024); + std::vector<AltSvc> altsvcs{ + AltSvc{ + .protocol_id = StringRef::from_lit("h3"), + .host = StringRef::from_lit("127.0.0.1"), + .service = StringRef::from_lit("443"), + .params = StringRef::from_lit("ma=3600"), + }, + }; + + CU_ASSERT(R"(h3="127.0.0.1:443"; ma=3600)" == + http::create_altsvc_header_value(balloc, altsvcs)); + } + + { + BlockAllocator balloc(1024, 1024); + std::vector<AltSvc> altsvcs{ + AltSvc{ + .protocol_id = StringRef::from_lit("h3"), + .service = StringRef::from_lit("443"), + .params = StringRef::from_lit("ma=3600"), + }, + AltSvc{ + .protocol_id = StringRef::from_lit("h3%"), + .host = StringRef::from_lit("\"foo\""), + .service = StringRef::from_lit("4433"), + }, + }; + + CU_ASSERT(R"(h3=":443"; ma=3600, h3%25="\"foo\":4433")" == + http::create_altsvc_header_value(balloc, altsvcs)); + } +} + +void test_shrpx_http_check_http_scheme(void) { + CU_ASSERT(http::check_http_scheme(StringRef::from_lit("https"), true)); + CU_ASSERT(!http::check_http_scheme(StringRef::from_lit("https"), false)); + CU_ASSERT(!http::check_http_scheme(StringRef::from_lit("http"), true)); + CU_ASSERT(http::check_http_scheme(StringRef::from_lit("http"), false)); + CU_ASSERT(!http::check_http_scheme(StringRef::from_lit("foo"), true)); + CU_ASSERT(!http::check_http_scheme(StringRef::from_lit("foo"), false)); + CU_ASSERT(!http::check_http_scheme(StringRef{}, true)); + CU_ASSERT(!http::check_http_scheme(StringRef{}, false)); +} + +} // namespace shrpx diff --git a/src/shrpx_http_test.h b/src/shrpx_http_test.h new file mode 100644 index 0000000..d50ab53 --- /dev/null +++ b/src/shrpx_http_test.h @@ -0,0 +1,42 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTP_TEST_H +#define SHRPX_HTTP_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_shrpx_http_create_forwarded(void); +void test_shrpx_http_create_via_header_value(void); +void test_shrpx_http_create_affinity_cookie(void); +void test_shrpx_http_create_altsvc_header_value(void); +void test_shrpx_http_check_http_scheme(void); + +} // namespace shrpx + +#endif // SHRPX_HTTP_TEST_H diff --git a/src/shrpx_https_upstream.cc b/src/shrpx_https_upstream.cc new file mode 100644 index 0000000..49d2088 --- /dev/null +++ b/src/shrpx_https_upstream.cc @@ -0,0 +1,1582 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_https_upstream.h" + +#include <cassert> +#include <set> +#include <sstream> + +#include "shrpx_client_handler.h" +#include "shrpx_downstream.h" +#include "shrpx_downstream_connection.h" +#include "shrpx_http.h" +#include "shrpx_config.h" +#include "shrpx_error.h" +#include "shrpx_log_config.h" +#include "shrpx_worker.h" +#include "shrpx_http2_session.h" +#include "shrpx_log.h" +#ifdef HAVE_MRUBY +# include "shrpx_mruby.h" +#endif // HAVE_MRUBY +#include "http2.h" +#include "util.h" +#include "template.h" +#include "base64.h" +#include "url-parser/url_parser.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +int htp_msg_begin(llhttp_t *htp); +int htp_uricb(llhttp_t *htp, const char *data, size_t len); +int htp_hdr_keycb(llhttp_t *htp, const char *data, size_t len); +int htp_hdr_valcb(llhttp_t *htp, const char *data, size_t len); +int htp_hdrs_completecb(llhttp_t *htp); +int htp_bodycb(llhttp_t *htp, const char *data, size_t len); +int htp_msg_completecb(llhttp_t *htp); +} // namespace + +namespace { +constexpr llhttp_settings_t htp_hooks = { + htp_msg_begin, // llhttp_cb on_message_begin; + htp_uricb, // llhttp_data_cb on_url; + nullptr, // llhttp_data_cb on_status; + nullptr, // llhttp_data_cb on_method; + nullptr, // llhttp_data_cb on_version; + htp_hdr_keycb, // llhttp_data_cb on_header_field; + htp_hdr_valcb, // llhttp_data_cb on_header_value; + nullptr, // llhttp_data_cb on_chunk_extension_name; + nullptr, // llhttp_data_cb on_chunk_extension_value; + htp_hdrs_completecb, // llhttp_cb on_headers_complete; + htp_bodycb, // llhttp_data_cb on_body; + htp_msg_completecb, // llhttp_cb on_message_complete; + nullptr, // llhttp_cb on_url_complete; + nullptr, // llhttp_cb on_status_complete; + nullptr, // llhttp_cb on_method_complete; + nullptr, // llhttp_cb on_version_complete; + nullptr, // llhttp_cb on_header_field_complete; + nullptr, // llhttp_cb on_header_value_complete; + nullptr, // llhttp_cb on_chunk_extension_name_complete; + nullptr, // llhttp_cb on_chunk_extension_value_complete; + nullptr, // llhttp_cb on_chunk_header; + nullptr, // llhttp_cb on_chunk_complete; + nullptr, // llhttp_cb on_reset; +}; +} // namespace + +HttpsUpstream::HttpsUpstream(ClientHandler *handler) + : handler_(handler), + current_header_length_(0), + ioctrl_(handler->get_rlimit()), + num_requests_(0) { + llhttp_init(&htp_, HTTP_REQUEST, &htp_hooks); + htp_.data = this; +} + +HttpsUpstream::~HttpsUpstream() {} + +void HttpsUpstream::reset_current_header_length() { + current_header_length_ = 0; +} + +void HttpsUpstream::on_start_request() { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "HTTP request started"; + } + reset_current_header_length(); + + auto downstream = + std::make_unique<Downstream>(this, handler_->get_mcpool(), 0); + + attach_downstream(std::move(downstream)); + + auto conn = handler_->get_connection(); + auto &upstreamconf = get_config()->conn.upstream; + + conn->rt.repeat = upstreamconf.timeout.read; + + handler_->repeat_read_timer(); + + ++num_requests_; +} + +namespace { +int htp_msg_begin(llhttp_t *htp) { + auto upstream = static_cast<HttpsUpstream *>(htp->data); + upstream->on_start_request(); + return 0; +} +} // namespace + +namespace { +int htp_uricb(llhttp_t *htp, const char *data, size_t len) { + auto upstream = static_cast<HttpsUpstream *>(htp->data); + auto downstream = upstream->get_downstream(); + auto &req = downstream->request(); + + auto &balloc = downstream->get_block_allocator(); + + // We happen to have the same value for method token. + req.method = htp->method; + + if (req.fs.buffer_size() + len > + get_config()->http.request_header_field_buffer) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Too large URI size=" + << req.fs.buffer_size() + len; + } + assert(downstream->get_request_state() == DownstreamState::INITIAL); + downstream->set_request_state( + DownstreamState::HTTP1_REQUEST_HEADER_TOO_LARGE); + llhttp_set_error_reason(htp, "too long request URI"); + return HPE_USER; + } + + req.fs.add_extra_buffer_size(len); + + if (req.method == HTTP_CONNECT) { + req.authority = + concat_string_ref(balloc, req.authority, StringRef{data, len}); + } else { + req.path = concat_string_ref(balloc, req.path, StringRef{data, len}); + } + + return 0; +} +} // namespace + +namespace { +int htp_hdr_keycb(llhttp_t *htp, const char *data, size_t len) { + auto upstream = static_cast<HttpsUpstream *>(htp->data); + auto downstream = upstream->get_downstream(); + auto &req = downstream->request(); + auto &httpconf = get_config()->http; + + if (req.fs.buffer_size() + len > httpconf.request_header_field_buffer) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Too large header block size=" + << req.fs.buffer_size() + len; + } + if (downstream->get_request_state() == DownstreamState::INITIAL) { + downstream->set_request_state( + DownstreamState::HTTP1_REQUEST_HEADER_TOO_LARGE); + } + llhttp_set_error_reason(htp, "too large header"); + return HPE_USER; + } + if (downstream->get_request_state() == DownstreamState::INITIAL) { + if (req.fs.header_key_prev()) { + req.fs.append_last_header_key(data, len); + } else { + if (req.fs.num_fields() >= httpconf.max_request_header_fields) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) + << "Too many header field num=" << req.fs.num_fields() + 1; + } + downstream->set_request_state( + DownstreamState::HTTP1_REQUEST_HEADER_TOO_LARGE); + llhttp_set_error_reason(htp, "too many headers"); + return HPE_USER; + } + req.fs.alloc_add_header_name(StringRef{data, len}); + } + } else { + // trailer part + if (req.fs.trailer_key_prev()) { + req.fs.append_last_trailer_key(data, len); + } else { + if (req.fs.num_fields() >= httpconf.max_request_header_fields) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) + << "Too many header field num=" << req.fs.num_fields() + 1; + } + llhttp_set_error_reason(htp, "too many headers"); + return HPE_USER; + } + req.fs.alloc_add_trailer_name(StringRef{data, len}); + } + } + return 0; +} +} // namespace + +namespace { +int htp_hdr_valcb(llhttp_t *htp, const char *data, size_t len) { + auto upstream = static_cast<HttpsUpstream *>(htp->data); + auto downstream = upstream->get_downstream(); + auto &req = downstream->request(); + + if (req.fs.buffer_size() + len > + get_config()->http.request_header_field_buffer) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "Too large header block size=" + << req.fs.buffer_size() + len; + } + if (downstream->get_request_state() == DownstreamState::INITIAL) { + downstream->set_request_state( + DownstreamState::HTTP1_REQUEST_HEADER_TOO_LARGE); + } + llhttp_set_error_reason(htp, "too large header"); + return HPE_USER; + } + if (downstream->get_request_state() == DownstreamState::INITIAL) { + req.fs.append_last_header_value(data, len); + } else { + req.fs.append_last_trailer_value(data, len); + } + return 0; +} +} // namespace + +namespace { +void rewrite_request_host_path_from_uri(BlockAllocator &balloc, Request &req, + const StringRef &uri, + http_parser_url &u) { + assert(u.field_set & (1 << UF_HOST)); + + // As per https://tools.ietf.org/html/rfc7230#section-5.4, we + // rewrite host header field with authority component. + auto authority = util::get_uri_field(uri.c_str(), u, UF_HOST); + // TODO properly check IPv6 numeric address + auto ipv6 = std::find(std::begin(authority), std::end(authority), ':') != + std::end(authority); + auto authoritylen = authority.size(); + if (ipv6) { + authoritylen += 2; + } + if (u.field_set & (1 << UF_PORT)) { + authoritylen += 1 + str_size("65535"); + } + if (authoritylen > authority.size()) { + auto iovec = make_byte_ref(balloc, authoritylen + 1); + auto p = iovec.base; + if (ipv6) { + *p++ = '['; + } + p = std::copy(std::begin(authority), std::end(authority), p); + if (ipv6) { + *p++ = ']'; + } + + if (u.field_set & (1 << UF_PORT)) { + *p++ = ':'; + p = util::utos(p, u.port); + } + *p = '\0'; + + req.authority = StringRef{iovec.base, p}; + } else { + req.authority = authority; + } + + req.scheme = util::get_uri_field(uri.c_str(), u, UF_SCHEMA); + + StringRef path; + if (u.field_set & (1 << UF_PATH)) { + path = util::get_uri_field(uri.c_str(), u, UF_PATH); + } else if (req.method == HTTP_OPTIONS) { + // Server-wide OPTIONS takes following form in proxy request: + // + // OPTIONS http://example.org HTTP/1.1 + // + // Notice that no slash after authority. See + // http://tools.ietf.org/html/rfc7230#section-5.3.4 + req.path = StringRef::from_lit(""); + // we ignore query component here + return; + } else { + path = StringRef::from_lit("/"); + } + + if (u.field_set & (1 << UF_QUERY)) { + auto &fdata = u.field_data[UF_QUERY]; + + if (u.field_set & (1 << UF_PATH)) { + auto q = util::get_uri_field(uri.c_str(), u, UF_QUERY); + path = StringRef{std::begin(path), std::end(q)}; + } else { + path = concat_string_ref(balloc, path, StringRef::from_lit("?"), + StringRef{&uri[fdata.off], fdata.len}); + } + } + + req.path = http2::rewrite_clean_path(balloc, path); +} +} // namespace + +namespace { +int htp_hdrs_completecb(llhttp_t *htp) { + int rv; + auto upstream = static_cast<HttpsUpstream *>(htp->data); + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "HTTP request headers completed"; + } + + auto handler = upstream->get_client_handler(); + + auto downstream = upstream->get_downstream(); + auto &req = downstream->request(); + auto &balloc = downstream->get_block_allocator(); + + for (auto &kv : req.fs.headers()) { + kv.value = util::rstrip(balloc, kv.value); + + if (kv.token == http2::HD_TRANSFER_ENCODING && + !http2::check_transfer_encoding(kv.value)) { + return -1; + } + } + + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + req.tstamp = lgconf->tstamp; + + req.http_major = htp->http_major; + req.http_minor = htp->http_minor; + + req.connection_close = !llhttp_should_keep_alive(htp); + + handler->stop_read_timer(); + + auto method = req.method; + + if (LOG_ENABLED(INFO)) { + std::stringstream ss; + ss << http2::to_method_string(method) << " " + << (method == HTTP_CONNECT ? req.authority : req.path) << " " + << "HTTP/" << req.http_major << "." << req.http_minor << "\n"; + + for (const auto &kv : req.fs.headers()) { + if (kv.name == "authorization") { + ss << TTY_HTTP_HD << kv.name << TTY_RST << ": <redacted>\n"; + continue; + } + ss << TTY_HTTP_HD << kv.name << TTY_RST << ": " << kv.value << "\n"; + } + + ULOG(INFO, upstream) << "HTTP request headers\n" << ss.str(); + } + + // set content-length if method is not CONNECT, and no + // transfer-encoding is given. If transfer-encoding is given, leave + // req.fs.content_length to -1. + if (method != HTTP_CONNECT && !req.fs.header(http2::HD_TRANSFER_ENCODING)) { + // llhttp sets 0 to htp->content_length if there is no + // content-length header field. If we don't have both + // transfer-encoding and content-length header field, we assume + // that there is no request body. + req.fs.content_length = htp->content_length; + } + + auto host = req.fs.header(http2::HD_HOST); + + if (req.http_major > 1 || req.http_minor > 1) { + req.http_major = 1; + req.http_minor = 1; + return -1; + } + + if (req.http_major == 1 && req.http_minor == 1 && !host) { + return -1; + } + + if (host) { + const auto &value = host->value; + // Not allow at least '"' or '\' in host. They are illegal in + // authority component, also they cause headaches when we put them + // in quoted-string. + if (std::find_if(std::begin(value), std::end(value), [](char c) { + return c == '"' || c == '\\'; + }) != std::end(value)) { + return -1; + } + } + + downstream->inspect_http1_request(); + + if (htp->flags & F_CHUNKED) { + downstream->set_chunked_request(true); + } + + auto transfer_encoding = req.fs.header(http2::HD_TRANSFER_ENCODING); + if (transfer_encoding && + http2::legacy_http1(req.http_major, req.http_minor)) { + return -1; + } + + auto faddr = handler->get_upstream_addr(); + auto config = get_config(); + + if (method != HTTP_CONNECT) { + http_parser_url u{}; + rv = http_parser_parse_url(req.path.c_str(), req.path.size(), 0, &u); + if (rv != 0) { + // Expect to respond with 400 bad request + return -1; + } + // checking UF_HOST could be redundant, but just in case ... + if (!(u.field_set & (1 << UF_SCHEMA)) || !(u.field_set & (1 << UF_HOST))) { + req.no_authority = true; + + if (method == HTTP_OPTIONS && req.path == StringRef::from_lit("*")) { + req.path = StringRef{}; + } else { + req.path = http2::rewrite_clean_path(balloc, req.path); + } + + if (host) { + req.authority = host->value; + } + + if (handler->get_ssl()) { + req.scheme = StringRef::from_lit("https"); + } else { + req.scheme = StringRef::from_lit("http"); + } + } else { + rewrite_request_host_path_from_uri(balloc, req, req.path, u); + } + } + + downstream->set_request_state(DownstreamState::HEADER_COMPLETE); + + auto &resp = downstream->response(); + + if (config->http.require_http_scheme && + !http::check_http_scheme(req.scheme, handler->get_ssl() != nullptr)) { + resp.http_status = 400; + return -1; + } + +#ifdef HAVE_MRUBY + auto worker = handler->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_request_proc(downstream) != 0) { + resp.http_status = 500; + return -1; + } +#endif // HAVE_MRUBY + + // mruby hook may change method value + + if (req.no_authority && config->http2_proxy && + faddr->alt_mode == UpstreamAltMode::NONE) { + // Request URI should be absolute-form for client proxy mode + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + +#ifdef HAVE_MRUBY + DownstreamConnection *dconn_ptr; +#endif // HAVE_MRUBY + + for (;;) { + auto dconn = handler->get_downstream_connection(rv, downstream); + + if (!dconn) { + if (rv == SHRPX_ERR_TLS_REQUIRED) { + upstream->redirect_to_https(downstream); + } + downstream->set_request_state(DownstreamState::CONNECT_FAIL); + return -1; + } + +#ifdef HAVE_MRUBY + dconn_ptr = dconn.get(); +#endif // HAVE_MRUBY + if (downstream->attach_downstream_connection(std::move(dconn)) == 0) { + break; + } + } + +#ifdef HAVE_MRUBY + const auto &group = dconn_ptr->get_downstream_addr_group(); + if (group) { + const auto &dmruby_ctx = group->shared_addr->mruby_ctx; + + if (dmruby_ctx->run_on_request_proc(downstream) != 0) { + resp.http_status = 500; + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + } +#endif // HAVE_MRUBY + + rv = downstream->push_request_headers(); + + if (rv != 0) { + return -1; + } + + if (faddr->alt_mode != UpstreamAltMode::NONE) { + // Normally, we forward expect: 100-continue to backend server, + // and let them decide whether responds with 100 Continue or not. + // For alternative mode, we have no backend, so just send 100 + // Continue here to make the client happy. + if (downstream->get_expect_100_continue()) { + auto output = downstream->get_response_buf(); + constexpr auto res = StringRef::from_lit("HTTP/1.1 100 Continue\r\n\r\n"); + output->append(res); + handler->signal_write(); + } + } + + return 0; +} +} // namespace + +namespace { +int htp_bodycb(llhttp_t *htp, const char *data, size_t len) { + int rv; + auto upstream = static_cast<HttpsUpstream *>(htp->data); + auto downstream = upstream->get_downstream(); + rv = downstream->push_upload_data_chunk( + reinterpret_cast<const uint8_t *>(data), len); + if (rv != 0) { + // Ignore error if response has been completed. We will end up in + // htp_msg_completecb, and request will end gracefully. + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return 0; + } + + llhttp_set_error_reason(htp, "could not process request body"); + return HPE_USER; + } + return 0; +} +} // namespace + +namespace { +int htp_msg_completecb(llhttp_t *htp) { + int rv; + auto upstream = static_cast<HttpsUpstream *>(htp->data); + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "HTTP request completed"; + } + auto handler = upstream->get_client_handler(); + auto downstream = upstream->get_downstream(); + auto &req = downstream->request(); + auto &balloc = downstream->get_block_allocator(); + + for (auto &kv : req.fs.trailers()) { + kv.value = util::rstrip(balloc, kv.value); + } + + downstream->set_request_state(DownstreamState::MSG_COMPLETE); + rv = downstream->end_upload_data(); + if (rv != 0) { + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + // Here both response and request were completed. One of the + // reason why end_upload_data() failed is when we sent response + // in request phase hook. We only delete and proceed to the + // next request handling (if we don't close the connection). We + // first pause parser here just as we normally do, and call + // signal_write() to run on_write(). + return HPE_PAUSED; + } + return -1; + } + + if (handler->get_http2_upgrade_allowed() && + downstream->get_http2_upgrade_request() && + handler->perform_http2_upgrade(upstream) != 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, upstream) << "HTTP Upgrade to HTTP/2 failed"; + } + } + + // Stop further processing to complete this request + return HPE_PAUSED; +} +} // namespace + +// on_read() does not consume all available data in input buffer if +// one http request is fully received. +int HttpsUpstream::on_read() { + auto rb = handler_->get_rb(); + auto rlimit = handler_->get_rlimit(); + auto downstream = get_downstream(); + + if (rb->rleft() == 0 || handler_->get_should_close_after_write()) { + return 0; + } + + // downstream can be nullptr here, because it is initialized in the + // callback chain called by llhttp_execute() + if (downstream && downstream->get_upgraded()) { + + auto rv = downstream->push_upload_data_chunk(rb->pos(), rb->rleft()); + + if (rv != 0) { + return -1; + } + + rb->reset(); + rlimit->startw(); + + if (downstream->request_buf_full()) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Downstream request buf is full"; + } + pause_read(SHRPX_NO_BUFFER); + + return 0; + } + + return 0; + } + + if (downstream) { + // To avoid reading next pipelined request + switch (downstream->get_request_state()) { + case DownstreamState::INITIAL: + case DownstreamState::HEADER_COMPLETE: + break; + default: + return 0; + } + } + + // llhttp_execute() does nothing once it entered error state. + auto htperr = llhttp_execute(&htp_, reinterpret_cast<const char *>(rb->pos()), + rb->rleft()); + + if (htperr == HPE_PAUSED_UPGRADE && + rb->pos() == + reinterpret_cast<const uint8_t *>(llhttp_get_error_pos(&htp_))) { + llhttp_resume_after_upgrade(&htp_); + + htperr = llhttp_execute(&htp_, reinterpret_cast<const char *>(rb->pos()), + rb->rleft()); + } + + auto nread = + htperr == HPE_OK + ? rb->rleft() + : reinterpret_cast<const uint8_t *>(llhttp_get_error_pos(&htp_)) - + rb->pos(); + rb->drain(nread); + rlimit->startw(); + + // Well, actually header length + some body bytes + current_header_length_ += nread; + + // Get downstream again because it may be initialized in http parser + // execution + downstream = get_downstream(); + + if (htperr == HPE_PAUSED) { + // We may pause parser in htp_msg_completecb when both side are + // completed. Signal write, so that we can run on_write(). + if (downstream && + downstream->get_request_state() == DownstreamState::MSG_COMPLETE && + downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + handler_->signal_write(); + } + return 0; + } + + if (htperr != HPE_OK) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "HTTP parse failure: " + << "(" << llhttp_errno_name(htperr) << ") " + << llhttp_get_error_reason(&htp_); + } + + if (downstream && + downstream->get_response_state() != DownstreamState::INITIAL) { + handler_->set_should_close_after_write(true); + handler_->signal_write(); + return 0; + } + + unsigned int status_code; + + if (htperr == HPE_INVALID_METHOD) { + status_code = 501; + } else if (downstream) { + status_code = downstream->response().http_status; + if (status_code == 0) { + if (downstream->get_request_state() == DownstreamState::CONNECT_FAIL) { + status_code = 502; + } else if (downstream->get_request_state() == + DownstreamState::HTTP1_REQUEST_HEADER_TOO_LARGE) { + status_code = 431; + } else { + status_code = 400; + } + } + } else { + status_code = 400; + } + + error_reply(status_code); + + handler_->signal_write(); + + return 0; + } + + // downstream can be NULL here. + if (downstream && downstream->request_buf_full()) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "Downstream request buffer is full"; + } + + pause_read(SHRPX_NO_BUFFER); + + return 0; + } + + return 0; +} + +int HttpsUpstream::on_write() { + auto downstream = get_downstream(); + if (!downstream) { + return 0; + } + + auto output = downstream->get_response_buf(); + const auto &resp = downstream->response(); + + if (output->rleft() > 0) { + return 0; + } + + // We need to postpone detachment until all data are sent so that + // we can notify nghttp2 library all data consumed. + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + if (downstream->can_detach_downstream_connection()) { + // Keep-alive + downstream->detach_downstream_connection(); + } else { + // Connection close + downstream->pop_downstream_connection(); + // dconn was deleted + } + // We need this if response ends before request. + if (downstream->get_request_state() == DownstreamState::MSG_COMPLETE) { + delete_downstream(); + + if (handler_->get_should_close_after_write()) { + return 0; + } + + auto conn = handler_->get_connection(); + auto &upstreamconf = get_config()->conn.upstream; + + conn->rt.repeat = upstreamconf.timeout.idle_read; + + handler_->repeat_read_timer(); + + return resume_read(SHRPX_NO_BUFFER, nullptr, 0); + } else { + // If the request is not complete, close the connection. + delete_downstream(); + + handler_->set_should_close_after_write(true); + + return 0; + } + } + + return downstream->resume_read(SHRPX_NO_BUFFER, resp.unconsumed_body_length); +} + +int HttpsUpstream::on_event() { return 0; } + +ClientHandler *HttpsUpstream::get_client_handler() const { return handler_; } + +void HttpsUpstream::pause_read(IOCtrlReason reason) { + ioctrl_.pause_read(reason); +} + +int HttpsUpstream::resume_read(IOCtrlReason reason, Downstream *downstream, + size_t consumed) { + // downstream could be nullptr + if (downstream && downstream->request_buf_full()) { + return 0; + } + if (ioctrl_.resume_read(reason)) { + // Process remaining data in input buffer here because these bytes + // are not notified by readcb until new data arrive. + llhttp_resume(&htp_); + + auto conn = handler_->get_connection(); + ev_feed_event(conn->loop, &conn->rev, EV_READ); + return 0; + } + + return 0; +} + +int HttpsUpstream::downstream_read(DownstreamConnection *dconn) { + auto downstream = dconn->get_downstream(); + int rv; + + rv = downstream->on_read(); + + if (rv == SHRPX_ERR_EOF) { + if (downstream->get_request_header_sent()) { + return downstream_eof(dconn); + } + return SHRPX_ERR_RETRY; + } + + if (rv == SHRPX_ERR_DCONN_CANCELED) { + downstream->pop_downstream_connection(); + goto end; + } + + if (rv < 0) { + return downstream_error(dconn, Downstream::EVENT_ERROR); + } + + if (downstream->get_response_state() == DownstreamState::MSG_RESET) { + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_BAD_HEADER) { + error_reply(502); + downstream->pop_downstream_connection(); + goto end; + } + + if (downstream->can_detach_downstream_connection()) { + // Keep-alive + downstream->detach_downstream_connection(); + } + +end: + handler_->signal_write(); + + return 0; +} + +int HttpsUpstream::downstream_write(DownstreamConnection *dconn) { + int rv; + rv = dconn->on_write(); + if (rv == SHRPX_ERR_NETWORK) { + return downstream_error(dconn, Downstream::EVENT_ERROR); + } + + if (rv != 0) { + return rv; + } + + return 0; +} + +int HttpsUpstream::downstream_eof(DownstreamConnection *dconn) { + auto downstream = dconn->get_downstream(); + + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "EOF"; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + goto end; + } + + if (downstream->get_response_state() == DownstreamState::HEADER_COMPLETE) { + // Server may indicate the end of the request by EOF + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "The end of the response body was indicated by " + << "EOF"; + } + on_downstream_body_complete(downstream); + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + downstream->pop_downstream_connection(); + goto end; + } + + if (downstream->get_response_state() == DownstreamState::INITIAL) { + // we did not send any response headers, so we can reply error + // message. + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, dconn) << "Return error reply"; + } + error_reply(502); + downstream->pop_downstream_connection(); + goto end; + } + + // Otherwise, we don't know how to recover from this situation. Just + // drop connection. + return -1; +end: + handler_->signal_write(); + + return 0; +} + +int HttpsUpstream::downstream_error(DownstreamConnection *dconn, int events) { + auto downstream = dconn->get_downstream(); + if (LOG_ENABLED(INFO)) { + if (events & Downstream::EVENT_ERROR) { + DCLOG(INFO, dconn) << "Network error/general error"; + } else { + DCLOG(INFO, dconn) << "Timeout"; + } + } + if (downstream->get_response_state() != DownstreamState::INITIAL) { + return -1; + } + + unsigned int status; + if (events & Downstream::EVENT_TIMEOUT) { + if (downstream->get_request_header_sent()) { + status = 504; + } else { + status = 408; + } + } else { + status = 502; + } + error_reply(status); + + downstream->pop_downstream_connection(); + + handler_->signal_write(); + return 0; +} + +int HttpsUpstream::send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) { + const auto &req = downstream->request(); + auto &resp = downstream->response(); + auto &balloc = downstream->get_block_allocator(); + auto config = get_config(); + auto &httpconf = config->http; + + auto connection_close = false; + + auto worker = handler_->get_worker(); + + if (httpconf.max_requests <= num_requests_ || + worker->get_graceful_shutdown()) { + resp.fs.add_header_token(StringRef::from_lit("connection"), + StringRef::from_lit("close"), false, + http2::HD_CONNECTION); + connection_close = true; + } else if (req.http_major <= 0 || + (req.http_major == 1 && req.http_minor == 0)) { + connection_close = true; + } else { + auto c = resp.fs.header(http2::HD_CONNECTION); + if (c && util::strieq_l("close", c->value)) { + connection_close = true; + } + } + + if (connection_close) { + resp.connection_close = true; + handler_->set_should_close_after_write(true); + } + + auto output = downstream->get_response_buf(); + + output->append("HTTP/1.1 "); + output->append(http2::stringify_status(balloc, resp.http_status)); + output->append(' '); + output->append(http2::get_reason_phrase(resp.http_status)); + output->append("\r\n"); + + for (auto &kv : resp.fs.headers()) { + if (kv.name.empty() || kv.name[0] == ':') { + continue; + } + http2::capitalize(output, kv.name); + output->append(": "); + output->append(kv.value); + output->append("\r\n"); + } + + if (!resp.fs.header(http2::HD_SERVER)) { + output->append("Server: "); + output->append(config->http.server_name); + output->append("\r\n"); + } + + for (auto &p : httpconf.add_response_headers) { + output->append(p.name); + output->append(": "); + output->append(p.value); + output->append("\r\n"); + } + + output->append("\r\n"); + + output->append(body, bodylen); + + downstream->response_sent_body_length += bodylen; + downstream->set_response_state(DownstreamState::MSG_COMPLETE); + + return 0; +} + +void HttpsUpstream::error_reply(unsigned int status_code) { + auto downstream = get_downstream(); + + if (!downstream) { + attach_downstream( + std::make_unique<Downstream>(this, handler_->get_mcpool(), 1)); + downstream = get_downstream(); + } + + auto &resp = downstream->response(); + auto &balloc = downstream->get_block_allocator(); + + auto html = http::create_error_html(balloc, status_code); + + resp.http_status = status_code; + // we are going to close connection for both frontend and backend in + // error condition. This is safest option. + resp.connection_close = true; + handler_->set_should_close_after_write(true); + + auto output = downstream->get_response_buf(); + + output->append("HTTP/1.1 "); + output->append(http2::stringify_status(balloc, status_code)); + output->append(' '); + output->append(http2::get_reason_phrase(status_code)); + output->append("\r\nServer: "); + output->append(get_config()->http.server_name); + output->append("\r\nContent-Length: "); + std::array<uint8_t, NGHTTP2_MAX_UINT64_DIGITS> intbuf; + output->append(StringRef{std::begin(intbuf), + util::utos(std::begin(intbuf), html.size())}); + output->append("\r\nDate: "); + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + output->append(lgconf->tstamp->time_http); + output->append("\r\nContent-Type: text/html; " + "charset=UTF-8\r\nConnection: close\r\n\r\n"); + output->append(html); + + downstream->response_sent_body_length += html.size(); + downstream->set_response_state(DownstreamState::MSG_COMPLETE); +} + +void HttpsUpstream::attach_downstream(std::unique_ptr<Downstream> downstream) { + assert(!downstream_); + downstream_ = std::move(downstream); +} + +void HttpsUpstream::delete_downstream() { + if (downstream_ && downstream_->accesslog_ready()) { + handler_->write_accesslog(downstream_.get()); + } + + downstream_.reset(); +} + +Downstream *HttpsUpstream::get_downstream() const { return downstream_.get(); } + +std::unique_ptr<Downstream> HttpsUpstream::pop_downstream() { + return std::unique_ptr<Downstream>(downstream_.release()); +} + +int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + if (downstream->get_non_final_response()) { + DLOG(INFO, downstream) << "HTTP non-final response header"; + } else { + DLOG(INFO, downstream) << "HTTP response header completed"; + } + } + + const auto &req = downstream->request(); + auto &resp = downstream->response(); + auto &balloc = downstream->get_block_allocator(); + auto dconn = downstream->get_downstream_connection(); + // dconn might be nullptr if this is non-final response from mruby. + + if (downstream->get_non_final_response() && + !downstream->supports_non_final_response()) { + resp.fs.clear_headers(); + return 0; + } + +#ifdef HAVE_MRUBY + if (!downstream->get_non_final_response()) { + assert(dconn); + const auto &group = dconn->get_downstream_addr_group(); + if (group) { + const auto &dmruby_ctx = group->shared_addr->mruby_ctx; + + if (dmruby_ctx->run_on_response_proc(downstream) != 0) { + error_reply(500); + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return -1; + } + } + + auto worker = handler_->get_worker(); + auto mruby_ctx = worker->get_mruby_context(); + + if (mruby_ctx->run_on_response_proc(downstream) != 0) { + error_reply(500); + return -1; + } + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + return -1; + } + } +#endif // HAVE_MRUBY + + auto connect_method = req.method == HTTP_CONNECT; + + auto buf = downstream->get_response_buf(); + buf->append("HTTP/"); + buf->append('0' + req.http_major); + buf->append('.'); + buf->append('0' + req.http_minor); + buf->append(' '); + if (req.connect_proto != ConnectProto::NONE && downstream->get_upgraded()) { + buf->append(http2::stringify_status(balloc, 101)); + buf->append(' '); + buf->append(http2::get_reason_phrase(101)); + } else { + buf->append(http2::stringify_status(balloc, resp.http_status)); + buf->append(' '); + buf->append(http2::get_reason_phrase(resp.http_status)); + } + buf->append("\r\n"); + + auto config = get_config(); + auto &httpconf = config->http; + + if (!config->http2_proxy && !httpconf.no_location_rewrite) { + downstream->rewrite_location_response_header( + get_client_handler()->get_upstream_scheme()); + } + + if (downstream->get_non_final_response()) { + http2::build_http1_headers_from_headers(buf, resp.fs.headers(), + http2::HDOP_STRIP_ALL); + + buf->append("\r\n"); + + if (LOG_ENABLED(INFO)) { + log_response_headers(buf); + } + + resp.fs.clear_headers(); + + return 0; + } + + auto build_flags = (http2::HDOP_STRIP_ALL & ~http2::HDOP_STRIP_VIA) | + (!http2::legacy_http1(req.http_major, req.http_minor) + ? 0 + : http2::HDOP_STRIP_TRANSFER_ENCODING); + + http2::build_http1_headers_from_headers(buf, resp.fs.headers(), build_flags); + + auto worker = handler_->get_worker(); + + // after graceful shutdown commenced, add connection: close header + // field. + if (httpconf.max_requests <= num_requests_ || + worker->get_graceful_shutdown()) { + resp.connection_close = true; + } + + // We check downstream->get_response_connection_close() in case when + // the Content-Length is not available. + if (!req.connection_close && !resp.connection_close) { + if (req.http_major <= 0 || req.http_minor <= 0) { + // We add this header for HTTP/1.0 or HTTP/0.9 clients + buf->append("Connection: Keep-Alive\r\n"); + } + } else if (!downstream->get_upgraded()) { + buf->append("Connection: close\r\n"); + } + + if (!connect_method && downstream->get_upgraded()) { + if (req.connect_proto == ConnectProto::WEBSOCKET && + resp.http_status / 100 == 2) { + buf->append("Upgrade: websocket\r\nConnection: Upgrade\r\n"); + auto key = req.fs.header(http2::HD_SEC_WEBSOCKET_KEY); + if (!key || key->value.size() != base64::encode_length(16)) { + return -1; + } + std::array<uint8_t, base64::encode_length(20)> out; + auto accept = http2::make_websocket_accept_token(out.data(), key->value); + if (accept.empty()) { + return -1; + } + buf->append("Sec-WebSocket-Accept: "); + buf->append(accept); + buf->append("\r\n"); + } else { + auto connection = resp.fs.header(http2::HD_CONNECTION); + if (connection) { + buf->append("Connection: "); + buf->append((*connection).value); + buf->append("\r\n"); + } + + auto upgrade = resp.fs.header(http2::HD_UPGRADE); + if (upgrade) { + buf->append("Upgrade: "); + buf->append((*upgrade).value); + buf->append("\r\n"); + } + } + } + + if (!resp.fs.header(http2::HD_ALT_SVC)) { + // We won't change or alter alt-svc from backend for now + if (!httpconf.altsvcs.empty()) { + buf->append("Alt-Svc: "); + buf->append(httpconf.altsvc_header_value); + buf->append("\r\n"); + } + } + + if (!config->http2_proxy && !httpconf.no_server_rewrite) { + buf->append("Server: "); + buf->append(httpconf.server_name); + buf->append("\r\n"); + } else { + auto server = resp.fs.header(http2::HD_SERVER); + if (server) { + buf->append("Server: "); + buf->append((*server).value); + buf->append("\r\n"); + } + } + + if (req.method != HTTP_CONNECT || !downstream->get_upgraded()) { + auto affinity_cookie = downstream->get_affinity_cookie_to_send(); + if (affinity_cookie) { + auto &group = dconn->get_downstream_addr_group(); + auto &shared_addr = group->shared_addr; + auto &cookieconf = shared_addr->affinity.cookie; + auto secure = + http::require_cookie_secure_attribute(cookieconf.secure, req.scheme); + auto cookie_str = http::create_affinity_cookie( + balloc, cookieconf.name, affinity_cookie, cookieconf.path, secure); + buf->append("Set-Cookie: "); + buf->append(cookie_str); + buf->append("\r\n"); + } + } + + auto via = resp.fs.header(http2::HD_VIA); + if (httpconf.no_via) { + if (via) { + buf->append("Via: "); + buf->append((*via).value); + buf->append("\r\n"); + } + } else { + buf->append("Via: "); + if (via) { + buf->append((*via).value); + buf->append(", "); + } + std::array<char, 16> viabuf; + auto end = http::create_via_header_value(viabuf.data(), resp.http_major, + resp.http_minor); + buf->append(viabuf.data(), end - std::begin(viabuf)); + buf->append("\r\n"); + } + + for (auto &p : httpconf.add_response_headers) { + buf->append(p.name); + buf->append(": "); + buf->append(p.value); + buf->append("\r\n"); + } + + buf->append("\r\n"); + + if (LOG_ENABLED(INFO)) { + log_response_headers(buf); + } + + return 0; +} + +int HttpsUpstream::on_downstream_body(Downstream *downstream, + const uint8_t *data, size_t len, + bool flush) { + if (len == 0) { + return 0; + } + auto output = downstream->get_response_buf(); + if (downstream->get_chunked_response()) { + output->append(util::utox(len)); + output->append("\r\n"); + } + output->append(data, len); + + downstream->response_sent_body_length += len; + + if (downstream->get_chunked_response()) { + output->append("\r\n"); + } + return 0; +} + +int HttpsUpstream::on_downstream_body_complete(Downstream *downstream) { + const auto &req = downstream->request(); + auto &resp = downstream->response(); + + if (downstream->get_chunked_response()) { + auto output = downstream->get_response_buf(); + const auto &trailers = resp.fs.trailers(); + if (trailers.empty()) { + output->append("0\r\n\r\n"); + } else { + output->append("0\r\n"); + http2::build_http1_headers_from_headers(output, trailers, + http2::HDOP_STRIP_ALL); + output->append("\r\n"); + } + } + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "HTTP response completed"; + } + + if (!downstream->validate_response_recv_body_length()) { + resp.connection_close = true; + } + + if (req.connection_close || resp.connection_close || + // To avoid to stall upload body + downstream->get_request_state() != DownstreamState::MSG_COMPLETE) { + auto handler = get_client_handler(); + handler->set_should_close_after_write(true); + } + return 0; +} + +int HttpsUpstream::on_downstream_abort_request(Downstream *downstream, + unsigned int status_code) { + error_reply(status_code); + handler_->signal_write(); + return 0; +} + +int HttpsUpstream::on_downstream_abort_request_with_https_redirect( + Downstream *downstream) { + redirect_to_https(downstream); + handler_->signal_write(); + return 0; +} + +int HttpsUpstream::redirect_to_https(Downstream *downstream) { + auto &req = downstream->request(); + if (req.method == HTTP_CONNECT || req.scheme != "http" || + req.authority.empty()) { + error_reply(400); + return 0; + } + + auto authority = util::extract_host(req.authority); + if (authority.empty()) { + error_reply(400); + return 0; + } + + auto &balloc = downstream->get_block_allocator(); + auto config = get_config(); + auto &httpconf = config->http; + + StringRef loc; + if (httpconf.redirect_https_port == StringRef::from_lit("443")) { + loc = concat_string_ref(balloc, StringRef::from_lit("https://"), authority, + req.path); + } else { + loc = concat_string_ref(balloc, StringRef::from_lit("https://"), authority, + StringRef::from_lit(":"), + httpconf.redirect_https_port, req.path); + } + + auto &resp = downstream->response(); + resp.http_status = 308; + resp.fs.add_header_token(StringRef::from_lit("location"), loc, false, + http2::HD_LOCATION); + resp.fs.add_header_token(StringRef::from_lit("connection"), + StringRef::from_lit("close"), false, + http2::HD_CONNECTION); + + return send_reply(downstream, nullptr, 0); +} + +void HttpsUpstream::log_response_headers(DefaultMemchunks *buf) const { + std::string nhdrs; + for (auto chunk = buf->head; chunk; chunk = chunk->next) { + nhdrs.append(chunk->pos, chunk->last); + } + if (log_config()->errorlog_tty) { + nhdrs = http::colorizeHeaders(nhdrs.c_str()); + } + ULOG(INFO, this) << "HTTP response headers\n" << nhdrs; +} + +void HttpsUpstream::on_handler_delete() { + if (downstream_ && downstream_->accesslog_ready()) { + handler_->write_accesslog(downstream_.get()); + } +} + +int HttpsUpstream::on_downstream_reset(Downstream *downstream, bool no_retry) { + int rv; + std::unique_ptr<DownstreamConnection> dconn; + + assert(downstream == downstream_.get()); + + downstream_->pop_downstream_connection(); + + if (!downstream_->request_submission_ready()) { + switch (downstream_->get_response_state()) { + case DownstreamState::MSG_COMPLETE: + // We have got all response body already. Send it off. + return 0; + case DownstreamState::INITIAL: + if (on_downstream_abort_request(downstream_.get(), 502) != 0) { + return -1; + } + return 0; + default: + break; + } + // Return error so that caller can delete handler + return -1; + } + + downstream_->add_retry(); + + rv = 0; + + if (no_retry || downstream_->no_more_retry()) { + goto fail; + } + + for (;;) { + auto dconn = handler_->get_downstream_connection(rv, downstream_.get()); + if (!dconn) { + goto fail; + } + + rv = downstream_->attach_downstream_connection(std::move(dconn)); + if (rv == 0) { + break; + } + } + + rv = downstream_->push_request_headers(); + if (rv != 0) { + goto fail; + } + + return 0; + +fail: + if (rv == SHRPX_ERR_TLS_REQUIRED) { + rv = on_downstream_abort_request_with_https_redirect(downstream); + } else { + rv = on_downstream_abort_request(downstream_.get(), 502); + } + if (rv != 0) { + return -1; + } + downstream_->pop_downstream_connection(); + + return 0; +} + +int HttpsUpstream::initiate_push(Downstream *downstream, const StringRef &uri) { + return 0; +} + +int HttpsUpstream::response_riovec(struct iovec *iov, int iovcnt) const { + if (!downstream_) { + return 0; + } + + auto buf = downstream_->get_response_buf(); + + return buf->riovec(iov, iovcnt); +} + +void HttpsUpstream::response_drain(size_t n) { + if (!downstream_) { + return; + } + + auto buf = downstream_->get_response_buf(); + + buf->drain(n); +} + +bool HttpsUpstream::response_empty() const { + if (!downstream_) { + return true; + } + + auto buf = downstream_->get_response_buf(); + + return buf->rleft() == 0; +} + +Downstream * +HttpsUpstream::on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) { + return nullptr; +} + +int HttpsUpstream::on_downstream_push_promise_complete( + Downstream *downstream, Downstream *promised_downstream) { + return -1; +} + +bool HttpsUpstream::push_enabled() const { return false; } + +void HttpsUpstream::cancel_premature_downstream( + Downstream *promised_downstream) {} + +} // namespace shrpx diff --git a/src/shrpx_https_upstream.h b/src/shrpx_https_upstream.h new file mode 100644 index 0000000..d85d2de --- /dev/null +++ b/src/shrpx_https_upstream.h @@ -0,0 +1,113 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_HTTPS_UPSTREAM_H +#define SHRPX_HTTPS_UPSTREAM_H + +#include "shrpx.h" + +#include <cinttypes> +#include <memory> + +#include "llhttp.h" + +#include "shrpx_upstream.h" +#include "memchunk.h" + +using namespace nghttp2; + +namespace shrpx { + +class ClientHandler; + +class HttpsUpstream : public Upstream { +public: + HttpsUpstream(ClientHandler *handler); + virtual ~HttpsUpstream(); + virtual int on_read(); + virtual int on_write(); + virtual int on_event(); + virtual int on_downstream_abort_request(Downstream *downstream, + unsigned int status_code); + virtual int + on_downstream_abort_request_with_https_redirect(Downstream *downstream); + virtual ClientHandler *get_client_handler() const; + + virtual int downstream_read(DownstreamConnection *dconn); + virtual int downstream_write(DownstreamConnection *dconn); + virtual int downstream_eof(DownstreamConnection *dconn); + virtual int downstream_error(DownstreamConnection *dconn, int events); + + void attach_downstream(std::unique_ptr<Downstream> downstream); + void delete_downstream(); + Downstream *get_downstream() const; + std::unique_ptr<Downstream> pop_downstream(); + void error_reply(unsigned int status_code); + + virtual void pause_read(IOCtrlReason reason); + virtual int resume_read(IOCtrlReason reason, Downstream *downstream, + size_t consumed); + + virtual int on_downstream_header_complete(Downstream *downstream); + virtual int on_downstream_body(Downstream *downstream, const uint8_t *data, + size_t len, bool flush); + virtual int on_downstream_body_complete(Downstream *downstream); + + virtual void on_handler_delete(); + virtual int on_downstream_reset(Downstream *downstream, bool no_retry); + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen); + virtual int initiate_push(Downstream *downstream, const StringRef &uri); + virtual int response_riovec(struct iovec *iov, int iovcnt) const; + virtual void response_drain(size_t n); + virtual bool response_empty() const; + + virtual Downstream *on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id); + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream); + virtual bool push_enabled() const; + virtual void cancel_premature_downstream(Downstream *promised_downstream); + + void reset_current_header_length(); + void log_response_headers(DefaultMemchunks *buf) const; + int redirect_to_https(Downstream *downstream); + + // Called when new request has started. + void on_start_request(); + +private: + ClientHandler *handler_; + llhttp_t htp_; + size_t current_header_length_; + std::unique_ptr<Downstream> downstream_; + IOControl ioctrl_; + // The number of requests seen so far. + size_t num_requests_; +}; + +} // namespace shrpx + +#endif // SHRPX_HTTPS_UPSTREAM_H diff --git a/src/shrpx_io_control.cc b/src/shrpx_io_control.cc new file mode 100644 index 0000000..f43a257 --- /dev/null +++ b/src/shrpx_io_control.cc @@ -0,0 +1,66 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_io_control.h" + +#include <algorithm> + +#include "shrpx_rate_limit.h" +#include "util.h" + +using namespace nghttp2; + +namespace shrpx { + +IOControl::IOControl(RateLimit *lim) : lim_(lim), rdbits_(0) {} + +IOControl::~IOControl() {} + +void IOControl::pause_read(IOCtrlReason reason) { + rdbits_ |= reason; + if (lim_) { + lim_->stopw(); + } +} + +bool IOControl::resume_read(IOCtrlReason reason) { + rdbits_ &= ~reason; + if (rdbits_ == 0) { + if (lim_) { + lim_->startw(); + } + return true; + } + + return false; +} + +void IOControl::force_resume_read() { + rdbits_ = 0; + if (lim_) { + lim_->startw(); + } +} + +} // namespace shrpx diff --git a/src/shrpx_io_control.h b/src/shrpx_io_control.h new file mode 100644 index 0000000..d427fcb --- /dev/null +++ b/src/shrpx_io_control.h @@ -0,0 +1,58 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_IO_CONTROL_H +#define SHRPX_IO_CONTROL_H + +#include "shrpx.h" + +#include <cinttypes> +#include <vector> + +#include <ev.h> + +#include "shrpx_rate_limit.h" + +namespace shrpx { + +enum IOCtrlReason { SHRPX_NO_BUFFER = 1 << 0, SHRPX_MSG_BLOCK = 1 << 1 }; + +class IOControl { +public: + IOControl(RateLimit *lim); + ~IOControl(); + void pause_read(IOCtrlReason reason); + // Returns true if read operation is enabled after this call + bool resume_read(IOCtrlReason reason); + // Clear all pause flags and enable read + void force_resume_read(); + +private: + RateLimit *lim_; + uint32_t rdbits_; +}; + +} // namespace shrpx + +#endif // SHRPX_IO_CONTROL_H diff --git a/src/shrpx_live_check.cc b/src/shrpx_live_check.cc new file mode 100644 index 0000000..9b932cd --- /dev/null +++ b/src/shrpx_live_check.cc @@ -0,0 +1,792 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_live_check.h" +#include "shrpx_worker.h" +#include "shrpx_connect_blocker.h" +#include "shrpx_tls.h" +#include "shrpx_log.h" + +namespace shrpx { + +namespace { +constexpr size_t MAX_BUFFER_SIZE = 4_k; +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto conn = static_cast<Connection *>(w->data); + auto live_check = static_cast<LiveCheck *>(conn->data); + + rv = live_check->do_read(); + if (rv != 0) { + live_check->on_failure(); + return; + } +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + int rv; + auto conn = static_cast<Connection *>(w->data); + auto live_check = static_cast<LiveCheck *>(conn->data); + + rv = live_check->do_write(); + if (rv != 0) { + live_check->on_failure(); + return; + } +} +} // namespace + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto live_check = static_cast<LiveCheck *>(conn->data); + + if (w == &conn->rt && !conn->expired_rt()) { + return; + } + + live_check->on_failure(); +} +} // namespace + +namespace { +void backoff_timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + int rv; + auto live_check = static_cast<LiveCheck *>(w->data); + + rv = live_check->initiate_connection(); + if (rv != 0) { + live_check->on_failure(); + return; + } +} +} // namespace + +namespace { +void settings_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto live_check = static_cast<LiveCheck *>(w->data); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SETTINGS timeout"; + } + + live_check->on_failure(); +} +} // namespace + +LiveCheck::LiveCheck(struct ev_loop *loop, SSL_CTX *ssl_ctx, Worker *worker, + DownstreamAddr *addr, std::mt19937 &gen) + : conn_(loop, -1, nullptr, worker->get_mcpool(), + worker->get_downstream_config()->timeout.write, + worker->get_downstream_config()->timeout.read, {}, {}, writecb, + readcb, timeoutcb, this, get_config()->tls.dyn_rec.warmup_threshold, + get_config()->tls.dyn_rec.idle_timeout, Proto::NONE), + wb_(worker->get_mcpool()), + gen_(gen), + read_(&LiveCheck::noop), + write_(&LiveCheck::noop), + worker_(worker), + ssl_ctx_(ssl_ctx), + addr_(addr), + session_(nullptr), + raddr_(nullptr), + success_count_(0), + fail_count_(0), + settings_ack_received_(false), + session_closing_(false) { + ev_timer_init(&backoff_timer_, backoff_timeoutcb, 0., 0.); + backoff_timer_.data = this; + + // SETTINGS ACK must be received in a short timeout. Otherwise, we + // assume that connection is broken. + ev_timer_init(&settings_timer_, settings_timeout_cb, 0., 0.); + settings_timer_.data = this; +} + +LiveCheck::~LiveCheck() { + disconnect(); + + ev_timer_stop(conn_.loop, &backoff_timer_); +} + +void LiveCheck::disconnect() { + if (dns_query_) { + auto dns_tracker = worker_->get_dns_tracker(); + + dns_tracker->cancel(dns_query_.get()); + } + + dns_query_.reset(); + // We can reuse resolved_addr_ + raddr_ = nullptr; + + conn_.rlimit.stopw(); + conn_.wlimit.stopw(); + + ev_timer_stop(conn_.loop, &settings_timer_); + + read_ = write_ = &LiveCheck::noop; + + conn_.disconnect(); + + nghttp2_session_del(session_); + session_ = nullptr; + + settings_ack_received_ = false; + session_closing_ = false; + + wb_.reset(); +} + +// Use the similar backoff algorithm described in +// https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md +namespace { +constexpr size_t MAX_BACKOFF_EXP = 10; +constexpr auto MULTIPLIER = 1.6; +constexpr auto JITTER = 0.2; +} // namespace + +void LiveCheck::schedule() { + auto base_backoff = + util::int_pow(MULTIPLIER, std::min(fail_count_, MAX_BACKOFF_EXP)); + auto dist = std::uniform_real_distribution<>(-JITTER * base_backoff, + JITTER * base_backoff); + + auto &downstreamconf = *get_config()->conn.downstream; + + auto backoff = + std::min(downstreamconf.timeout.max_backoff, base_backoff + dist(gen_)); + + ev_timer_set(&backoff_timer_, backoff, 0.); + ev_timer_start(conn_.loop, &backoff_timer_); +} + +int LiveCheck::do_read() { return read_(*this); } + +int LiveCheck::do_write() { return write_(*this); } + +int LiveCheck::initiate_connection() { + int rv; + + auto worker_blocker = worker_->get_connect_blocker(); + if (worker_blocker->blocked()) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Worker wide backend connection was blocked temporarily"; + } + return -1; + } + + if (!dns_query_ && addr_->tls) { + assert(ssl_ctx_); + + auto ssl = tls::create_ssl(ssl_ctx_); + if (!ssl) { + return -1; + } + + switch (addr_->proto) { + case Proto::HTTP1: + tls::setup_downstream_http1_alpn(ssl); + break; + case Proto::HTTP2: + tls::setup_downstream_http2_alpn(ssl); + break; + default: + assert(0); + } + + conn_.set_ssl(ssl); + conn_.tls.client_session_cache = &addr_->tls_session_cache; + } + + if (addr_->dns) { + if (!dns_query_) { + auto dns_query = std::make_unique<DNSQuery>( + addr_->host, [this](DNSResolverStatus status, const Address *result) { + int rv; + + if (status == DNSResolverStatus::OK) { + *this->resolved_addr_ = *result; + } + rv = this->initiate_connection(); + if (rv != 0) { + this->on_failure(); + } + }); + auto dns_tracker = worker_->get_dns_tracker(); + + if (!resolved_addr_) { + resolved_addr_ = std::make_unique<Address>(); + } + + switch (dns_tracker->resolve(resolved_addr_.get(), dns_query.get())) { + case DNSResolverStatus::ERROR: + return -1; + case DNSResolverStatus::RUNNING: + dns_query_ = std::move(dns_query); + return 0; + case DNSResolverStatus::OK: + break; + default: + assert(0); + } + } else { + switch (dns_query_->status) { + case DNSResolverStatus::ERROR: + dns_query_.reset(); + return -1; + case DNSResolverStatus::OK: + dns_query_.reset(); + break; + default: + assert(0); + } + } + + util::set_port(*resolved_addr_, addr_->port); + raddr_ = resolved_addr_.get(); + } else { + raddr_ = &addr_->addr; + } + + conn_.fd = util::create_nonblock_socket(raddr_->su.storage.ss_family); + + if (conn_.fd == -1) { + auto error = errno; + LOG(WARN) << "socket() failed; addr=" << util::to_numeric_addr(raddr_) + << ", errno=" << error; + return -1; + } + + rv = connect(conn_.fd, &raddr_->su.sa, raddr_->len); + if (rv != 0 && errno != EINPROGRESS) { + auto error = errno; + LOG(WARN) << "connect() failed; addr=" << util::to_numeric_addr(raddr_) + << ", errno=" << error; + + close(conn_.fd); + conn_.fd = -1; + + return -1; + } + + if (addr_->tls) { + auto sni_name = + addr_->sni.empty() ? StringRef{addr_->host} : StringRef{addr_->sni}; + if (!util::numeric_host(sni_name.c_str())) { + SSL_set_tlsext_host_name(conn_.tls.ssl, sni_name.c_str()); + } + + auto session = tls::reuse_tls_session(addr_->tls_session_cache); + if (session) { + SSL_set_session(conn_.tls.ssl, session); + SSL_SESSION_free(session); + } + + conn_.prepare_client_handshake(); + } + + write_ = &LiveCheck::connected; + + ev_io_set(&conn_.wev, conn_.fd, EV_WRITE); + ev_io_set(&conn_.rev, conn_.fd, EV_READ); + + conn_.wlimit.startw(); + + auto &downstreamconf = *get_config()->conn.downstream; + + conn_.wt.repeat = downstreamconf.timeout.connect; + ev_timer_again(conn_.loop, &conn_.wt); + + return 0; +} + +int LiveCheck::connected() { + auto sock_error = util::get_socket_error(conn_.fd); + if (sock_error != 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Backend connect failed; addr=" + << util::to_numeric_addr(raddr_) << ": errno=" << sock_error; + } + + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Connection established"; + } + + auto &downstreamconf = *get_config()->conn.downstream; + + // Reset timeout for write. Previously, we set timeout for connect. + conn_.wt.repeat = downstreamconf.timeout.write; + ev_timer_again(conn_.loop, &conn_.wt); + + conn_.rlimit.startw(); + conn_.again_rt(); + + if (conn_.tls.ssl) { + read_ = &LiveCheck::tls_handshake; + write_ = &LiveCheck::tls_handshake; + + return do_write(); + } + + if (addr_->proto == Proto::HTTP2) { + // For HTTP/2, we try to read SETTINGS ACK from server to make + // sure it is really alive, and serving HTTP/2. + read_ = &LiveCheck::read_clear; + write_ = &LiveCheck::write_clear; + + if (connection_made() != 0) { + return -1; + } + + return 0; + } + + on_success(); + + return 0; +} + +int LiveCheck::tls_handshake() { + conn_.last_read = std::chrono::steady_clock::now(); + + ERR_clear_error(); + + auto rv = conn_.tls_handshake(); + + if (rv == SHRPX_ERR_INPROGRESS) { + return 0; + } + + if (rv < 0) { + return rv; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL/TLS handshake completed"; + } + + if (!get_config()->tls.insecure && + tls::check_cert(conn_.tls.ssl, addr_, raddr_) != 0) { + return -1; + } + + // Check negotiated ALPN + + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len = 0; + + SSL_get0_alpn_selected(conn_.tls.ssl, &next_proto, &next_proto_len); + + auto proto = StringRef{next_proto, next_proto_len}; + + switch (addr_->proto) { + case Proto::HTTP1: + if (proto.empty() || proto == StringRef::from_lit("http/1.1")) { + break; + } + return -1; + case Proto::HTTP2: + if (util::check_h2_is_selected(proto)) { + // For HTTP/2, we try to read SETTINGS ACK from server to make + // sure it is really alive, and serving HTTP/2. + read_ = &LiveCheck::read_tls; + write_ = &LiveCheck::write_tls; + + if (connection_made() != 0) { + return -1; + } + + return 0; + } + return -1; + default: + break; + } + + on_success(); + + return 0; +} + +int LiveCheck::read_tls() { + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<uint8_t, 4_k> buf; + + ERR_clear_error(); + + for (;;) { + auto nread = conn_.read_tls(buf.data(), buf.size()); + + if (nread == 0) { + return 0; + } + + if (nread < 0) { + return nread; + } + + if (on_read(buf.data(), nread) != 0) { + return -1; + } + } +} + +int LiveCheck::write_tls() { + conn_.last_read = std::chrono::steady_clock::now(); + + ERR_clear_error(); + + struct iovec iov; + + for (;;) { + if (wb_.rleft() > 0) { + auto iovcnt = wb_.riovec(&iov, 1); + if (iovcnt != 1) { + assert(0); + return -1; + } + auto nwrite = conn_.write_tls(iov.iov_base, iov.iov_len); + + if (nwrite == 0) { + return 0; + } + + if (nwrite < 0) { + return nwrite; + } + + wb_.drain(nwrite); + + continue; + } + + if (on_write() != 0) { + return -1; + } + + if (wb_.rleft() == 0) { + conn_.start_tls_write_idle(); + break; + } + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + if (settings_ack_received_) { + on_success(); + } + + return 0; +} + +int LiveCheck::read_clear() { + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<uint8_t, 4_k> buf; + + for (;;) { + auto nread = conn_.read_clear(buf.data(), buf.size()); + + if (nread == 0) { + return 0; + } + + if (nread < 0) { + return nread; + } + + if (on_read(buf.data(), nread) != 0) { + return -1; + } + } +} + +int LiveCheck::write_clear() { + conn_.last_read = std::chrono::steady_clock::now(); + + struct iovec iov; + + for (;;) { + if (wb_.rleft() > 0) { + auto iovcnt = wb_.riovec(&iov, 1); + if (iovcnt != 1) { + assert(0); + return -1; + } + auto nwrite = conn_.write_clear(iov.iov_base, iov.iov_len); + + if (nwrite == 0) { + return 0; + } + + if (nwrite < 0) { + return nwrite; + } + + wb_.drain(nwrite); + + continue; + } + + if (on_write() != 0) { + return -1; + } + + if (wb_.rleft() == 0) { + break; + } + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + if (settings_ack_received_) { + on_success(); + } + + return 0; +} + +int LiveCheck::on_read(const uint8_t *data, size_t len) { + ssize_t rv; + + rv = nghttp2_session_mem_recv(session_, data, len); + if (rv < 0) { + LOG(ERROR) << "nghttp2_session_mem_recv() returned error: " + << nghttp2_strerror(rv); + return -1; + } + + if (settings_ack_received_ && !session_closing_) { + session_closing_ = true; + rv = nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); + if (rv != 0) { + return -1; + } + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "No more read/write for this session"; + } + + // If we have SETTINGS ACK already, we treat this success. + if (settings_ack_received_) { + return 0; + } + + return -1; + } + + signal_write(); + + return 0; +} + +int LiveCheck::on_write() { + for (;;) { + const uint8_t *data; + auto datalen = nghttp2_session_mem_send(session_, &data); + + if (datalen < 0) { + LOG(ERROR) << "nghttp2_session_mem_send() returned error: " + << nghttp2_strerror(datalen); + return -1; + } + if (datalen == 0) { + break; + } + wb_.append(data, datalen); + + if (wb_.rleft() >= MAX_BUFFER_SIZE) { + break; + } + } + + if (nghttp2_session_want_read(session_) == 0 && + nghttp2_session_want_write(session_) == 0 && wb_.rleft() == 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "No more read/write for this session"; + } + + if (settings_ack_received_) { + return 0; + } + + return -1; + } + + return 0; +} + +void LiveCheck::on_failure() { + ++fail_count_; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Liveness check for " << addr_->host << ":" << addr_->port + << " failed " << fail_count_ << " time(s) in a row"; + } + + disconnect(); + + schedule(); +} + +void LiveCheck::on_success() { + ++success_count_; + fail_count_ = 0; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Liveness check for " << addr_->host << ":" << addr_->port + << " succeeded " << success_count_ << " time(s) in a row"; + } + + if (success_count_ < addr_->rise) { + disconnect(); + + schedule(); + + return; + } + + LOG(NOTICE) << util::to_numeric_addr(&addr_->addr) << " is considered online"; + + addr_->connect_blocker->online(); + + success_count_ = 0; + fail_count_ = 0; + + disconnect(); +} + +int LiveCheck::noop() { return 0; } + +void LiveCheck::start_settings_timer() { + auto &downstreamconf = get_config()->http2.downstream; + + ev_timer_set(&settings_timer_, downstreamconf.timeout.settings, 0.); + ev_timer_start(conn_.loop, &settings_timer_); +} + +void LiveCheck::stop_settings_timer() { + ev_timer_stop(conn_.loop, &settings_timer_); +} + +void LiveCheck::settings_ack_received() { settings_ack_received_ = true; } + +namespace { +int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + auto live_check = static_cast<LiveCheck *>(user_data); + + if (frame->hd.type != NGHTTP2_SETTINGS || + (frame->hd.flags & NGHTTP2_FLAG_ACK)) { + return 0; + } + + live_check->start_settings_timer(); + + return 0; +} +} // namespace + +namespace { +int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, + void *user_data) { + auto live_check = static_cast<LiveCheck *>(user_data); + + if (frame->hd.type != NGHTTP2_SETTINGS || + (frame->hd.flags & NGHTTP2_FLAG_ACK) == 0) { + return 0; + } + + live_check->stop_settings_timer(); + live_check->settings_ack_received(); + + return 0; +} +} // namespace + +int LiveCheck::connection_made() { + int rv; + + nghttp2_session_callbacks *callbacks; + rv = nghttp2_session_callbacks_new(&callbacks); + if (rv != 0) { + return -1; + } + + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, + on_frame_send_callback); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, + on_frame_recv_callback); + + rv = nghttp2_session_client_new(&session_, callbacks, this); + + nghttp2_session_callbacks_del(callbacks); + + if (rv != 0) { + return -1; + } + + rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, nullptr, 0); + if (rv != 0) { + return -1; + } + + auto must_terminate = + addr_->tls && !nghttp2::tls::check_http2_requirement(conn_.tls.ssl); + + if (must_terminate) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "TLSv1.2 was not negotiated. HTTP/2 must not be negotiated."; + } + + rv = nghttp2_session_terminate_session(session_, + NGHTTP2_INADEQUATE_SECURITY); + if (rv != 0) { + return -1; + } + } + + signal_write(); + + return 0; +} + +void LiveCheck::signal_write() { conn_.wlimit.startw(); } + +} // namespace shrpx diff --git a/src/shrpx_live_check.h b/src/shrpx_live_check.h new file mode 100644 index 0000000..b65ecdc --- /dev/null +++ b/src/shrpx_live_check.h @@ -0,0 +1,125 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_LIVE_CHECK_H +#define SHRPX_LIVE_CHECK_H + +#include "shrpx.h" + +#include <functional> +#include <random> + +#include <openssl/ssl.h> + +#include <ev.h> + +#include <nghttp2/nghttp2.h> + +#include "shrpx_connection.h" + +namespace shrpx { + +class Worker; +struct DownstreamAddr; +struct DNSQuery; + +class LiveCheck { +public: + LiveCheck(struct ev_loop *loop, SSL_CTX *ssl_ctx, Worker *worker, + DownstreamAddr *addr, std::mt19937 &gen); + ~LiveCheck(); + + void disconnect(); + + void on_success(); + void on_failure(); + + int initiate_connection(); + + // Schedules next connection attempt + void schedule(); + + // Low level I/O operation callback; they are called from do_read() + // or do_write(). + int noop(); + int connected(); + int tls_handshake(); + int read_tls(); + int write_tls(); + int read_clear(); + int write_clear(); + + int do_read(); + int do_write(); + + // These functions are used to feed / extract data to + // nghttp2_session object. + int on_read(const uint8_t *data, size_t len); + int on_write(); + + // Call this function when HTTP/2 connection was established. We + // don't call this function for HTTP/1 at the moment. + int connection_made(); + + void start_settings_timer(); + void stop_settings_timer(); + + // Call this function when SETTINGS ACK was received from server. + void settings_ack_received(); + + void signal_write(); + +private: + Connection conn_; + DefaultMemchunks wb_; + std::mt19937 &gen_; + ev_timer backoff_timer_; + ev_timer settings_timer_; + std::function<int(LiveCheck &)> read_, write_; + Worker *worker_; + // nullptr if no TLS is configured + SSL_CTX *ssl_ctx_; + // Address of remote endpoint + DownstreamAddr *addr_; + nghttp2_session *session_; + // Actual remote address used to contact backend. This is initially + // nullptr, and may point to either &addr_->addr, or + // resolved_addr_.get(). + const Address *raddr_; + // Resolved IP address if dns parameter is used + std::unique_ptr<Address> resolved_addr_; + std::unique_ptr<DNSQuery> dns_query_; + // The number of successful connect attempt in a row. + size_t success_count_; + // The number of unsuccessful connect attempt in a row. + size_t fail_count_; + // true when SETTINGS ACK has been received from server. + bool settings_ack_received_; + // true when GOAWAY has been queued. + bool session_closing_; +}; + +} // namespace shrpx + +#endif // SHRPX_LIVE_CHECK_H diff --git a/src/shrpx_log.cc b/src/shrpx_log.cc new file mode 100644 index 0000000..de5e09f --- /dev/null +++ b/src/shrpx_log.cc @@ -0,0 +1,1008 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_log.h" + +#ifdef HAVE_SYSLOG_H +# include <syslog.h> +#endif // HAVE_SYSLOG_H +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#ifdef HAVE_INTTYPES_H +# include <inttypes.h> +#endif // HAVE_INTTYPES_H +#include <sys/types.h> +#include <sys/stat.h> +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#include <sys/wait.h> + +#include <cerrno> +#include <cstdio> +#include <cstring> +#include <ctime> +#include <iostream> +#include <iomanip> + +#include "shrpx_config.h" +#include "shrpx_downstream.h" +#include "shrpx_worker.h" +#include "util.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +constexpr StringRef SEVERITY_STR[] = { + StringRef::from_lit("INFO"), StringRef::from_lit("NOTICE"), + StringRef::from_lit("WARN"), StringRef::from_lit("ERROR"), + StringRef::from_lit("FATAL")}; +} // namespace + +namespace { +constexpr const char *SEVERITY_COLOR[] = { + "\033[1;32m", // INFO + "\033[1;36m", // NOTICE + "\033[1;33m", // WARN + "\033[1;31m", // ERROR + "\033[1;35m", // FATAL +}; +} // namespace + +#ifndef NOTHREADS +# ifdef HAVE_THREAD_LOCAL +namespace { +thread_local LogBuffer logbuf_; +} // namespace + +namespace { +LogBuffer *get_logbuf() { return &logbuf_; } +} // namespace +# else // !HAVE_THREAD_LOCAL +namespace { +pthread_key_t lckey; +pthread_once_t lckey_once = PTHREAD_ONCE_INIT; +} // namespace + +namespace { +void make_key() { pthread_key_create(&lckey, nullptr); } +} // namespace + +LogBuffer *get_logbuf() { + pthread_once(&lckey_once, make_key); + auto buf = static_cast<LogBuffer *>(pthread_getspecific(lckey)); + if (!buf) { + buf = new LogBuffer(); + pthread_setspecific(lckey, buf); + } + return buf; +} +# endif // !HAVE_THREAD_LOCAL +#else // NOTHREADS +namespace { +LogBuffer *get_logbuf() { + static LogBuffer logbuf; + return &logbuf; +} +} // namespace +#endif // NOTHREADS + +int Log::severity_thres_ = NOTICE; + +void Log::set_severity_level(int severity) { severity_thres_ = severity; } + +int Log::get_severity_level_by_name(const StringRef &name) { + for (size_t i = 0, max = array_size(SEVERITY_STR); i < max; ++i) { + if (name == SEVERITY_STR[i]) { + return i; + } + } + return -1; +} + +int severity_to_syslog_level(int severity) { + switch (severity) { + case (INFO): + return LOG_INFO; + case (NOTICE): + return LOG_NOTICE; + case (WARN): + return LOG_WARNING; + case (ERROR): + return LOG_ERR; + case (FATAL): + return LOG_CRIT; + default: + return -1; + } +} + +Log::Log(int severity, const char *filename, int linenum) + : buf_(*get_logbuf()), + begin_(buf_.data()), + end_(begin_ + buf_.size()), + last_(begin_), + filename_(filename), + flags_(0), + severity_(severity), + linenum_(linenum), + full_(false) {} + +Log::~Log() { + int rv; + auto config = get_config(); + + if (!config) { + return; + } + + auto lgconf = log_config(); + + auto &errorconf = config->logging.error; + + if (!log_enabled(severity_) || + (lgconf->errorlog_fd == -1 && !errorconf.syslog)) { + return; + } + + if (errorconf.syslog) { + if (severity_ == NOTICE) { + syslog(severity_to_syslog_level(severity_), "[%s] %.*s", + SEVERITY_STR[severity_].c_str(), static_cast<int>(rleft()), + begin_); + } else { + syslog(severity_to_syslog_level(severity_), "[%s] %.*s (%s:%d)", + SEVERITY_STR[severity_].c_str(), static_cast<int>(rleft()), begin_, + filename_, linenum_); + } + + return; + } + + char buf[4_k]; + auto tty = lgconf->errorlog_tty; + + lgconf->update_tstamp_millis(std::chrono::system_clock::now()); + + // Error log format: <datetime> <main-pid> <current-pid> + // <thread-id> <level> (<filename>:<line>) <msg> + rv = snprintf(buf, sizeof(buf), "%s %d %d %s %s%s%s (%s:%d) %.*s\n", + lgconf->tstamp->time_iso8601.c_str(), config->pid, lgconf->pid, + lgconf->thread_id.c_str(), tty ? SEVERITY_COLOR[severity_] : "", + SEVERITY_STR[severity_].c_str(), tty ? "\033[0m" : "", + filename_, linenum_, static_cast<int>(rleft()), begin_); + + if (rv < 0) { + return; + } + + auto nwrite = std::min(static_cast<size_t>(rv), sizeof(buf) - 1); + + while (write(lgconf->errorlog_fd, buf, nwrite) == -1 && errno == EINTR) + ; +} + +Log &Log::operator<<(const std::string &s) { + write_seq(std::begin(s), std::end(s)); + return *this; +} + +Log &Log::operator<<(const StringRef &s) { + write_seq(std::begin(s), std::end(s)); + return *this; +} + +Log &Log::operator<<(const char *s) { + write_seq(s, s + strlen(s)); + return *this; +} + +Log &Log::operator<<(const ImmutableString &s) { + write_seq(std::begin(s), std::end(s)); + return *this; +} + +Log &Log::operator<<(long long n) { + if (n >= 0) { + return *this << static_cast<uint64_t>(n); + } + + if (flags_ & fmt_hex) { + write_hex(n); + return *this; + } + + if (full_) { + return *this; + } + + n *= -1; + + size_t nlen = 0; + for (auto t = n; t; t /= 10, ++nlen) + ; + if (wleft() < 1 /* sign */ + nlen) { + full_ = true; + return *this; + } + *last_++ = '-'; + last_ += nlen; + update_full(); + + auto p = last_ - 1; + for (; n; n /= 10) { + *p-- = (n % 10) + '0'; + } + return *this; +} + +Log &Log::operator<<(unsigned long long n) { + if (flags_ & fmt_hex) { + write_hex(n); + return *this; + } + + if (full_) { + return *this; + } + + if (n == 0) { + *last_++ = '0'; + update_full(); + return *this; + } + size_t nlen = 0; + for (auto t = n; t; t /= 10, ++nlen) + ; + if (wleft() < nlen) { + full_ = true; + return *this; + } + + last_ += nlen; + update_full(); + + auto p = last_ - 1; + for (; n; n /= 10) { + *p-- = (n % 10) + '0'; + } + return *this; +} + +Log &Log::operator<<(double n) { + if (full_) { + return *this; + } + + auto left = wleft(); + auto rv = snprintf(reinterpret_cast<char *>(last_), left, "%.9f", n); + if (rv > static_cast<int>(left)) { + full_ = true; + return *this; + } + + last_ += rv; + update_full(); + + return *this; +} + +Log &Log::operator<<(long double n) { + if (full_) { + return *this; + } + + auto left = wleft(); + auto rv = snprintf(reinterpret_cast<char *>(last_), left, "%.9Lf", n); + if (rv > static_cast<int>(left)) { + full_ = true; + return *this; + } + + last_ += rv; + update_full(); + + return *this; +} + +Log &Log::operator<<(bool n) { + if (full_) { + return *this; + } + + *last_++ = n ? '1' : '0'; + update_full(); + + return *this; +} + +Log &Log::operator<<(const void *p) { + if (full_) { + return *this; + } + + write_hex(reinterpret_cast<uintptr_t>(p)); + + return *this; +} + +namespace log { +void hex(Log &log) { log.set_flags(Log::fmt_hex); }; + +void dec(Log &log) { log.set_flags(Log::fmt_dec); }; +} // namespace log + +namespace { +template <typename OutputIterator> +std::pair<OutputIterator, OutputIterator> copy(const char *src, size_t srclen, + OutputIterator d_first, + OutputIterator d_last) { + auto nwrite = + std::min(static_cast<size_t>(std::distance(d_first, d_last)), srclen); + return std::make_pair(std::copy_n(src, nwrite, d_first), d_last); +} +} // namespace + +namespace { +template <typename OutputIterator> +std::pair<OutputIterator, OutputIterator> +copy(const char *src, OutputIterator d_first, OutputIterator d_last) { + return copy(src, strlen(src), d_first, d_last); +} +} // namespace + +namespace { +template <typename OutputIterator> +std::pair<OutputIterator, OutputIterator> +copy(const StringRef &src, OutputIterator d_first, OutputIterator d_last) { + return copy(src.c_str(), src.size(), d_first, d_last); +} +} // namespace + +namespace { +template <size_t N, typename OutputIterator> +std::pair<OutputIterator, OutputIterator> +copy_l(const char (&src)[N], OutputIterator d_first, OutputIterator d_last) { + return copy(src, N - 1, d_first, d_last); +} +} // namespace + +namespace { +template <typename OutputIterator> +std::pair<OutputIterator, OutputIterator> copy(char c, OutputIterator d_first, + OutputIterator d_last) { + if (d_first == d_last) { + return std::make_pair(d_last, d_last); + } + *d_first++ = c; + return std::make_pair(d_first, d_last); +} +} // namespace + +namespace { +constexpr char LOWER_XDIGITS[] = "0123456789abcdef"; +} // namespace + +namespace { +template <typename OutputIterator> +std::pair<OutputIterator, OutputIterator> +copy_hex_low(const uint8_t *src, size_t srclen, OutputIterator d_first, + OutputIterator d_last) { + auto nwrite = std::min(static_cast<size_t>(std::distance(d_first, d_last)), + srclen * 2) / + 2; + for (size_t i = 0; i < nwrite; ++i) { + *d_first++ = LOWER_XDIGITS[src[i] >> 4]; + *d_first++ = LOWER_XDIGITS[src[i] & 0xf]; + } + return std::make_pair(d_first, d_last); +} +} // namespace + +namespace { +template <typename OutputIterator, typename T> +std::pair<OutputIterator, OutputIterator> copy(T n, OutputIterator d_first, + OutputIterator d_last) { + if (static_cast<size_t>(std::distance(d_first, d_last)) < + NGHTTP2_MAX_UINT64_DIGITS) { + return std::make_pair(d_last, d_last); + } + return std::make_pair(util::utos(d_first, n), d_last); +} +} // namespace + +namespace { +// 1 means that character must be escaped as "\xNN", where NN is ascii +// code of the character in hex notation. +constexpr uint8_t ESCAPE_TBL[] = { + 1 /* NUL */, 1 /* SOH */, 1 /* STX */, 1 /* ETX */, 1 /* EOT */, + 1 /* ENQ */, 1 /* ACK */, 1 /* BEL */, 1 /* BS */, 1 /* HT */, + 1 /* LF */, 1 /* VT */, 1 /* FF */, 1 /* CR */, 1 /* SO */, + 1 /* SI */, 1 /* DLE */, 1 /* DC1 */, 1 /* DC2 */, 1 /* DC3 */, + 1 /* DC4 */, 1 /* NAK */, 1 /* SYN */, 1 /* ETB */, 1 /* CAN */, + 1 /* EM */, 1 /* SUB */, 1 /* ESC */, 1 /* FS */, 1 /* GS */, + 1 /* RS */, 1 /* US */, 0 /* SPC */, 0 /* ! */, 1 /* " */, + 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, + 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, + 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, + 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, + 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, + 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, + 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, + 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, + 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, + 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, + 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, + 0 /* Z */, 0 /* [ */, 1 /* \ */, 0 /* ] */, 0 /* ^ */, + 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, + 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, + 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, + 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, + 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, + 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, + 0 /* } */, 0 /* ~ */, 1 /* DEL */, 1 /* 0x80 */, 1 /* 0x81 */, + 1 /* 0x82 */, 1 /* 0x83 */, 1 /* 0x84 */, 1 /* 0x85 */, 1 /* 0x86 */, + 1 /* 0x87 */, 1 /* 0x88 */, 1 /* 0x89 */, 1 /* 0x8a */, 1 /* 0x8b */, + 1 /* 0x8c */, 1 /* 0x8d */, 1 /* 0x8e */, 1 /* 0x8f */, 1 /* 0x90 */, + 1 /* 0x91 */, 1 /* 0x92 */, 1 /* 0x93 */, 1 /* 0x94 */, 1 /* 0x95 */, + 1 /* 0x96 */, 1 /* 0x97 */, 1 /* 0x98 */, 1 /* 0x99 */, 1 /* 0x9a */, + 1 /* 0x9b */, 1 /* 0x9c */, 1 /* 0x9d */, 1 /* 0x9e */, 1 /* 0x9f */, + 1 /* 0xa0 */, 1 /* 0xa1 */, 1 /* 0xa2 */, 1 /* 0xa3 */, 1 /* 0xa4 */, + 1 /* 0xa5 */, 1 /* 0xa6 */, 1 /* 0xa7 */, 1 /* 0xa8 */, 1 /* 0xa9 */, + 1 /* 0xaa */, 1 /* 0xab */, 1 /* 0xac */, 1 /* 0xad */, 1 /* 0xae */, + 1 /* 0xaf */, 1 /* 0xb0 */, 1 /* 0xb1 */, 1 /* 0xb2 */, 1 /* 0xb3 */, + 1 /* 0xb4 */, 1 /* 0xb5 */, 1 /* 0xb6 */, 1 /* 0xb7 */, 1 /* 0xb8 */, + 1 /* 0xb9 */, 1 /* 0xba */, 1 /* 0xbb */, 1 /* 0xbc */, 1 /* 0xbd */, + 1 /* 0xbe */, 1 /* 0xbf */, 1 /* 0xc0 */, 1 /* 0xc1 */, 1 /* 0xc2 */, + 1 /* 0xc3 */, 1 /* 0xc4 */, 1 /* 0xc5 */, 1 /* 0xc6 */, 1 /* 0xc7 */, + 1 /* 0xc8 */, 1 /* 0xc9 */, 1 /* 0xca */, 1 /* 0xcb */, 1 /* 0xcc */, + 1 /* 0xcd */, 1 /* 0xce */, 1 /* 0xcf */, 1 /* 0xd0 */, 1 /* 0xd1 */, + 1 /* 0xd2 */, 1 /* 0xd3 */, 1 /* 0xd4 */, 1 /* 0xd5 */, 1 /* 0xd6 */, + 1 /* 0xd7 */, 1 /* 0xd8 */, 1 /* 0xd9 */, 1 /* 0xda */, 1 /* 0xdb */, + 1 /* 0xdc */, 1 /* 0xdd */, 1 /* 0xde */, 1 /* 0xdf */, 1 /* 0xe0 */, + 1 /* 0xe1 */, 1 /* 0xe2 */, 1 /* 0xe3 */, 1 /* 0xe4 */, 1 /* 0xe5 */, + 1 /* 0xe6 */, 1 /* 0xe7 */, 1 /* 0xe8 */, 1 /* 0xe9 */, 1 /* 0xea */, + 1 /* 0xeb */, 1 /* 0xec */, 1 /* 0xed */, 1 /* 0xee */, 1 /* 0xef */, + 1 /* 0xf0 */, 1 /* 0xf1 */, 1 /* 0xf2 */, 1 /* 0xf3 */, 1 /* 0xf4 */, + 1 /* 0xf5 */, 1 /* 0xf6 */, 1 /* 0xf7 */, 1 /* 0xf8 */, 1 /* 0xf9 */, + 1 /* 0xfa */, 1 /* 0xfb */, 1 /* 0xfc */, 1 /* 0xfd */, 1 /* 0xfe */, + 1 /* 0xff */, +}; +} // namespace + +namespace { +template <typename OutputIterator> +std::pair<OutputIterator, OutputIterator> +copy_escape(const char *src, size_t srclen, OutputIterator d_first, + OutputIterator d_last) { + auto safe_first = src; + for (auto p = src; p != src + srclen && d_first != d_last; ++p) { + unsigned char c = *p; + if (!ESCAPE_TBL[c]) { + continue; + } + + auto n = + std::min(std::distance(d_first, d_last), std::distance(safe_first, p)); + d_first = std::copy_n(safe_first, n, d_first); + if (std::distance(d_first, d_last) < 4) { + return std::make_pair(d_first, d_last); + } + *d_first++ = '\\'; + *d_first++ = 'x'; + *d_first++ = LOWER_XDIGITS[c >> 4]; + *d_first++ = LOWER_XDIGITS[c & 0xf]; + safe_first = p + 1; + } + + auto n = std::min(std::distance(d_first, d_last), + std::distance(safe_first, src + srclen)); + return std::make_pair(std::copy_n(safe_first, n, d_first), d_last); +} +} // namespace + +namespace { +template <typename OutputIterator> +std::pair<OutputIterator, OutputIterator> copy_escape(const StringRef &src, + OutputIterator d_first, + OutputIterator d_last) { + return copy_escape(src.c_str(), src.size(), d_first, d_last); +} +} // namespace + +namespace { +// Construct absolute request URI from |Request|, mainly to log +// request URI for proxy request (HTTP/2 proxy or client proxy). This +// is mostly same routine found in +// HttpDownstreamConnection::push_request_headers(), but vastly +// simplified since we only care about absolute URI. +StringRef construct_absolute_request_uri(BlockAllocator &balloc, + const Request &req) { + if (req.authority.empty()) { + return req.path; + } + + auto len = req.authority.size() + req.path.size(); + if (req.scheme.empty()) { + len += str_size("http://"); + } else { + len += req.scheme.size() + str_size("://"); + } + + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + + if (req.scheme.empty()) { + // We may have to log the request which lacks scheme (e.g., + // http/1.1 with origin form). + p = util::copy_lit(p, "http://"); + } else { + p = std::copy(std::begin(req.scheme), std::end(req.scheme), p); + p = util::copy_lit(p, "://"); + } + p = std::copy(std::begin(req.authority), std::end(req.authority), p); + p = std::copy(std::begin(req.path), std::end(req.path), p); + *p = '\0'; + + return StringRef{iov.base, p}; +} +} // namespace + +void upstream_accesslog(const std::vector<LogFragment> &lfv, + const LogSpec &lgsp) { + auto config = get_config(); + auto lgconf = log_config(); + auto &accessconf = get_config()->logging.access; + + if (lgconf->accesslog_fd == -1 && !accessconf.syslog) { + return; + } + + std::array<char, 4_k> buf; + + auto downstream = lgsp.downstream; + + const auto &req = downstream->request(); + const auto &resp = downstream->response(); + const auto &tstamp = req.tstamp; + auto &balloc = downstream->get_block_allocator(); + + auto downstream_addr = downstream->get_addr(); + auto method = req.method == -1 ? StringRef::from_lit("<unknown>") + : http2::to_method_string(req.method); + auto path = + req.method == HTTP_CONNECT ? req.authority + : config->http2_proxy ? construct_absolute_request_uri(balloc, req) + : req.path.empty() ? req.method == HTTP_OPTIONS ? StringRef::from_lit("*") + : StringRef::from_lit("-") + : req.path; + auto path_without_query = + req.method == HTTP_CONNECT + ? path + : StringRef{std::begin(path), + std::find(std::begin(path), std::end(path), '?')}; + + auto p = std::begin(buf); + auto last = std::end(buf) - 2; + + for (auto &lf : lfv) { + switch (lf.type) { + case LogFragmentType::LITERAL: + std::tie(p, last) = copy(lf.value, p, last); + break; + case LogFragmentType::REMOTE_ADDR: + std::tie(p, last) = copy(lgsp.remote_addr, p, last); + break; + case LogFragmentType::TIME_LOCAL: + std::tie(p, last) = copy(tstamp->time_local, p, last); + break; + case LogFragmentType::TIME_ISO8601: + std::tie(p, last) = copy(tstamp->time_iso8601, p, last); + break; + case LogFragmentType::REQUEST: + std::tie(p, last) = copy(method, p, last); + std::tie(p, last) = copy(' ', p, last); + std::tie(p, last) = copy_escape(path, p, last); + std::tie(p, last) = copy_l(" HTTP/", p, last); + std::tie(p, last) = copy(req.http_major, p, last); + if (req.http_major < 2) { + std::tie(p, last) = copy('.', p, last); + std::tie(p, last) = copy(req.http_minor, p, last); + } + break; + case LogFragmentType::METHOD: + std::tie(p, last) = copy(method, p, last); + break; + case LogFragmentType::PATH: + std::tie(p, last) = copy_escape(path, p, last); + break; + case LogFragmentType::PATH_WITHOUT_QUERY: + std::tie(p, last) = copy_escape(path_without_query, p, last); + break; + case LogFragmentType::PROTOCOL_VERSION: + std::tie(p, last) = copy_l("HTTP/", p, last); + std::tie(p, last) = copy(req.http_major, p, last); + if (req.http_major < 2) { + std::tie(p, last) = copy('.', p, last); + std::tie(p, last) = copy(req.http_minor, p, last); + } + break; + case LogFragmentType::STATUS: + std::tie(p, last) = copy(resp.http_status, p, last); + break; + case LogFragmentType::BODY_BYTES_SENT: + std::tie(p, last) = copy(downstream->response_sent_body_length, p, last); + break; + case LogFragmentType::HTTP: { + auto hd = req.fs.header(lf.value); + if (hd) { + std::tie(p, last) = copy_escape((*hd).value, p, last); + break; + } + + std::tie(p, last) = copy('-', p, last); + + break; + } + case LogFragmentType::AUTHORITY: + if (!req.authority.empty()) { + std::tie(p, last) = copy(req.authority, p, last); + break; + } + + std::tie(p, last) = copy('-', p, last); + + break; + case LogFragmentType::REMOTE_PORT: + std::tie(p, last) = copy(lgsp.remote_port, p, last); + break; + case LogFragmentType::SERVER_PORT: + std::tie(p, last) = copy(lgsp.server_port, p, last); + break; + case LogFragmentType::REQUEST_TIME: { + auto t = std::chrono::duration_cast<std::chrono::milliseconds>( + lgsp.request_end_time - downstream->get_request_start_time()) + .count(); + std::tie(p, last) = copy(t / 1000, p, last); + std::tie(p, last) = copy('.', p, last); + auto frac = t % 1000; + if (frac < 100) { + auto n = frac < 10 ? 2 : 1; + std::tie(p, last) = copy("000", n, p, last); + } + std::tie(p, last) = copy(frac, p, last); + break; + } + case LogFragmentType::PID: + std::tie(p, last) = copy(lgsp.pid, p, last); + break; + case LogFragmentType::ALPN: + std::tie(p, last) = copy_escape(lgsp.alpn, p, last); + break; + case LogFragmentType::TLS_CIPHER: + if (!lgsp.ssl) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy(SSL_get_cipher_name(lgsp.ssl), p, last); + break; + case LogFragmentType::TLS_PROTOCOL: + if (!lgsp.ssl) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = + copy(nghttp2::tls::get_tls_protocol(lgsp.ssl), p, last); + break; + case LogFragmentType::TLS_SESSION_ID: { + auto session = SSL_get_session(lgsp.ssl); + if (!session) { + std::tie(p, last) = copy('-', p, last); + break; + } + unsigned int session_id_length = 0; + auto session_id = SSL_SESSION_get_id(session, &session_id_length); + if (session_id_length == 0) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy_hex_low(session_id, session_id_length, p, last); + break; + } + case LogFragmentType::TLS_SESSION_REUSED: + if (!lgsp.ssl) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = + copy(SSL_session_reused(lgsp.ssl) ? 'r' : '.', p, last); + break; + case LogFragmentType::TLS_SNI: + if (lgsp.sni.empty()) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy_escape(lgsp.sni, p, last); + break; + case LogFragmentType::TLS_CLIENT_FINGERPRINT_SHA1: + case LogFragmentType::TLS_CLIENT_FINGERPRINT_SHA256: { + if (!lgsp.ssl) { + std::tie(p, last) = copy('-', p, last); + break; + } +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(lgsp.ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(lgsp.ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::array<uint8_t, 32> buf; + auto len = tls::get_x509_fingerprint( + buf.data(), buf.size(), x, + lf.type == LogFragmentType::TLS_CLIENT_FINGERPRINT_SHA256 + ? EVP_sha256() + : EVP_sha1()); +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + if (len <= 0) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy_hex_low(buf.data(), len, p, last); + break; + } + case LogFragmentType::TLS_CLIENT_ISSUER_NAME: + case LogFragmentType::TLS_CLIENT_SUBJECT_NAME: { + if (!lgsp.ssl) { + std::tie(p, last) = copy('-', p, last); + break; + } +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(lgsp.ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(lgsp.ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + std::tie(p, last) = copy('-', p, last); + break; + } + auto name = lf.type == LogFragmentType::TLS_CLIENT_ISSUER_NAME + ? tls::get_x509_issuer_name(balloc, x) + : tls::get_x509_subject_name(balloc, x); +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + if (name.empty()) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy(name, p, last); + break; + } + case LogFragmentType::TLS_CLIENT_SERIAL: { + if (!lgsp.ssl) { + std::tie(p, last) = copy('-', p, last); + break; + } +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(lgsp.ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(lgsp.ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + std::tie(p, last) = copy('-', p, last); + break; + } + auto sn = tls::get_x509_serial(balloc, x); +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + if (sn.empty()) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy(sn, p, last); + break; + } + case LogFragmentType::BACKEND_HOST: + if (!downstream_addr) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy(downstream_addr->host, p, last); + break; + case LogFragmentType::BACKEND_PORT: + if (!downstream_addr) { + std::tie(p, last) = copy('-', p, last); + break; + } + std::tie(p, last) = copy(downstream_addr->port, p, last); + break; + case LogFragmentType::NONE: + break; + default: + break; + } + } + + *p = '\0'; + + if (accessconf.syslog) { + syslog(LOG_INFO, "%s", buf.data()); + + return; + } + + *p++ = '\n'; + + auto nwrite = std::distance(std::begin(buf), p); + while (write(lgconf->accesslog_fd, buf.data(), nwrite) == -1 && + errno == EINTR) + ; +} + +int reopen_log_files(const LoggingConfig &loggingconf) { + int res = 0; + int new_accesslog_fd = -1; + int new_errorlog_fd = -1; + + auto lgconf = log_config(); + auto &accessconf = loggingconf.access; + auto &errorconf = loggingconf.error; + + if (!accessconf.syslog && !accessconf.file.empty()) { + new_accesslog_fd = open_log_file(accessconf.file.c_str()); + + if (new_accesslog_fd == -1) { + LOG(ERROR) << "Failed to open accesslog file " << accessconf.file; + res = -1; + } + } + + if (!errorconf.syslog && !errorconf.file.empty()) { + new_errorlog_fd = open_log_file(errorconf.file.c_str()); + + if (new_errorlog_fd == -1) { + if (lgconf->errorlog_fd != -1) { + LOG(ERROR) << "Failed to open errorlog file " << errorconf.file; + } else { + std::cerr << "Failed to open errorlog file " << errorconf.file + << std::endl; + } + + res = -1; + } + } + + close_log_file(lgconf->accesslog_fd); + close_log_file(lgconf->errorlog_fd); + + lgconf->accesslog_fd = new_accesslog_fd; + lgconf->errorlog_fd = new_errorlog_fd; + lgconf->errorlog_tty = + (new_errorlog_fd == -1) ? false : isatty(new_errorlog_fd); + + return res; +} + +void log_chld(pid_t pid, int rstatus, const char *msg) { + std::string signalstr; + if (WIFSIGNALED(rstatus)) { + signalstr += "; signal "; + auto sig = WTERMSIG(rstatus); + auto s = strsignal(sig); + if (s) { + signalstr += s; + signalstr += '('; + } else { + signalstr += "UNKNOWN("; + } + signalstr += util::utos(sig); + signalstr += ')'; + } + + LOG(NOTICE) << msg << ": [" << pid << "] exited " + << (WIFEXITED(rstatus) ? "normally" : "abnormally") + << " with status " << log::hex << rstatus << log::dec + << "; exit status " + << (WIFEXITED(rstatus) ? WEXITSTATUS(rstatus) : 0) + << (signalstr.empty() ? "" : signalstr.c_str()); +} + +void redirect_stderr_to_errorlog(const LoggingConfig &loggingconf) { + auto lgconf = log_config(); + auto &errorconf = loggingconf.error; + + if (errorconf.syslog || lgconf->errorlog_fd == -1) { + return; + } + + dup2(lgconf->errorlog_fd, STDERR_FILENO); +} + +namespace { +int STDERR_COPY = -1; +int STDOUT_COPY = -1; +} // namespace + +void store_original_fds() { + // consider dup'ing stdout too + STDERR_COPY = dup(STDERR_FILENO); + STDOUT_COPY = STDOUT_FILENO; + // no race here, since it is called early + util::make_socket_closeonexec(STDERR_COPY); +} + +void restore_original_fds() { dup2(STDERR_COPY, STDERR_FILENO); } + +void close_log_file(int &fd) { + if (fd != STDERR_COPY && fd != STDOUT_COPY && fd != -1) { + close(fd); + } + fd = -1; +} + +int open_log_file(const char *path) { + + if (strcmp(path, "/dev/stdout") == 0 || + strcmp(path, "/proc/self/fd/1") == 0) { + return STDOUT_COPY; + } + + if (strcmp(path, "/dev/stderr") == 0 || + strcmp(path, "/proc/self/fd/2") == 0) { + return STDERR_COPY; + } +#ifdef O_CLOEXEC + + auto fd = open(path, O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC, + S_IRUSR | S_IWUSR | S_IRGRP); +#else // !O_CLOEXEC + + auto fd = + open(path, O_WRONLY | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP); + + // We get race condition if execve is called at the same time. + if (fd != -1) { + util::make_socket_closeonexec(fd); + } + +#endif // !O_CLOEXEC + + if (fd == -1) { + return -1; + } + + return fd; +} + +} // namespace shrpx diff --git a/src/shrpx_log.h b/src/shrpx_log.h new file mode 100644 index 0000000..bc30097 --- /dev/null +++ b/src/shrpx_log.h @@ -0,0 +1,318 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_LOG_H +#define SHRPX_LOG_H + +#include "shrpx.h" + +#include <sys/types.h> + +#include <memory> +#include <vector> +#include <chrono> + +#include "shrpx_config.h" +#include "shrpx_log_config.h" +#include "tls.h" +#include "template.h" +#include "util.h" + +using namespace nghttp2; + +#define ENABLE_LOG 1 + +#define LOG_ENABLED(SEVERITY) (ENABLE_LOG && shrpx::Log::log_enabled(SEVERITY)) + +#ifdef __FILE_NAME__ +# define NGHTTP2_FILE_NAME __FILE_NAME__ +#else // !__FILE_NAME__ +# define NGHTTP2_FILE_NAME __FILE__ +#endif // !__FILE_NAME__ + +#define LOG(SEVERITY) shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) + +// Listener log +#define LLOG(SEVERITY, LISTEN) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[LISTEN:" << LISTEN << "] ") + +// Worker log +#define WLOG(SEVERITY, WORKER) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[WORKER:" << WORKER << "] ") + +// ClientHandler log +#define CLOG(SEVERITY, CLIENT_HANDLER) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[CLIENT_HANDLER:" << CLIENT_HANDLER << "] ") + +// Upstream log +#define ULOG(SEVERITY, UPSTREAM) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[UPSTREAM:" << UPSTREAM << "] ") + +// Downstream log +#define DLOG(SEVERITY, DOWNSTREAM) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[DOWNSTREAM:" << DOWNSTREAM << "] ") + +// Downstream connection log +#define DCLOG(SEVERITY, DCONN) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[DCONN:" << DCONN << "] ") + +// Downstream HTTP2 session log +#define SSLOG(SEVERITY, HTTP2) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[DHTTP2:" << HTTP2 << "] ") + +// Memcached connection log +#define MCLOG(SEVERITY, MCONN) \ + (shrpx::Log(SEVERITY, NGHTTP2_FILE_NAME, __LINE__) \ + << "[MCONN:" << MCONN << "] ") + +namespace shrpx { + +class Downstream; +struct DownstreamAddr; + +enum SeverityLevel { INFO, NOTICE, WARN, ERROR, FATAL }; + +using LogBuffer = std::array<uint8_t, 4_k>; + +class Log { +public: + Log(int severity, const char *filename, int linenum); + ~Log(); + Log &operator<<(const std::string &s); + Log &operator<<(const char *s); + Log &operator<<(const StringRef &s); + Log &operator<<(const ImmutableString &s); + Log &operator<<(short n) { return *this << static_cast<long long>(n); } + Log &operator<<(int n) { return *this << static_cast<long long>(n); } + Log &operator<<(long n) { return *this << static_cast<long long>(n); } + Log &operator<<(long long n); + Log &operator<<(unsigned short n) { + return *this << static_cast<unsigned long long>(n); + } + Log &operator<<(unsigned int n) { + return *this << static_cast<unsigned long long>(n); + } + Log &operator<<(unsigned long n) { + return *this << static_cast<unsigned long long>(n); + } + Log &operator<<(unsigned long long n); + Log &operator<<(float n) { return *this << static_cast<double>(n); } + Log &operator<<(double n); + Log &operator<<(long double n); + Log &operator<<(bool n); + Log &operator<<(const void *p); + template <typename T> Log &operator<<(const std::shared_ptr<T> &ptr) { + return *this << ptr.get(); + } + Log &operator<<(void (*func)(Log &log)) { + func(*this); + return *this; + } + template <typename InputIt> void write_seq(InputIt first, InputIt last) { + if (full_) { + return; + } + + auto d = std::distance(first, last); + auto n = std::min(wleft(), static_cast<size_t>(d)); + last_ = std::copy(first, first + n, last_); + update_full(); + } + + template <typename T> void write_hex(T n) { + if (full_) { + return; + } + + if (n == 0) { + if (wleft() < 4 /* for "0x00" */) { + full_ = true; + return; + } + *last_++ = '0'; + *last_++ = 'x'; + *last_++ = '0'; + *last_++ = '0'; + update_full(); + return; + } + + size_t nlen = 0; + for (auto t = n; t; t >>= 8, ++nlen) + ; + + nlen *= 2; + + if (wleft() < 2 /* for "0x" */ + nlen) { + full_ = true; + return; + } + + *last_++ = '0'; + *last_++ = 'x'; + + last_ += nlen; + update_full(); + + auto p = last_ - 1; + for (; n; n >>= 8) { + uint8_t b = n & 0xff; + *p-- = util::LOWER_XDIGITS[b & 0xf]; + *p-- = util::LOWER_XDIGITS[b >> 4]; + } + } + static void set_severity_level(int severity); + // Returns the severity level by |name|. Returns -1 if |name| is + // unknown. + static int get_severity_level_by_name(const StringRef &name); + static bool log_enabled(int severity) { return severity >= severity_thres_; } + + enum { + fmt_dec = 0x00, + fmt_hex = 0x01, + }; + + void set_flags(int flags) { flags_ = flags; } + +private: + size_t rleft() { return last_ - begin_; } + size_t wleft() { return end_ - last_; } + void update_full() { full_ = last_ == end_; } + + LogBuffer &buf_; + uint8_t *begin_; + uint8_t *end_; + uint8_t *last_; + const char *filename_; + uint32_t flags_; + int severity_; + int linenum_; + bool full_; + static int severity_thres_; +}; + +namespace log { +void hex(Log &log); +void dec(Log &log); +} // namespace log + +#define TTY_HTTP_HD (log_config()->errorlog_tty ? "\033[1;34m" : "") +#define TTY_RST (log_config()->errorlog_tty ? "\033[0m" : "") + +enum class LogFragmentType { + NONE, + LITERAL, + REMOTE_ADDR, + TIME_LOCAL, + TIME_ISO8601, + REQUEST, + STATUS, + BODY_BYTES_SENT, + HTTP, + AUTHORITY, + REMOTE_PORT, + SERVER_PORT, + REQUEST_TIME, + PID, + ALPN, + TLS_CIPHER, + SSL_CIPHER = TLS_CIPHER, + TLS_PROTOCOL, + SSL_PROTOCOL = TLS_PROTOCOL, + TLS_SESSION_ID, + SSL_SESSION_ID = TLS_SESSION_ID, + TLS_SESSION_REUSED, + SSL_SESSION_REUSED = TLS_SESSION_REUSED, + TLS_SNI, + TLS_CLIENT_FINGERPRINT_SHA1, + TLS_CLIENT_FINGERPRINT_SHA256, + TLS_CLIENT_ISSUER_NAME, + TLS_CLIENT_SERIAL, + TLS_CLIENT_SUBJECT_NAME, + BACKEND_HOST, + BACKEND_PORT, + METHOD, + PATH, + PATH_WITHOUT_QUERY, + PROTOCOL_VERSION, +}; + +struct LogFragment { + LogFragment(LogFragmentType type, StringRef value = StringRef::from_lit("")) + : type(type), value(std::move(value)) {} + LogFragmentType type; + StringRef value; +}; + +struct LogSpec { + Downstream *downstream; + StringRef remote_addr; + StringRef alpn; + StringRef sni; + SSL *ssl; + std::chrono::high_resolution_clock::time_point request_end_time; + StringRef remote_port; + uint16_t server_port; + pid_t pid; +}; + +void upstream_accesslog(const std::vector<LogFragment> &lf, + const LogSpec &lgsp); + +int reopen_log_files(const LoggingConfig &loggingconf); + +// Logs message when process whose pid is |pid| and exist status is +// |rstatus| exited. The |msg| is prepended to the log message. +void log_chld(pid_t pid, int rstatus, const char *msg); + +void redirect_stderr_to_errorlog(const LoggingConfig &loggingconf); + +// Makes internal copy of stderr (and possibly stdout in the future), +// which is then used as pointer to /dev/stderr or /proc/self/fd/2 +void store_original_fds(); + +// Restores the original stderr that was stored with copy_original_fds +// Used just before execv +void restore_original_fds(); + +// Closes |fd| which was returned by open_log_file (see below) +// and sets it to -1. In the case that |fd| points to stdout or +// stderr, or is -1, the descriptor is not closed (but still set to -1). +void close_log_file(int &fd); + +// Opens |path| with O_APPEND enabled. If file does not exist, it is +// created first. This function returns file descriptor referring the +// opened file if it succeeds, or -1. +int open_log_file(const char *path); + +} // namespace shrpx + +#endif // SHRPX_LOG_H diff --git a/src/shrpx_log_config.cc b/src/shrpx_log_config.cc new file mode 100644 index 0000000..92eb055 --- /dev/null +++ b/src/shrpx_log_config.cc @@ -0,0 +1,127 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_log_config.h" + +#include <unistd.h> + +#include <thread> +#include <sstream> + +#include "util.h" + +using namespace nghttp2; + +namespace shrpx { + +Timestamp::Timestamp(const std::chrono::system_clock::time_point &tp) { + time_local = util::format_common_log(time_local_buf.data(), tp); + time_iso8601 = util::format_iso8601(time_iso8601_buf.data(), tp); + time_http = util::format_http_date(time_http_buf.data(), tp); +} + +LogConfig::LogConfig() + : time_str_updated(std::chrono::system_clock::now()), + tstamp(std::make_shared<Timestamp>(time_str_updated)), + pid(getpid()), + accesslog_fd(-1), + errorlog_fd(-1), + errorlog_tty(false) { + auto tid = std::this_thread::get_id(); + auto tid_hash = + util::hash32(StringRef{reinterpret_cast<uint8_t *>(&tid), + reinterpret_cast<uint8_t *>(&tid) + sizeof(tid)}); + thread_id = util::format_hex(reinterpret_cast<uint8_t *>(&tid_hash), + sizeof(tid_hash)); +} + +#ifndef NOTHREADS +# ifdef HAVE_THREAD_LOCAL +namespace { +thread_local std::unique_ptr<LogConfig> config = std::make_unique<LogConfig>(); +} // namespace + +LogConfig *log_config() { return config.get(); } +void delete_log_config() {} +# else // !HAVE_THREAD_LOCAL +namespace { +pthread_key_t lckey; +pthread_once_t lckey_once = PTHREAD_ONCE_INIT; +} // namespace + +namespace { +void make_key() { pthread_key_create(&lckey, nullptr); } +} // namespace + +LogConfig *log_config() { + pthread_once(&lckey_once, make_key); + LogConfig *config = (LogConfig *)pthread_getspecific(lckey); + if (!config) { + config = new LogConfig(); + pthread_setspecific(lckey, config); + } + return config; +} + +void delete_log_config() { delete log_config(); } +# endif // !HAVE_THREAD_LOCAL +#else // NOTHREADS +namespace { +std::unique_ptr<LogConfig> config = std::make_unique<LogConfig>(); +} // namespace + +LogConfig *log_config() { return config.get(); } + +void delete_log_config() {} +#endif // NOTHREADS + +void LogConfig::update_tstamp_millis( + const std::chrono::system_clock::time_point &now) { + if (std::chrono::duration_cast<std::chrono::milliseconds>( + now.time_since_epoch()) == + std::chrono::duration_cast<std::chrono::milliseconds>( + time_str_updated.time_since_epoch())) { + return; + } + + time_str_updated = now; + + tstamp = std::make_shared<Timestamp>(now); +} + +void LogConfig::update_tstamp( + const std::chrono::system_clock::time_point &now) { + if (std::chrono::duration_cast<std::chrono::seconds>( + now.time_since_epoch()) == + std::chrono::duration_cast<std::chrono::seconds>( + time_str_updated.time_since_epoch())) { + return; + } + + time_str_updated = now; + + tstamp = std::make_shared<Timestamp>(now); +} + +} // namespace shrpx diff --git a/src/shrpx_log_config.h b/src/shrpx_log_config.h new file mode 100644 index 0000000..76fa78b --- /dev/null +++ b/src/shrpx_log_config.h @@ -0,0 +1,79 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_LOG_CONFIG_H +#define SHRPX_LOG_CONFIG_H + +#include "shrpx.h" + +#include <sys/types.h> + +#include <chrono> + +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +struct Timestamp { + Timestamp(const std::chrono::system_clock::time_point &tp); + + std::array<char, sizeof("03/Jul/2014:00:19:38 +0900")> time_local_buf; + std::array<char, sizeof("2014-11-15T12:58:24.741+09:00")> time_iso8601_buf; + std::array<char, sizeof("Mon, 10 Oct 2016 10:25:58 GMT")> time_http_buf; + StringRef time_local; + StringRef time_iso8601; + StringRef time_http; +}; + +struct LogConfig { + std::chrono::system_clock::time_point time_str_updated; + std::shared_ptr<Timestamp> tstamp; + std::string thread_id; + pid_t pid; + int accesslog_fd; + int errorlog_fd; + // true if errorlog_fd is referring to a terminal. + bool errorlog_tty; + + LogConfig(); + // Updates time stamp if difference between time_str_updated and now + // is 1 or more milliseconds. + void update_tstamp_millis(const std::chrono::system_clock::time_point &now); + // Updates time stamp if difference between time_str_updated and + // now, converted to time_t, is 1 or more seconds. + void update_tstamp(const std::chrono::system_clock::time_point &now); +}; + +// We need LogConfig per thread to avoid data race around opening file +// descriptor for log files. +LogConfig *log_config(); + +// Deletes log_config +void delete_log_config(); + +} // namespace shrpx + +#endif // SHRPX_LOG_CONFIG_H diff --git a/src/shrpx_memcached_connection.cc b/src/shrpx_memcached_connection.cc new file mode 100644 index 0000000..f72cb11 --- /dev/null +++ b/src/shrpx_memcached_connection.cc @@ -0,0 +1,777 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_memcached_connection.h" + +#include <limits.h> +#include <sys/uio.h> + +#include <cerrno> + +#include "shrpx_memcached_request.h" +#include "shrpx_memcached_result.h" +#include "shrpx_config.h" +#include "shrpx_tls.h" +#include "shrpx_log.h" +#include "util.h" + +namespace shrpx { + +namespace { +void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto mconn = static_cast<MemcachedConnection *>(conn->data); + + if (w == &conn->rt && !conn->expired_rt()) { + return; + } + + if (LOG_ENABLED(INFO)) { + MCLOG(INFO, mconn) << "Time out"; + } + + mconn->disconnect(); +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto mconn = static_cast<MemcachedConnection *>(conn->data); + + if (mconn->on_read() != 0) { + mconn->reconnect_or_fail(); + return; + } +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto mconn = static_cast<MemcachedConnection *>(conn->data); + + if (mconn->on_write() != 0) { + mconn->reconnect_or_fail(); + return; + } +} +} // namespace + +namespace { +void connectcb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn = static_cast<Connection *>(w->data); + auto mconn = static_cast<MemcachedConnection *>(conn->data); + + if (mconn->connected() != 0) { + mconn->disconnect(); + return; + } + + writecb(loop, w, revents); +} +} // namespace + +constexpr auto write_timeout = 10_s; +constexpr auto read_timeout = 10_s; + +MemcachedConnection::MemcachedConnection(const Address *addr, + struct ev_loop *loop, SSL_CTX *ssl_ctx, + const StringRef &sni_name, + MemchunkPool *mcpool, + std::mt19937 &gen) + : conn_(loop, -1, nullptr, mcpool, write_timeout, read_timeout, {}, {}, + connectcb, readcb, timeoutcb, this, 0, 0., Proto::MEMCACHED), + do_read_(&MemcachedConnection::noop), + do_write_(&MemcachedConnection::noop), + sni_name_(sni_name), + connect_blocker_( + gen, loop, [] {}, [] {}), + parse_state_{}, + addr_(addr), + ssl_ctx_(ssl_ctx), + sendsum_(0), + try_count_(0), + connected_(false) {} + +MemcachedConnection::~MemcachedConnection() { conn_.disconnect(); } + +namespace { +void clear_request(std::deque<std::unique_ptr<MemcachedRequest>> &q) { + for (auto &req : q) { + if (req->cb) { + req->cb(req.get(), + MemcachedResult(MemcachedStatusCode::EXT_NETWORK_ERROR)); + } + } + q.clear(); +} +} // namespace + +void MemcachedConnection::disconnect() { + clear_request(recvq_); + clear_request(sendq_); + + sendbufv_.clear(); + sendsum_ = 0; + + parse_state_ = {}; + + connected_ = false; + + conn_.disconnect(); + + assert(recvbuf_.rleft() == 0); + recvbuf_.reset(); + + do_read_ = do_write_ = &MemcachedConnection::noop; +} + +int MemcachedConnection::initiate_connection() { + assert(conn_.fd == -1); + + if (ssl_ctx_) { + auto ssl = tls::create_ssl(ssl_ctx_); + if (!ssl) { + return -1; + } + conn_.set_ssl(ssl); + conn_.tls.client_session_cache = &tls_session_cache_; + } + + conn_.fd = util::create_nonblock_socket(addr_->su.storage.ss_family); + + if (conn_.fd == -1) { + auto error = errno; + MCLOG(WARN, this) << "socket() failed; errno=" << error; + + return -1; + } + + int rv; + rv = connect(conn_.fd, &addr_->su.sa, addr_->len); + if (rv != 0 && errno != EINPROGRESS) { + auto error = errno; + MCLOG(WARN, this) << "connect() failed; errno=" << error; + + close(conn_.fd); + conn_.fd = -1; + + return -1; + } + + if (ssl_ctx_) { + if (!util::numeric_host(sni_name_.c_str())) { + SSL_set_tlsext_host_name(conn_.tls.ssl, sni_name_.c_str()); + } + + auto session = tls::reuse_tls_session(tls_session_cache_); + if (session) { + SSL_set_session(conn_.tls.ssl, session); + SSL_SESSION_free(session); + } + + conn_.prepare_client_handshake(); + } + + if (LOG_ENABLED(INFO)) { + MCLOG(INFO, this) << "Connecting to memcached server"; + } + + ev_io_set(&conn_.wev, conn_.fd, EV_WRITE); + ev_io_set(&conn_.rev, conn_.fd, EV_READ); + + ev_set_cb(&conn_.wev, connectcb); + + conn_.wlimit.startw(); + ev_timer_again(conn_.loop, &conn_.wt); + + return 0; +} + +int MemcachedConnection::connected() { + auto sock_error = util::get_socket_error(conn_.fd); + if (sock_error != 0) { + MCLOG(WARN, this) << "memcached connect failed; addr=" + << util::to_numeric_addr(addr_) + << ": errno=" << sock_error; + + connect_blocker_.on_failure(); + + conn_.wlimit.stopw(); + + return -1; + } + + if (LOG_ENABLED(INFO)) { + MCLOG(INFO, this) << "connected to memcached server"; + } + + conn_.rlimit.startw(); + + ev_set_cb(&conn_.wev, writecb); + + if (conn_.tls.ssl) { + conn_.again_rt(); + + do_read_ = &MemcachedConnection::tls_handshake; + do_write_ = &MemcachedConnection::tls_handshake; + + return 0; + } + + ev_timer_stop(conn_.loop, &conn_.wt); + + connected_ = true; + + connect_blocker_.on_success(); + + do_read_ = &MemcachedConnection::read_clear; + do_write_ = &MemcachedConnection::write_clear; + + return 0; +} + +int MemcachedConnection::on_write() { return do_write_(*this); } +int MemcachedConnection::on_read() { return do_read_(*this); } + +int MemcachedConnection::tls_handshake() { + ERR_clear_error(); + + conn_.last_read = std::chrono::steady_clock::now(); + + auto rv = conn_.tls_handshake(); + if (rv == SHRPX_ERR_INPROGRESS) { + return 0; + } + + if (rv < 0) { + connect_blocker_.on_failure(); + return rv; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "SSL/TLS handshake completed"; + } + + auto &tlsconf = get_config()->tls; + + if (!tlsconf.insecure && + tls::check_cert(conn_.tls.ssl, addr_, sni_name_) != 0) { + connect_blocker_.on_failure(); + return -1; + } + + ev_timer_stop(conn_.loop, &conn_.rt); + ev_timer_stop(conn_.loop, &conn_.wt); + + connected_ = true; + + connect_blocker_.on_success(); + + do_read_ = &MemcachedConnection::read_tls; + do_write_ = &MemcachedConnection::write_tls; + + return on_write(); +} + +int MemcachedConnection::write_tls() { + if (!connected_) { + return 0; + } + + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<struct iovec, MAX_WR_IOVCNT> iov; + std::array<uint8_t, 16_k> buf; + + for (; !sendq_.empty();) { + auto iovcnt = fill_request_buffer(iov.data(), iov.size()); + auto p = std::begin(buf); + for (size_t i = 0; i < iovcnt; ++i) { + auto &v = iov[i]; + auto n = std::min(static_cast<size_t>(std::end(buf) - p), v.iov_len); + p = std::copy_n(static_cast<uint8_t *>(v.iov_base), n, p); + if (p == std::end(buf)) { + break; + } + } + + auto nwrite = conn_.write_tls(buf.data(), p - std::begin(buf)); + if (nwrite < 0) { + return -1; + } + if (nwrite == 0) { + return 0; + } + + drain_send_queue(nwrite); + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + return 0; +} + +int MemcachedConnection::read_tls() { + if (!connected_) { + return 0; + } + + conn_.last_read = std::chrono::steady_clock::now(); + + for (;;) { + auto nread = conn_.read_tls(recvbuf_.last, recvbuf_.wleft()); + + if (nread == 0) { + return 0; + } + + if (nread < 0) { + return -1; + } + + recvbuf_.write(nread); + + if (parse_packet() != 0) { + return -1; + } + } + + return 0; +} + +int MemcachedConnection::write_clear() { + if (!connected_) { + return 0; + } + + conn_.last_read = std::chrono::steady_clock::now(); + + std::array<struct iovec, MAX_WR_IOVCNT> iov; + + for (; !sendq_.empty();) { + auto iovcnt = fill_request_buffer(iov.data(), iov.size()); + auto nwrite = conn_.writev_clear(iov.data(), iovcnt); + if (nwrite < 0) { + return -1; + } + if (nwrite == 0) { + return 0; + } + + drain_send_queue(nwrite); + } + + conn_.wlimit.stopw(); + ev_timer_stop(conn_.loop, &conn_.wt); + + return 0; +} + +int MemcachedConnection::read_clear() { + if (!connected_) { + return 0; + } + + conn_.last_read = std::chrono::steady_clock::now(); + + for (;;) { + auto nread = conn_.read_clear(recvbuf_.last, recvbuf_.wleft()); + + if (nread == 0) { + return 0; + } + + if (nread < 0) { + return -1; + } + + recvbuf_.write(nread); + + if (parse_packet() != 0) { + return -1; + } + } + + return 0; +} + +int MemcachedConnection::parse_packet() { + auto in = recvbuf_.pos; + + for (;;) { + auto busy = false; + + switch (parse_state_.state) { + case MemcachedParseState::HEADER24: { + if (recvbuf_.last - in < 24) { + recvbuf_.drain_reset(in - recvbuf_.pos); + return 0; + } + + if (recvq_.empty()) { + MCLOG(WARN, this) + << "Response received, but there is no in-flight request."; + return -1; + } + + auto &req = recvq_.front(); + + if (*in != MEMCACHED_RES_MAGIC) { + MCLOG(WARN, this) << "Response has bad magic: " + << static_cast<uint32_t>(*in); + return -1; + } + ++in; + + parse_state_.op = static_cast<MemcachedOp>(*in++); + parse_state_.keylen = util::get_uint16(in); + in += 2; + parse_state_.extralen = *in++; + // skip 1 byte reserved data type + ++in; + parse_state_.status_code = + static_cast<MemcachedStatusCode>(util::get_uint16(in)); + in += 2; + parse_state_.totalbody = util::get_uint32(in); + in += 4; + // skip 4 bytes opaque + in += 4; + parse_state_.cas = util::get_uint64(in); + in += 8; + + if (req->op != parse_state_.op) { + MCLOG(WARN, this) + << "opcode in response does not match to the request: want " + << static_cast<uint32_t>(req->op) << ", got " + << static_cast<uint32_t>(parse_state_.op); + return -1; + } + + if (parse_state_.keylen != 0) { + MCLOG(WARN, this) << "zero length keylen expected: got " + << parse_state_.keylen; + return -1; + } + + if (parse_state_.totalbody > 16_k) { + MCLOG(WARN, this) << "totalbody is too large: got " + << parse_state_.totalbody; + return -1; + } + + if (parse_state_.op == MemcachedOp::GET && + parse_state_.status_code == MemcachedStatusCode::NO_ERROR && + parse_state_.extralen == 0) { + MCLOG(WARN, this) << "response for GET does not have extra"; + return -1; + } + + if (parse_state_.totalbody < + parse_state_.keylen + parse_state_.extralen) { + MCLOG(WARN, this) << "totalbody is too short: totalbody " + << parse_state_.totalbody << ", want min " + << parse_state_.keylen + parse_state_.extralen; + return -1; + } + + if (parse_state_.extralen) { + parse_state_.state = MemcachedParseState::EXTRA; + parse_state_.read_left = parse_state_.extralen; + } else { + parse_state_.state = MemcachedParseState::VALUE; + parse_state_.read_left = parse_state_.totalbody - parse_state_.keylen - + parse_state_.extralen; + } + busy = true; + break; + } + case MemcachedParseState::EXTRA: { + // We don't use extra for now. Just read and forget. + auto n = std::min(static_cast<size_t>(recvbuf_.last - in), + parse_state_.read_left); + + parse_state_.read_left -= n; + in += n; + if (parse_state_.read_left) { + recvbuf_.reset(); + return 0; + } + parse_state_.state = MemcachedParseState::VALUE; + // since we require keylen == 0, totalbody - extralen == + // valuelen + parse_state_.read_left = + parse_state_.totalbody - parse_state_.keylen - parse_state_.extralen; + busy = true; + break; + } + case MemcachedParseState::VALUE: { + auto n = std::min(static_cast<size_t>(recvbuf_.last - in), + parse_state_.read_left); + + parse_state_.value.insert(std::end(parse_state_.value), in, in + n); + + parse_state_.read_left -= n; + in += n; + if (parse_state_.read_left) { + recvbuf_.reset(); + return 0; + } + + if (LOG_ENABLED(INFO)) { + if (parse_state_.status_code != MemcachedStatusCode::NO_ERROR) { + MCLOG(INFO, this) << "response returned error status: " + << static_cast<uint16_t>(parse_state_.status_code); + } + } + + // We require at least one complete response to clear try count. + try_count_ = 0; + + auto req = std::move(recvq_.front()); + recvq_.pop_front(); + + if (sendq_.empty() && recvq_.empty()) { + ev_timer_stop(conn_.loop, &conn_.rt); + } + + if (!req->canceled && req->cb) { + req->cb(req.get(), MemcachedResult(parse_state_.status_code, + std::move(parse_state_.value))); + } + + parse_state_ = {}; + break; + } + } + + if (!busy && in == recvbuf_.last) { + break; + } + } + + assert(in == recvbuf_.last); + recvbuf_.reset(); + + return 0; +} + +#undef DEFAULT_WR_IOVCNT +#define DEFAULT_WR_IOVCNT 128 + +#if defined(IOV_MAX) && IOV_MAX < DEFAULT_WR_IOVCNT +# define MAX_WR_IOVCNT IOV_MAX +#else // !defined(IOV_MAX) || IOV_MAX >= DEFAULT_WR_IOVCNT +# define MAX_WR_IOVCNT DEFAULT_WR_IOVCNT +#endif // !defined(IOV_MAX) || IOV_MAX >= DEFAULT_WR_IOVCNT + +size_t MemcachedConnection::fill_request_buffer(struct iovec *iov, + size_t iovlen) { + if (sendsum_ == 0) { + for (auto &req : sendq_) { + if (req->canceled) { + continue; + } + if (serialized_size(req.get()) + sendsum_ > 1300) { + break; + } + sendbufv_.emplace_back(); + sendbufv_.back().req = req.get(); + make_request(&sendbufv_.back(), req.get()); + sendsum_ += sendbufv_.back().left(); + } + + if (sendsum_ == 0) { + sendq_.clear(); + return 0; + } + } + + size_t iovcnt = 0; + for (auto &buf : sendbufv_) { + if (iovcnt + 2 > iovlen) { + break; + } + + auto req = buf.req; + if (buf.headbuf.rleft()) { + iov[iovcnt++] = {buf.headbuf.pos, buf.headbuf.rleft()}; + } + if (buf.send_value_left) { + iov[iovcnt++] = {req->value.data() + req->value.size() - + buf.send_value_left, + buf.send_value_left}; + } + } + + return iovcnt; +} + +void MemcachedConnection::drain_send_queue(size_t nwrite) { + sendsum_ -= nwrite; + + while (nwrite > 0) { + auto &buf = sendbufv_.front(); + auto &req = sendq_.front(); + if (req->canceled) { + sendq_.pop_front(); + continue; + } + assert(buf.req == req.get()); + auto n = std::min(static_cast<size_t>(nwrite), buf.headbuf.rleft()); + buf.headbuf.drain(n); + nwrite -= n; + n = std::min(static_cast<size_t>(nwrite), buf.send_value_left); + buf.send_value_left -= n; + nwrite -= n; + + if (buf.headbuf.rleft() || buf.send_value_left) { + break; + } + sendbufv_.pop_front(); + recvq_.push_back(std::move(sendq_.front())); + sendq_.pop_front(); + } + + // start read timer only when we wait for responses. + if (recvq_.empty()) { + ev_timer_stop(conn_.loop, &conn_.rt); + } else if (!ev_is_active(&conn_.rt)) { + conn_.again_rt(); + } +} + +size_t MemcachedConnection::serialized_size(MemcachedRequest *req) { + switch (req->op) { + case MemcachedOp::GET: + return 24 + req->key.size(); + case MemcachedOp::ADD: + default: + return 24 + 8 + req->key.size() + req->value.size(); + } +} + +void MemcachedConnection::make_request(MemcachedSendbuf *sendbuf, + MemcachedRequest *req) { + auto &headbuf = sendbuf->headbuf; + + std::fill(std::begin(headbuf.buf), std::end(headbuf.buf), 0); + + headbuf[0] = MEMCACHED_REQ_MAGIC; + headbuf[1] = static_cast<uint8_t>(req->op); + switch (req->op) { + case MemcachedOp::GET: + util::put_uint16be(&headbuf[2], req->key.size()); + util::put_uint32be(&headbuf[8], req->key.size()); + headbuf.write(24); + break; + case MemcachedOp::ADD: + util::put_uint16be(&headbuf[2], req->key.size()); + headbuf[4] = 8; + util::put_uint32be(&headbuf[8], 8 + req->key.size() + req->value.size()); + util::put_uint32be(&headbuf[28], req->expiry); + headbuf.write(32); + break; + } + + headbuf.write(req->key.c_str(), req->key.size()); + + sendbuf->send_value_left = req->value.size(); +} + +int MemcachedConnection::add_request(std::unique_ptr<MemcachedRequest> req) { + if (connect_blocker_.blocked()) { + return -1; + } + + sendq_.push_back(std::move(req)); + + if (connected_) { + signal_write(); + return 0; + } + + if (conn_.fd == -1 && initiate_connection() != 0) { + connect_blocker_.on_failure(); + disconnect(); + return -1; + } + + return 0; +} + +// TODO should we start write timer too? +void MemcachedConnection::signal_write() { conn_.wlimit.startw(); } + +int MemcachedConnection::noop() { return 0; } + +void MemcachedConnection::reconnect_or_fail() { + if (!connected_ || (recvq_.empty() && sendq_.empty())) { + disconnect(); + return; + } + + constexpr size_t MAX_TRY_COUNT = 3; + + if (++try_count_ >= MAX_TRY_COUNT) { + if (LOG_ENABLED(INFO)) { + MCLOG(INFO, this) << "Tried " << MAX_TRY_COUNT + << " times, and all failed. Aborting"; + } + try_count_ = 0; + disconnect(); + return; + } + + std::vector<std::unique_ptr<MemcachedRequest>> q; + q.reserve(recvq_.size() + sendq_.size()); + + if (LOG_ENABLED(INFO)) { + MCLOG(INFO, this) << "Retry connection, enqueue " + << recvq_.size() + sendq_.size() << " request(s) again"; + } + + q.insert(std::end(q), std::make_move_iterator(std::begin(recvq_)), + std::make_move_iterator(std::end(recvq_))); + q.insert(std::end(q), std::make_move_iterator(std::begin(sendq_)), + std::make_move_iterator(std::end(sendq_))); + + recvq_.clear(); + sendq_.clear(); + + disconnect(); + + sendq_.insert(std::end(sendq_), std::make_move_iterator(std::begin(q)), + std::make_move_iterator(std::end(q))); + + if (initiate_connection() != 0) { + connect_blocker_.on_failure(); + disconnect(); + return; + } +} + +} // namespace shrpx diff --git a/src/shrpx_memcached_connection.h b/src/shrpx_memcached_connection.h new file mode 100644 index 0000000..2e097e1 --- /dev/null +++ b/src/shrpx_memcached_connection.h @@ -0,0 +1,155 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MEMCACHED_CONNECTION_H +#define SHRPX_MEMCACHED_CONNECTION_H + +#include "shrpx.h" + +#include <memory> +#include <deque> + +#include <ev.h> + +#include "shrpx_connection.h" +#include "shrpx_tls.h" +#include "shrpx_connect_blocker.h" +#include "buffer.h" +#include "network.h" + +using namespace nghttp2; + +namespace shrpx { + +struct MemcachedRequest; +enum class MemcachedOp : uint8_t; +enum class MemcachedStatusCode : uint16_t; + +enum class MemcachedParseState { + HEADER24, + EXTRA, + VALUE, +}; + +// Stores state when parsing response from memcached server +struct MemcachedParseContext { + // Buffer for value, dynamically allocated. + std::vector<uint8_t> value; + // cas in response + uint64_t cas; + // keylen in response + size_t keylen; + // extralen in response + size_t extralen; + // totalbody in response. The length of value is totalbody - + // extralen - keylen. + size_t totalbody; + // Number of bytes left to read variable length field. + size_t read_left; + // Parser state; see enum above + MemcachedParseState state; + // status_code in response + MemcachedStatusCode status_code; + // op in response + MemcachedOp op; +}; + +struct MemcachedSendbuf { + // Buffer for header + extra + key + Buffer<512> headbuf; + // MemcachedRequest associated to this object + MemcachedRequest *req; + // Number of bytes left when sending value + size_t send_value_left; + // Returns the number of bytes this object transmits. + size_t left() const { return headbuf.rleft() + send_value_left; } +}; + +constexpr uint8_t MEMCACHED_REQ_MAGIC = 0x80; +constexpr uint8_t MEMCACHED_RES_MAGIC = 0x81; + +// MemcachedConnection implements part of memcached binary protocol. +// This is not full brown implementation. Just the part we need is +// implemented. We only use GET and ADD. +// +// https://github.com/memcached/memcached/blob/master/doc/protocol-binary.xml +// https://code.google.com/p/memcached/wiki/MemcacheBinaryProtocol +class MemcachedConnection { +public: + MemcachedConnection(const Address *addr, struct ev_loop *loop, + SSL_CTX *ssl_ctx, const StringRef &sni_name, + MemchunkPool *mcpool, std::mt19937 &gen); + ~MemcachedConnection(); + + void disconnect(); + + int add_request(std::unique_ptr<MemcachedRequest> req); + int initiate_connection(); + + int connected(); + int on_write(); + int on_read(); + + int write_clear(); + int read_clear(); + + int tls_handshake(); + int write_tls(); + int read_tls(); + + size_t fill_request_buffer(struct iovec *iov, size_t iovlen); + void drain_send_queue(size_t nwrite); + + void make_request(MemcachedSendbuf *sendbuf, MemcachedRequest *req); + int parse_packet(); + size_t serialized_size(MemcachedRequest *req); + + void signal_write(); + + int noop(); + + void reconnect_or_fail(); + +private: + Connection conn_; + std::deque<std::unique_ptr<MemcachedRequest>> recvq_; + std::deque<std::unique_ptr<MemcachedRequest>> sendq_; + std::deque<MemcachedSendbuf> sendbufv_; + std::function<int(MemcachedConnection &)> do_read_, do_write_; + StringRef sni_name_; + tls::TLSSessionCache tls_session_cache_; + ConnectBlocker connect_blocker_; + MemcachedParseContext parse_state_; + const Address *addr_; + SSL_CTX *ssl_ctx_; + // Sum of the bytes to be transmitted in sendbufv_. + size_t sendsum_; + size_t try_count_; + bool connected_; + Buffer<8_k> recvbuf_; +}; + +} // namespace shrpx + +#endif // SHRPX_MEMCACHED_CONNECTION_H diff --git a/src/shrpx_memcached_dispatcher.cc b/src/shrpx_memcached_dispatcher.cc new file mode 100644 index 0000000..024bd5a --- /dev/null +++ b/src/shrpx_memcached_dispatcher.cc @@ -0,0 +1,53 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_memcached_dispatcher.h" + +#include "shrpx_memcached_request.h" +#include "shrpx_memcached_connection.h" +#include "shrpx_config.h" +#include "shrpx_log.h" + +namespace shrpx { + +MemcachedDispatcher::MemcachedDispatcher(const Address *addr, + struct ev_loop *loop, SSL_CTX *ssl_ctx, + const StringRef &sni_name, + MemchunkPool *mcpool, + std::mt19937 &gen) + : loop_(loop), + mconn_(std::make_unique<MemcachedConnection>(addr, loop_, ssl_ctx, + sni_name, mcpool, gen)) {} + +MemcachedDispatcher::~MemcachedDispatcher() {} + +int MemcachedDispatcher::add_request(std::unique_ptr<MemcachedRequest> req) { + if (mconn_->add_request(std::move(req)) != 0) { + return -1; + } + + return 0; +} + +} // namespace shrpx diff --git a/src/shrpx_memcached_dispatcher.h b/src/shrpx_memcached_dispatcher.h new file mode 100644 index 0000000..de1af80 --- /dev/null +++ b/src/shrpx_memcached_dispatcher.h @@ -0,0 +1,63 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MEMCACHED_DISPATCHER_H +#define SHRPX_MEMCACHED_DISPATCHER_H + +#include "shrpx.h" + +#include <memory> +#include <random> + +#include <ev.h> + +#include <openssl/ssl.h> + +#include "memchunk.h" +#include "network.h" + +using namespace nghttp2; + +namespace shrpx { + +struct MemcachedRequest; +class MemcachedConnection; + +class MemcachedDispatcher { +public: + MemcachedDispatcher(const Address *addr, struct ev_loop *loop, + SSL_CTX *ssl_ctx, const StringRef &sni_name, + MemchunkPool *mcpool, std::mt19937 &gen); + ~MemcachedDispatcher(); + + int add_request(std::unique_ptr<MemcachedRequest> req); + +private: + struct ev_loop *loop_; + std::unique_ptr<MemcachedConnection> mconn_; +}; + +} // namespace shrpx + +#endif // SHRPX_MEMCACHED_DISPATCHER_H diff --git a/src/shrpx_memcached_request.h b/src/shrpx_memcached_request.h new file mode 100644 index 0000000..3696983 --- /dev/null +++ b/src/shrpx_memcached_request.h @@ -0,0 +1,59 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MEMCACHED_REQUEST_H +#define SHRPX_MEMCACHED_REQUEST_H + +#include "shrpx.h" + +#include <string> +#include <vector> +#include <memory> + +#include "shrpx_memcached_result.h" + +namespace shrpx { + +enum class MemcachedOp : uint8_t { + GET = 0x00, + ADD = 0x02, +}; + +struct MemcachedRequest; + +using MemcachedResultCallback = + std::function<void(MemcachedRequest *req, MemcachedResult res)>; + +struct MemcachedRequest { + std::string key; + std::vector<uint8_t> value; + MemcachedResultCallback cb; + uint32_t expiry; + MemcachedOp op; + bool canceled; +}; + +} // namespace shrpx + +#endif // SHRPX_MEMCACHED_REQUEST_H diff --git a/src/shrpx_memcached_result.h b/src/shrpx_memcached_result.h new file mode 100644 index 0000000..60a87af --- /dev/null +++ b/src/shrpx_memcached_result.h @@ -0,0 +1,50 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MEMCACHED_RESULT_H +#define SHRPX_MEMCACHED_RESULT_H + +#include "shrpx.h" + +#include <vector> + +namespace shrpx { + +enum class MemcachedStatusCode : uint16_t { + NO_ERROR, + EXT_NETWORK_ERROR = 0x1001, +}; + +struct MemcachedResult { + MemcachedResult(MemcachedStatusCode status_code) : status_code(status_code) {} + MemcachedResult(MemcachedStatusCode status_code, std::vector<uint8_t> value) + : value(std::move(value)), status_code(status_code) {} + + std::vector<uint8_t> value; + MemcachedStatusCode status_code; +}; + +} // namespace shrpx + +#endif // SHRPX_MEMCACHED_RESULT_H diff --git a/src/shrpx_mruby.cc b/src/shrpx_mruby.cc new file mode 100644 index 0000000..b5c6ed3 --- /dev/null +++ b/src/shrpx_mruby.cc @@ -0,0 +1,238 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_mruby.h" + +#include <mruby/compile.h> +#include <mruby/string.h> + +#include "shrpx_downstream.h" +#include "shrpx_config.h" +#include "shrpx_mruby_module.h" +#include "shrpx_downstream_connection.h" +#include "shrpx_log.h" + +namespace shrpx { + +namespace mruby { + +MRubyContext::MRubyContext(mrb_state *mrb, mrb_value app, mrb_value env) + : mrb_(mrb), app_(std::move(app)), env_(std::move(env)) {} + +MRubyContext::~MRubyContext() { + if (mrb_) { + mrb_close(mrb_); + } +} + +int MRubyContext::run_app(Downstream *downstream, int phase) { + if (!mrb_) { + return 0; + } + + MRubyAssocData data{downstream, phase}; + + mrb_->ud = &data; + + int rv = 0; + auto ai = mrb_gc_arena_save(mrb_); + auto ai_d = defer([ai, this]() { mrb_gc_arena_restore(mrb_, ai); }); + + const char *method; + switch (phase) { + case PHASE_REQUEST: + if (!mrb_respond_to(mrb_, app_, mrb_intern_lit(mrb_, "on_req"))) { + return 0; + } + method = "on_req"; + break; + case PHASE_RESPONSE: + if (!mrb_respond_to(mrb_, app_, mrb_intern_lit(mrb_, "on_resp"))) { + return 0; + } + method = "on_resp"; + break; + default: + assert(0); + abort(); + } + + auto res = mrb_funcall(mrb_, app_, method, 1, env_); + (void)res; + + if (mrb_->exc) { + // If response has been committed, ignore error + if (downstream->get_response_state() != DownstreamState::MSG_COMPLETE) { + rv = -1; + } + + auto exc = mrb_obj_value(mrb_->exc); + auto inspect = mrb_inspect(mrb_, exc); + + LOG(ERROR) << "Exception caught while executing mruby code: " + << mrb_str_to_cstr(mrb_, inspect); + } + + mrb_->ud = nullptr; + + return rv; +} + +int MRubyContext::run_on_request_proc(Downstream *downstream) { + return run_app(downstream, PHASE_REQUEST); +} + +int MRubyContext::run_on_response_proc(Downstream *downstream) { + return run_app(downstream, PHASE_RESPONSE); +} + +void MRubyContext::delete_downstream(Downstream *downstream) { + if (!mrb_) { + return; + } + delete_downstream_from_module(mrb_, downstream); +} + +namespace { +mrb_value instantiate_app(mrb_state *mrb, RProc *proc) { + mrb->ud = nullptr; + + auto res = mrb_top_run(mrb, proc, mrb_top_self(mrb), 0); + + if (mrb->exc) { + auto exc = mrb_obj_value(mrb->exc); + auto inspect = mrb_inspect(mrb, exc); + + LOG(ERROR) << "Exception caught while executing mruby code: " + << mrb_str_to_cstr(mrb, inspect); + + return mrb_nil_value(); + } + + return res; +} +} // namespace + +// Based on +// https://github.com/h2o/h2o/blob/master/lib/handler/mruby.c. It is +// very hard to write these kind of code because mruby has almost no +// documentation about compiling or generating code, at least at the +// time of this writing. +RProc *compile(mrb_state *mrb, const StringRef &filename) { + if (filename.empty()) { + return nullptr; + } + + auto infile = fopen(filename.c_str(), "rb"); + if (infile == nullptr) { + LOG(ERROR) << "Could not open mruby file " << filename; + return nullptr; + } + auto infile_d = defer(fclose, infile); + + auto mrbc = mrbc_context_new(mrb); + if (mrbc == nullptr) { + LOG(ERROR) << "mrb_context_new failed"; + return nullptr; + } + auto mrbc_d = defer(mrbc_context_free, mrb, mrbc); + + auto parser = mrb_parse_file(mrb, infile, nullptr); + if (parser == nullptr) { + LOG(ERROR) << "mrb_parse_nstring failed"; + return nullptr; + } + auto parser_d = defer(mrb_parser_free, parser); + + if (parser->nerr != 0) { + LOG(ERROR) << "mruby parser detected parse error"; + return nullptr; + } + + auto proc = mrb_generate_code(mrb, parser); + if (proc == nullptr) { + LOG(ERROR) << "mrb_generate_code failed"; + return nullptr; + } + + return proc; +} + +std::unique_ptr<MRubyContext> create_mruby_context(const StringRef &filename) { + if (filename.empty()) { + return std::make_unique<MRubyContext>(nullptr, mrb_nil_value(), + mrb_nil_value()); + } + + auto mrb = mrb_open(); + if (mrb == nullptr) { + LOG(ERROR) << "mrb_open failed"; + return nullptr; + } + + auto ai = mrb_gc_arena_save(mrb); + + auto req_proc = compile(mrb, filename); + + if (!req_proc) { + mrb_gc_arena_restore(mrb, ai); + LOG(ERROR) << "Could not compile mruby code " << filename; + mrb_close(mrb); + return nullptr; + } + + auto env = init_module(mrb); + + auto app = instantiate_app(mrb, req_proc); + if (mrb_nil_p(app)) { + mrb_gc_arena_restore(mrb, ai); + LOG(ERROR) << "Could not instantiate mruby app from " << filename; + mrb_close(mrb); + return nullptr; + } + + mrb_gc_arena_restore(mrb, ai); + + // TODO These are not necessary, because we retain app and env? + mrb_gc_protect(mrb, env); + mrb_gc_protect(mrb, app); + + return std::make_unique<MRubyContext>(mrb, std::move(app), std::move(env)); +} + +mrb_sym intern_ptr(mrb_state *mrb, void *ptr) { + auto p = reinterpret_cast<uintptr_t>(ptr); + + return mrb_intern(mrb, reinterpret_cast<const char *>(&p), sizeof(p)); +} + +void check_phase(mrb_state *mrb, int phase, int phase_mask) { + if ((phase & phase_mask) == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "operation was not allowed in this phase"); + } +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby.h b/src/shrpx_mruby.h new file mode 100644 index 0000000..518063d --- /dev/null +++ b/src/shrpx_mruby.h @@ -0,0 +1,89 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MRUBY_H +#define SHRPX_MRUBY_H + +#include "shrpx.h" + +#include <memory> + +#include <mruby.h> +#include <mruby/proc.h> + +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +class Downstream; + +namespace mruby { + +class MRubyContext { +public: + MRubyContext(mrb_state *mrb, mrb_value app, mrb_value env); + ~MRubyContext(); + + int run_on_request_proc(Downstream *downstream); + int run_on_response_proc(Downstream *downstream); + + int run_app(Downstream *downstream, int phase); + + void delete_downstream(Downstream *downstream); + +private: + mrb_state *mrb_; + mrb_value app_; + mrb_value env_; +}; + +enum { + PHASE_NONE = 0, + PHASE_REQUEST = 1, + PHASE_RESPONSE = 1 << 1, +}; + +struct MRubyAssocData { + Downstream *downstream; + int phase; +}; + +RProc *compile(mrb_state *mrb, const StringRef &filename); + +std::unique_ptr<MRubyContext> create_mruby_context(const StringRef &filename); + +// Return interned |ptr|. +mrb_sym intern_ptr(mrb_state *mrb, void *ptr); + +// Checks that |phase| is set in |phase_mask|. If not set, raise +// exception. +void check_phase(mrb_state *mrb, int phase, int phase_mask); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_H diff --git a/src/shrpx_mruby_module.cc b/src/shrpx_mruby_module.cc new file mode 100644 index 0000000..27b7769 --- /dev/null +++ b/src/shrpx_mruby_module.cc @@ -0,0 +1,113 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_mruby_module.h" + +#include <array> + +#include <mruby/variable.h> +#include <mruby/string.h> +#include <mruby/hash.h> +#include <mruby/array.h> + +#include "shrpx_mruby.h" +#include "shrpx_mruby_module_env.h" +#include "shrpx_mruby_module_request.h" +#include "shrpx_mruby_module_response.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value create_env(mrb_state *mrb) { + auto module = mrb_module_get(mrb, "Nghttpx"); + + auto env_class = mrb_class_get_under(mrb, module, "Env"); + auto request_class = mrb_class_get_under(mrb, module, "Request"); + auto response_class = mrb_class_get_under(mrb, module, "Response"); + + auto env = mrb_obj_new(mrb, env_class, 0, nullptr); + auto req = mrb_obj_new(mrb, request_class, 0, nullptr); + auto resp = mrb_obj_new(mrb, response_class, 0, nullptr); + + mrb_iv_set(mrb, env, mrb_intern_lit(mrb, "req"), req); + mrb_iv_set(mrb, env, mrb_intern_lit(mrb, "resp"), resp); + + return env; +} +} // namespace + +void delete_downstream_from_module(mrb_state *mrb, Downstream *downstream) { + auto module = mrb_module_get(mrb, "Nghttpx"); + auto env = mrb_obj_iv_get(mrb, reinterpret_cast<RObject *>(module), + mrb_intern_lit(mrb, "env")); + if (mrb_nil_p(env)) { + return; + } + + mrb_iv_remove(mrb, env, intern_ptr(mrb, downstream)); +} + +mrb_value init_module(mrb_state *mrb) { + auto module = mrb_define_module(mrb, "Nghttpx"); + + mrb_define_const(mrb, module, "REQUEST_PHASE", + mrb_fixnum_value(PHASE_REQUEST)); + mrb_define_const(mrb, module, "RESPONSE_PHASE", + mrb_fixnum_value(PHASE_RESPONSE)); + + init_env_class(mrb, module); + init_request_class(mrb, module); + init_response_class(mrb, module); + + return create_env(mrb); +} + +mrb_value create_headers_hash(mrb_state *mrb, const HeaderRefs &headers) { + auto hash = mrb_hash_new(mrb); + + for (auto &hd : headers) { + if (hd.name.empty() || hd.name[0] == ':') { + continue; + } + auto ai = mrb_gc_arena_save(mrb); + + auto key = mrb_str_new(mrb, hd.name.c_str(), hd.name.size()); + auto ary = mrb_hash_get(mrb, hash, key); + if (mrb_nil_p(ary)) { + ary = mrb_ary_new(mrb); + mrb_hash_set(mrb, hash, key, ary); + } + mrb_ary_push(mrb, ary, mrb_str_new(mrb, hd.value.c_str(), hd.value.size())); + + mrb_gc_arena_restore(mrb, ai); + } + + return hash; +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module.h b/src/shrpx_mruby_module.h new file mode 100644 index 0000000..a426bea --- /dev/null +++ b/src/shrpx_mruby_module.h @@ -0,0 +1,52 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MRUBY_MODULE_H +#define SHRPX_MRUBY_MODULE_H + +#include "shrpx.h" + +#include <mruby.h> + +#include "http2.h" + +using namespace nghttp2; + +namespace shrpx { + +class Downstream; + +namespace mruby { + +mrb_value init_module(mrb_state *mrb); + +void delete_downstream_from_module(mrb_state *mrb, Downstream *downstream); + +mrb_value create_headers_hash(mrb_state *mrb, const HeaderRefs &headers); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_H diff --git a/src/shrpx_mruby_module_env.cc b/src/shrpx_mruby_module_env.cc new file mode 100644 index 0000000..5ebd9c0 --- /dev/null +++ b/src/shrpx_mruby_module_env.cc @@ -0,0 +1,500 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_mruby_module_env.h" + +#include <mruby/variable.h> +#include <mruby/string.h> +#include <mruby/hash.h> + +#include "shrpx_downstream.h" +#include "shrpx_upstream.h" +#include "shrpx_client_handler.h" +#include "shrpx_mruby.h" +#include "shrpx_mruby_module.h" +#include "shrpx_log.h" +#include "shrpx_tls.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value env_init(mrb_state *mrb, mrb_value self) { return self; } +} // namespace + +namespace { +mrb_value env_get_req(mrb_state *mrb, mrb_value self) { + return mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "req")); +} +} // namespace + +namespace { +mrb_value env_get_resp(mrb_state *mrb, mrb_value self) { + return mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "resp")); +} +} // namespace + +namespace { +mrb_value env_get_ctx(mrb_state *mrb, mrb_value self) { + auto data = reinterpret_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + + auto dsym = intern_ptr(mrb, downstream); + + auto ctx = mrb_iv_get(mrb, self, dsym); + if (mrb_nil_p(ctx)) { + ctx = mrb_hash_new(mrb); + mrb_iv_set(mrb, self, dsym, ctx); + } + + return ctx; +} +} // namespace + +namespace { +mrb_value env_get_phase(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + + return mrb_fixnum_value(data->phase); +} +} // namespace + +namespace { +mrb_value env_get_remote_addr(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + + auto &ipaddr = handler->get_ipaddr(); + + return mrb_str_new(mrb, ipaddr.c_str(), ipaddr.size()); +} +} // namespace + +namespace { +mrb_value env_get_server_port(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto faddr = handler->get_upstream_addr(); + + return mrb_fixnum_value(faddr->port); +} +} // namespace + +namespace { +mrb_value env_get_server_addr(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto faddr = handler->get_upstream_addr(); + + return mrb_str_new(mrb, faddr->host.c_str(), faddr->host.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_used(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + + return handler->get_ssl() ? mrb_true_value() : mrb_false_value(); +} +} // namespace + +namespace { +mrb_value env_get_tls_sni(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto sni = handler->get_tls_sni(); + + return mrb_str_new(mrb, sni.c_str(), sni.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_fingerprint_md(mrb_state *mrb, const EVP_MD *md) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_str_new_static(mrb, "", 0); + } + +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + return mrb_str_new_static(mrb, "", 0); + } + + // Currently the largest hash value is SHA-256, which is 32 bytes. + std::array<uint8_t, 32> buf; + auto slen = tls::get_x509_fingerprint(buf.data(), buf.size(), x, md); +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + if (slen == -1) { + mrb_raise(mrb, E_RUNTIME_ERROR, "could not compute client fingerprint"); + } + + // TODO Use template version of format_hex + auto &balloc = downstream->get_block_allocator(); + auto f = util::format_hex(balloc, + StringRef{std::begin(buf), std::begin(buf) + slen}); + return mrb_str_new(mrb, f.c_str(), f.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_fingerprint_sha256(mrb_state *mrb, + mrb_value self) { + return env_get_tls_client_fingerprint_md(mrb, EVP_sha256()); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_fingerprint_sha1(mrb_state *mrb, mrb_value self) { + return env_get_tls_client_fingerprint_md(mrb, EVP_sha1()); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_subject_name(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_str_new_static(mrb, "", 0); + } + +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + return mrb_str_new_static(mrb, "", 0); + } + + auto &balloc = downstream->get_block_allocator(); + auto name = tls::get_x509_subject_name(balloc, x); +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + return mrb_str_new(mrb, name.c_str(), name.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_issuer_name(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_str_new_static(mrb, "", 0); + } + +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + return mrb_str_new_static(mrb, "", 0); + } + + auto &balloc = downstream->get_block_allocator(); + auto name = tls::get_x509_issuer_name(balloc, x); +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + return mrb_str_new(mrb, name.c_str(), name.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_serial(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_str_new_static(mrb, "", 0); + } + +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + return mrb_str_new_static(mrb, "", 0); + } + + auto &balloc = downstream->get_block_allocator(); + auto sn = tls::get_x509_serial(balloc, x); +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + return mrb_str_new(mrb, sn.c_str(), sn.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_not_before(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_fixnum_value(0); + } + +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + return mrb_fixnum_value(0); + } + + time_t t; + if (tls::get_x509_not_before(t, x) != 0) { + t = 0; + } + +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + + return mrb_fixnum_value(t); +} +} // namespace + +namespace { +mrb_value env_get_tls_client_not_after(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_fixnum_value(0); + } + +#if OPENSSL_3_0_0_API + auto x = SSL_get0_peer_certificate(ssl); +#else // !OPENSSL_3_0_0_API + auto x = SSL_get_peer_certificate(ssl); +#endif // !OPENSSL_3_0_0_API + if (!x) { + return mrb_fixnum_value(0); + } + + time_t t; + if (tls::get_x509_not_after(t, x) != 0) { + t = 0; + } + +#if !OPENSSL_3_0_0_API + X509_free(x); +#endif // !OPENSSL_3_0_0_API + + return mrb_fixnum_value(t); +} +} // namespace + +namespace { +mrb_value env_get_tls_cipher(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_str_new_static(mrb, "", 0); + } + + return mrb_str_new_cstr(mrb, SSL_get_cipher_name(ssl)); +} +} // namespace + +namespace { +mrb_value env_get_tls_protocol(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_str_new_static(mrb, "", 0); + } + + return mrb_str_new_cstr(mrb, nghttp2::tls::get_tls_protocol(ssl)); +} +} // namespace + +namespace { +mrb_value env_get_tls_session_id(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_str_new_static(mrb, "", 0); + } + + auto session = SSL_get_session(ssl); + if (!session) { + return mrb_str_new_static(mrb, "", 0); + } + + unsigned int session_id_length = 0; + auto session_id = SSL_SESSION_get_id(session, &session_id_length); + + // TODO Use template version of util::format_hex. + auto &balloc = downstream->get_block_allocator(); + auto id = util::format_hex(balloc, StringRef{session_id, session_id_length}); + return mrb_str_new(mrb, id.c_str(), id.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_session_reused(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto ssl = handler->get_ssl(); + + if (!ssl) { + return mrb_false_value(); + } + + return SSL_session_reused(ssl) ? mrb_true_value() : mrb_false_value(); +} +} // namespace + +namespace { +mrb_value env_get_alpn(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto alpn = handler->get_alpn(); + return mrb_str_new(mrb, alpn.c_str(), alpn.size()); +} +} // namespace + +namespace { +mrb_value env_get_tls_handshake_finished(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + auto handler = upstream->get_client_handler(); + auto conn = handler->get_connection(); + return SSL_is_init_finished(conn->tls.ssl) ? mrb_true_value() + : mrb_false_value(); +} +} // namespace + +void init_env_class(mrb_state *mrb, RClass *module) { + auto env_class = + mrb_define_class_under(mrb, module, "Env", mrb->object_class); + + mrb_define_method(mrb, env_class, "initialize", env_init, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "req", env_get_req, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "resp", env_get_resp, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "ctx", env_get_ctx, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "phase", env_get_phase, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "remote_addr", env_get_remote_addr, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "server_addr", env_get_server_addr, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "server_port", env_get_server_port, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_used", env_get_tls_used, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_sni", env_get_tls_sni, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_client_fingerprint_sha256", + env_get_tls_client_fingerprint_sha256, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_client_fingerprint_sha1", + env_get_tls_client_fingerprint_sha1, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_client_issuer_name", + env_get_tls_client_issuer_name, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_client_subject_name", + env_get_tls_client_subject_name, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_client_serial", + env_get_tls_client_serial, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_client_not_before", + env_get_tls_client_not_before, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_client_not_after", + env_get_tls_client_not_after, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_cipher", env_get_tls_cipher, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_protocol", env_get_tls_protocol, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_session_id", env_get_tls_session_id, + MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_session_reused", + env_get_tls_session_reused, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "alpn", env_get_alpn, MRB_ARGS_NONE()); + mrb_define_method(mrb, env_class, "tls_handshake_finished", + env_get_tls_handshake_finished, MRB_ARGS_NONE()); +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module_env.h b/src/shrpx_mruby_module_env.h new file mode 100644 index 0000000..0884678 --- /dev/null +++ b/src/shrpx_mruby_module_env.h @@ -0,0 +1,42 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MRUBY_MODULE_ENV_H +#define SHRPX_MRUBY_MODULE_ENV_H + +#include "shrpx.h" + +#include <mruby.h> + +namespace shrpx { + +namespace mruby { + +void init_env_class(mrb_state *mrb, RClass *module); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_ENV_H diff --git a/src/shrpx_mruby_module_request.cc b/src/shrpx_mruby_module_request.cc new file mode 100644 index 0000000..1ed3aa9 --- /dev/null +++ b/src/shrpx_mruby_module_request.cc @@ -0,0 +1,367 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_mruby_module_request.h" + +#include <mruby/variable.h> +#include <mruby/string.h> +#include <mruby/hash.h> +#include <mruby/array.h> + +#include "shrpx_downstream.h" +#include "shrpx_upstream.h" +#include "shrpx_client_handler.h" +#include "shrpx_mruby.h" +#include "shrpx_mruby_module.h" +#include "shrpx_log.h" +#include "util.h" +#include "http2.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value request_init(mrb_state *mrb, mrb_value self) { return self; } +} // namespace + +namespace { +mrb_value request_get_http_version_major(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &req = downstream->request(); + return mrb_fixnum_value(req.http_major); +} +} // namespace + +namespace { +mrb_value request_get_http_version_minor(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &req = downstream->request(); + return mrb_fixnum_value(req.http_minor); +} +} // namespace + +namespace { +mrb_value request_get_method(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &req = downstream->request(); + auto method = http2::to_method_string(req.method); + + return mrb_str_new(mrb, method.c_str(), method.size()); +} +} // namespace + +namespace { +mrb_value request_set_method(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &req = downstream->request(); + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *method; + mrb_int n; + mrb_get_args(mrb, "s", &method, &n); + if (n == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "method must not be empty string"); + } + auto token = + http2::lookup_method_token(reinterpret_cast<const uint8_t *>(method), n); + if (token == -1) { + mrb_raise(mrb, E_RUNTIME_ERROR, "method not supported"); + } + + req.method = token; + + return self; +} +} // namespace + +namespace { +mrb_value request_get_authority(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &req = downstream->request(); + + return mrb_str_new(mrb, req.authority.c_str(), req.authority.size()); +} +} // namespace + +namespace { +mrb_value request_set_authority(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &req = downstream->request(); + + auto &balloc = downstream->get_block_allocator(); + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *authority; + mrb_int n; + mrb_get_args(mrb, "s", &authority, &n); + if (n == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "authority must not be empty string"); + } + + req.authority = + make_string_ref(balloc, StringRef{authority, static_cast<size_t>(n)}); + + return self; +} +} // namespace + +namespace { +mrb_value request_get_scheme(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &req = downstream->request(); + + return mrb_str_new(mrb, req.scheme.c_str(), req.scheme.size()); +} +} // namespace + +namespace { +mrb_value request_set_scheme(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &req = downstream->request(); + + auto &balloc = downstream->get_block_allocator(); + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *scheme; + mrb_int n; + mrb_get_args(mrb, "s", &scheme, &n); + if (n == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "scheme must not be empty string"); + } + + req.scheme = + make_string_ref(balloc, StringRef{scheme, static_cast<size_t>(n)}); + + return self; +} +} // namespace + +namespace { +mrb_value request_get_path(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &req = downstream->request(); + + return mrb_str_new(mrb, req.path.c_str(), req.path.size()); +} +} // namespace + +namespace { +mrb_value request_set_path(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &req = downstream->request(); + + auto &balloc = downstream->get_block_allocator(); + + check_phase(mrb, data->phase, PHASE_REQUEST); + + const char *path; + mrb_int pathlen; + mrb_get_args(mrb, "s", &path, &pathlen); + + req.path = + make_string_ref(balloc, StringRef{path, static_cast<size_t>(pathlen)}); + + return self; +} +} // namespace + +namespace { +mrb_value request_get_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &req = downstream->request(); + return create_headers_hash(mrb, req.fs.headers()); +} +} // namespace + +namespace { +mrb_value request_mod_header(mrb_state *mrb, mrb_value self, bool repl) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &req = downstream->request(); + auto &balloc = downstream->get_block_allocator(); + + check_phase(mrb, data->phase, PHASE_REQUEST); + + mrb_value key, values; + mrb_get_args(mrb, "So", &key, &values); + + if (RSTRING_LEN(key) == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "empty key is not allowed"); + } + + auto ai = mrb_gc_arena_save(mrb); + + key = mrb_funcall(mrb, key, "downcase", 0); + + auto keyref = + make_string_ref(balloc, StringRef{RSTRING_PTR(key), + static_cast<size_t>(RSTRING_LEN(key))}); + + mrb_gc_arena_restore(mrb, ai); + + auto token = http2::lookup_token(keyref.byte(), keyref.size()); + + if (repl) { + size_t p = 0; + auto &headers = req.fs.headers(); + for (size_t i = 0; i < headers.size(); ++i) { + auto &kv = headers[i]; + if (kv.name == keyref) { + continue; + } + if (i != p) { + headers[p] = std::move(kv); + } + ++p; + } + headers.resize(p); + } + + if (mrb_array_p(values)) { + auto n = RARRAY_LEN(values); + for (int i = 0; i < n; ++i) { + auto value = mrb_ary_ref(mrb, values, i); + if (!mrb_string_p(value)) { + mrb_raise(mrb, E_RUNTIME_ERROR, "value must be string"); + } + + req.fs.add_header_token( + keyref, + make_string_ref(balloc, + StringRef{RSTRING_PTR(value), + static_cast<size_t>(RSTRING_LEN(value))}), + false, token); + } + } else if (mrb_string_p(values)) { + req.fs.add_header_token( + keyref, + make_string_ref(balloc, + StringRef{RSTRING_PTR(values), + static_cast<size_t>(RSTRING_LEN(values))}), + false, token); + } else { + mrb_raise(mrb, E_RUNTIME_ERROR, "value must be string"); + } + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value request_set_header(mrb_state *mrb, mrb_value self) { + return request_mod_header(mrb, self, true); +} +} // namespace + +namespace { +mrb_value request_add_header(mrb_state *mrb, mrb_value self) { + return request_mod_header(mrb, self, false); +} +} // namespace + +namespace { +mrb_value request_clear_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &req = downstream->request(); + + check_phase(mrb, data->phase, PHASE_REQUEST); + + req.fs.clear_headers(); + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value request_push(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto upstream = downstream->get_upstream(); + + const char *uri; + mrb_int len; + mrb_get_args(mrb, "s", &uri, &len); + + upstream->initiate_push(downstream, StringRef{uri, static_cast<size_t>(len)}); + + return mrb_nil_value(); +} +} // namespace + +void init_request_class(mrb_state *mrb, RClass *module) { + auto request_class = + mrb_define_class_under(mrb, module, "Request", mrb->object_class); + + mrb_define_method(mrb, request_class, "initialize", request_init, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "http_version_major", + request_get_http_version_major, MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "http_version_minor", + request_get_http_version_minor, MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "method", request_get_method, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "method=", request_set_method, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "authority", request_get_authority, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "authority=", request_set_authority, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "scheme", request_get_scheme, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "scheme=", request_set_scheme, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "path", request_get_path, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "path=", request_set_path, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, request_class, "headers", request_get_headers, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "add_header", request_add_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, request_class, "set_header", request_set_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, request_class, "clear_headers", request_clear_headers, + MRB_ARGS_NONE()); + mrb_define_method(mrb, request_class, "push", request_push, MRB_ARGS_REQ(1)); +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module_request.h b/src/shrpx_mruby_module_request.h new file mode 100644 index 0000000..e74f335 --- /dev/null +++ b/src/shrpx_mruby_module_request.h @@ -0,0 +1,42 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MRUBY_MODULE_REQUEST_H +#define SHRPX_MRUBY_MODULE_REQUEST_H + +#include "shrpx.h" + +#include <mruby.h> + +namespace shrpx { + +namespace mruby { + +void init_request_class(mrb_state *mrb, RClass *module); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_REQUEST_H diff --git a/src/shrpx_mruby_module_response.cc b/src/shrpx_mruby_module_response.cc new file mode 100644 index 0000000..1de1d5f --- /dev/null +++ b/src/shrpx_mruby_module_response.cc @@ -0,0 +1,398 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_mruby_module_response.h" + +#include <mruby/variable.h> +#include <mruby/string.h> +#include <mruby/hash.h> +#include <mruby/array.h> + +#include "shrpx_downstream.h" +#include "shrpx_upstream.h" +#include "shrpx_client_handler.h" +#include "shrpx_mruby.h" +#include "shrpx_mruby_module.h" +#include "shrpx_log.h" +#include "util.h" +#include "http2.h" + +namespace shrpx { + +namespace mruby { + +namespace { +mrb_value response_init(mrb_state *mrb, mrb_value self) { return self; } +} // namespace + +namespace { +mrb_value response_get_http_version_major(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &resp = downstream->response(); + return mrb_fixnum_value(resp.http_major); +} +} // namespace + +namespace { +mrb_value response_get_http_version_minor(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &resp = downstream->response(); + return mrb_fixnum_value(resp.http_minor); +} +} // namespace + +namespace { +mrb_value response_get_status(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &resp = downstream->response(); + return mrb_fixnum_value(resp.http_status); +} +} // namespace + +namespace { +mrb_value response_set_status(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &resp = downstream->response(); + + mrb_int status; + mrb_get_args(mrb, "i", &status); + // We don't support 1xx status code for mruby scripting yet. + if (status < 200 || status > 999) { + mrb_raise(mrb, E_RUNTIME_ERROR, + "invalid status; it should be [200, 999], inclusive"); + } + + resp.http_status = status; + + return self; +} +} // namespace + +namespace { +mrb_value response_get_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + const auto &resp = downstream->response(); + + return create_headers_hash(mrb, resp.fs.headers()); +} +} // namespace + +namespace { +mrb_value response_mod_header(mrb_state *mrb, mrb_value self, bool repl) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &resp = downstream->response(); + auto &balloc = downstream->get_block_allocator(); + + mrb_value key, values; + mrb_get_args(mrb, "So", &key, &values); + + if (RSTRING_LEN(key) == 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "empty key is not allowed"); + } + + auto ai = mrb_gc_arena_save(mrb); + + key = mrb_funcall(mrb, key, "downcase", 0); + + auto keyref = + make_string_ref(balloc, StringRef{RSTRING_PTR(key), + static_cast<size_t>(RSTRING_LEN(key))}); + + mrb_gc_arena_restore(mrb, ai); + + auto token = http2::lookup_token(keyref.byte(), keyref.size()); + + if (repl) { + size_t p = 0; + auto &headers = resp.fs.headers(); + for (size_t i = 0; i < headers.size(); ++i) { + auto &kv = headers[i]; + if (kv.name == keyref) { + continue; + } + if (i != p) { + headers[p] = std::move(kv); + } + ++p; + } + headers.resize(p); + } + + if (mrb_array_p(values)) { + auto n = RARRAY_LEN(values); + for (int i = 0; i < n; ++i) { + auto value = mrb_ary_ref(mrb, values, i); + if (!mrb_string_p(value)) { + mrb_raise(mrb, E_RUNTIME_ERROR, "value must be string"); + } + + resp.fs.add_header_token( + keyref, + make_string_ref(balloc, + StringRef{RSTRING_PTR(value), + static_cast<size_t>(RSTRING_LEN(value))}), + false, token); + } + } else if (mrb_string_p(values)) { + resp.fs.add_header_token( + keyref, + make_string_ref(balloc, + StringRef{RSTRING_PTR(values), + static_cast<size_t>(RSTRING_LEN(values))}), + false, token); + } else { + mrb_raise(mrb, E_RUNTIME_ERROR, "value must be string"); + } + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value response_set_header(mrb_state *mrb, mrb_value self) { + return response_mod_header(mrb, self, true); +} +} // namespace + +namespace { +mrb_value response_add_header(mrb_state *mrb, mrb_value self) { + return response_mod_header(mrb, self, false); +} +} // namespace + +namespace { +mrb_value response_clear_headers(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &resp = downstream->response(); + + resp.fs.clear_headers(); + + return mrb_nil_value(); +} +} // namespace + +namespace { +mrb_value response_return(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &req = downstream->request(); + auto &resp = downstream->response(); + int rv; + + auto &balloc = downstream->get_block_allocator(); + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + mrb_raise(mrb, E_RUNTIME_ERROR, "response has already been committed"); + } + + const char *val; + mrb_int vallen; + mrb_get_args(mrb, "|s", &val, &vallen); + + const uint8_t *body = nullptr; + size_t bodylen = 0; + + if (resp.http_status == 0) { + resp.http_status = 200; + } + + if (downstream->expect_response_body() && vallen > 0) { + body = reinterpret_cast<const uint8_t *>(val); + bodylen = vallen; + } + + auto cl = resp.fs.header(http2::HD_CONTENT_LENGTH); + + if (resp.http_status == 204 || + (resp.http_status == 200 && req.method == HTTP_CONNECT)) { + if (cl) { + // Delete content-length here + http2::erase_header(cl); + } + + resp.fs.content_length = -1; + } else { + auto content_length = util::make_string_ref_uint(balloc, vallen); + + if (cl) { + cl->value = content_length; + } else { + resp.fs.add_header_token(StringRef::from_lit("content-length"), + content_length, false, http2::HD_CONTENT_LENGTH); + } + + resp.fs.content_length = vallen; + } + + auto date = resp.fs.header(http2::HD_DATE); + if (!date) { + auto lgconf = log_config(); + lgconf->update_tstamp(std::chrono::system_clock::now()); + resp.fs.add_header_token(StringRef::from_lit("date"), + make_string_ref(balloc, lgconf->tstamp->time_http), + false, http2::HD_DATE); + } + + auto upstream = downstream->get_upstream(); + + rv = upstream->send_reply(downstream, body, bodylen); + if (rv != 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "could not send response"); + } + + auto handler = upstream->get_client_handler(); + + handler->signal_write(); + + return self; +} +} // namespace + +namespace { +mrb_value response_send_info(mrb_state *mrb, mrb_value self) { + auto data = static_cast<MRubyAssocData *>(mrb->ud); + auto downstream = data->downstream; + auto &resp = downstream->response(); + int rv; + + if (downstream->get_response_state() == DownstreamState::MSG_COMPLETE) { + mrb_raise(mrb, E_RUNTIME_ERROR, "response has already been committed"); + } + + mrb_int http_status; + mrb_value hash; + mrb_get_args(mrb, "iH", &http_status, &hash); + + if (http_status / 100 != 1) { + mrb_raise(mrb, E_RUNTIME_ERROR, + "status_code must be in range [100, 199], inclusive"); + } + + auto &balloc = downstream->get_block_allocator(); + + auto keys = mrb_hash_keys(mrb, hash); + auto keyslen = RARRAY_LEN(keys); + + for (int i = 0; i < keyslen; ++i) { + auto key = mrb_ary_ref(mrb, keys, i); + if (!mrb_string_p(key)) { + mrb_raise(mrb, E_RUNTIME_ERROR, "key must be string"); + } + + auto values = mrb_hash_get(mrb, hash, key); + + auto ai = mrb_gc_arena_save(mrb); + + key = mrb_funcall(mrb, key, "downcase", 0); + + auto keyref = make_string_ref( + balloc, + StringRef{RSTRING_PTR(key), static_cast<size_t>(RSTRING_LEN(key))}); + + mrb_gc_arena_restore(mrb, ai); + + auto token = http2::lookup_token(keyref.byte(), keyref.size()); + + if (mrb_array_p(values)) { + auto n = RARRAY_LEN(values); + for (int i = 0; i < n; ++i) { + auto value = mrb_ary_ref(mrb, values, i); + if (!mrb_string_p(value)) { + mrb_raise(mrb, E_RUNTIME_ERROR, "value must be string"); + } + + resp.fs.add_header_token( + keyref, + make_string_ref(balloc, + StringRef{RSTRING_PTR(value), + static_cast<size_t>(RSTRING_LEN(value))}), + false, token); + } + } else if (mrb_string_p(values)) { + resp.fs.add_header_token( + keyref, + make_string_ref(balloc, + StringRef{RSTRING_PTR(values), + static_cast<size_t>(RSTRING_LEN(values))}), + false, token); + } else { + mrb_raise(mrb, E_RUNTIME_ERROR, "value must be string"); + } + } + + resp.http_status = http_status; + + auto upstream = downstream->get_upstream(); + + rv = upstream->on_downstream_header_complete(downstream); + if (rv != 0) { + mrb_raise(mrb, E_RUNTIME_ERROR, "could not send non-final response"); + } + + auto handler = upstream->get_client_handler(); + + handler->signal_write(); + + return self; +} +} // namespace + +void init_response_class(mrb_state *mrb, RClass *module) { + auto response_class = + mrb_define_class_under(mrb, module, "Response", mrb->object_class); + + mrb_define_method(mrb, response_class, "initialize", response_init, + MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "http_version_major", + response_get_http_version_major, MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "http_version_minor", + response_get_http_version_minor, MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "status", response_get_status, + MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "status=", response_set_status, + MRB_ARGS_REQ(1)); + mrb_define_method(mrb, response_class, "headers", response_get_headers, + MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "add_header", response_add_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, response_class, "set_header", response_set_header, + MRB_ARGS_REQ(2)); + mrb_define_method(mrb, response_class, "clear_headers", + response_clear_headers, MRB_ARGS_NONE()); + mrb_define_method(mrb, response_class, "return", response_return, + MRB_ARGS_OPT(1)); + mrb_define_method(mrb, response_class, "send_info", response_send_info, + MRB_ARGS_REQ(2)); +} + +} // namespace mruby + +} // namespace shrpx diff --git a/src/shrpx_mruby_module_response.h b/src/shrpx_mruby_module_response.h new file mode 100644 index 0000000..a35b42b --- /dev/null +++ b/src/shrpx_mruby_module_response.h @@ -0,0 +1,42 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_MRUBY_MODULE_RESPONSE_H +#define SHRPX_MRUBY_MODULE_RESPONSE_H + +#include "shrpx.h" + +#include <mruby.h> + +namespace shrpx { + +namespace mruby { + +void init_response_class(mrb_state *mrb, RClass *module); + +} // namespace mruby + +} // namespace shrpx + +#endif // SHRPX_MRUBY_MODULE_RESPONSE_H diff --git a/src/shrpx_null_downstream_connection.cc b/src/shrpx_null_downstream_connection.cc new file mode 100644 index 0000000..cd81c8a --- /dev/null +++ b/src/shrpx_null_downstream_connection.cc @@ -0,0 +1,88 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_null_downstream_connection.h" +#include "shrpx_upstream.h" +#include "shrpx_downstream.h" +#include "shrpx_log.h" + +namespace shrpx { + +NullDownstreamConnection::NullDownstreamConnection( + const std::shared_ptr<DownstreamAddrGroup> &group) + : group_(group) {} + +NullDownstreamConnection::~NullDownstreamConnection() {} + +int NullDownstreamConnection::attach_downstream(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Attaching to DOWNSTREAM:" << downstream; + } + + downstream_ = downstream; + + return 0; +} + +void NullDownstreamConnection::detach_downstream(Downstream *downstream) { + if (LOG_ENABLED(INFO)) { + DCLOG(INFO, this) << "Detaching from DOWNSTREAM:" << downstream; + } + downstream_ = nullptr; +} + +int NullDownstreamConnection::push_request_headers() { return 0; } + +int NullDownstreamConnection::push_upload_data_chunk(const uint8_t *data, + size_t datalen) { + return 0; +} + +int NullDownstreamConnection::end_upload_data() { return 0; } + +void NullDownstreamConnection::pause_read(IOCtrlReason reason) {} + +int NullDownstreamConnection::resume_read(IOCtrlReason reason, + size_t consumed) { + return 0; +} + +void NullDownstreamConnection::force_resume_read() {} + +int NullDownstreamConnection::on_read() { return 0; } + +int NullDownstreamConnection::on_write() { return 0; } + +void NullDownstreamConnection::on_upstream_change(Upstream *upstream) {} + +bool NullDownstreamConnection::poolable() const { return false; } + +const std::shared_ptr<DownstreamAddrGroup> & +NullDownstreamConnection::get_downstream_addr_group() const { + return group_; +} + +DownstreamAddr *NullDownstreamConnection::get_addr() const { return nullptr; } + +} // namespace shrpx diff --git a/src/shrpx_null_downstream_connection.h b/src/shrpx_null_downstream_connection.h new file mode 100644 index 0000000..7defcc3 --- /dev/null +++ b/src/shrpx_null_downstream_connection.h @@ -0,0 +1,68 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_NULL_DOWNSTREAM_CONNECTION_H +#define SHRPX_NULL_DOWNSTREAM_CONNECTION_H + +#include "shrpx_downstream_connection.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +class NullDownstreamConnection : public DownstreamConnection { +public: + NullDownstreamConnection(const std::shared_ptr<DownstreamAddrGroup> &group); + virtual ~NullDownstreamConnection(); + virtual int attach_downstream(Downstream *downstream); + virtual void detach_downstream(Downstream *downstream); + + virtual int push_request_headers(); + virtual int push_upload_data_chunk(const uint8_t *data, size_t datalen); + virtual int end_upload_data(); + + virtual void pause_read(IOCtrlReason reason); + virtual int resume_read(IOCtrlReason reason, size_t consumed); + virtual void force_resume_read(); + + virtual int on_read(); + virtual int on_write(); + + virtual void on_upstream_change(Upstream *upstream); + + // true if this object is poolable. + virtual bool poolable() const; + + virtual const std::shared_ptr<DownstreamAddrGroup> & + get_downstream_addr_group() const; + virtual DownstreamAddr *get_addr() const; + +private: + std::shared_ptr<DownstreamAddrGroup> group_; +}; + +} // namespace shrpx + +#endif // SHRPX_NULL_DOWNSTREAM_CONNECTION_H diff --git a/src/shrpx_process.h b/src/shrpx_process.h new file mode 100644 index 0000000..d35461b --- /dev/null +++ b/src/shrpx_process.h @@ -0,0 +1,37 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_PROCESS_H +#define SHRPX_PROCESS_H + +#include "shrpx.h" + +namespace shrpx { + +constexpr uint8_t SHRPX_IPC_REOPEN_LOG = 1; +constexpr uint8_t SHRPX_IPC_GRACEFUL_SHUTDOWN = 2; + +} // namespace shrpx + +#endif // SHRPX_PROCESS_H diff --git a/src/shrpx_quic.cc b/src/shrpx_quic.cc new file mode 100644 index 0000000..2d4de59 --- /dev/null +++ b/src/shrpx_quic.cc @@ -0,0 +1,393 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_quic.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> +#include <netinet/udp.h> + +#include <array> +#include <chrono> + +#include <ngtcp2/ngtcp2_crypto.h> + +#include <nghttp3/nghttp3.h> + +#include <openssl/rand.h> + +#include "shrpx_config.h" +#include "shrpx_log.h" +#include "util.h" +#include "xsi_strerror.h" + +bool operator==(const ngtcp2_cid &lhs, const ngtcp2_cid &rhs) { + return ngtcp2_cid_eq(&lhs, &rhs); +} + +namespace shrpx { + +ngtcp2_tstamp quic_timestamp() { + return std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +int quic_send_packet(const UpstreamAddr *faddr, const sockaddr *remote_sa, + size_t remote_salen, const sockaddr *local_sa, + size_t local_salen, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen, size_t gso_size) { + iovec msg_iov = {const_cast<uint8_t *>(data), datalen}; + msghdr msg{}; + msg.msg_name = const_cast<sockaddr *>(remote_sa); + msg.msg_namelen = remote_salen; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(int)) + +#ifdef UDP_SEGMENT + CMSG_SPACE(sizeof(uint16_t)) + +#endif // UDP_SEGMENT + CMSG_SPACE(sizeof(in6_pktinfo))]; + + memset(msg_ctrl, 0, sizeof(msg_ctrl)); + + msg.msg_control = msg_ctrl; + msg.msg_controllen = sizeof(msg_ctrl); + + size_t controllen = 0; + + auto cm = CMSG_FIRSTHDR(&msg); + + switch (local_sa->sa_family) { + case AF_INET: { + controllen += CMSG_SPACE(sizeof(in_pktinfo)); + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in_pktinfo)); + in_pktinfo pktinfo{}; + auto addrin = + reinterpret_cast<sockaddr_in *>(const_cast<sockaddr *>(local_sa)); + pktinfo.ipi_spec_dst = addrin->sin_addr; + memcpy(CMSG_DATA(cm), &pktinfo, sizeof(pktinfo)); + + break; + } + case AF_INET6: { + controllen += CMSG_SPACE(sizeof(in6_pktinfo)); + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_PKTINFO; + cm->cmsg_len = CMSG_LEN(sizeof(in6_pktinfo)); + in6_pktinfo pktinfo{}; + auto addrin = + reinterpret_cast<sockaddr_in6 *>(const_cast<sockaddr *>(local_sa)); + pktinfo.ipi6_addr = addrin->sin6_addr; + memcpy(CMSG_DATA(cm), &pktinfo, sizeof(pktinfo)); + + break; + } + default: + assert(0); + } + +#ifdef UDP_SEGMENT + if (gso_size && datalen > gso_size) { + controllen += CMSG_SPACE(sizeof(uint16_t)); + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + uint16_t n = gso_size; + memcpy(CMSG_DATA(cm), &n, sizeof(n)); + } +#endif // UDP_SEGMENT + + controllen += CMSG_SPACE(sizeof(int)); + cm = CMSG_NXTHDR(&msg, cm); + cm->cmsg_len = CMSG_LEN(sizeof(int)); + unsigned int tos = pi.ecn; + memcpy(CMSG_DATA(cm), &tos, sizeof(tos)); + + switch (local_sa->sa_family) { + case AF_INET: + cm->cmsg_level = IPPROTO_IP; + cm->cmsg_type = IP_TOS; + + break; + case AF_INET6: + cm->cmsg_level = IPPROTO_IPV6; + cm->cmsg_type = IPV6_TCLASS; + + break; + default: + assert(0); + } + + msg.msg_controllen = controllen; + + ssize_t nwrite; + + do { + nwrite = sendmsg(faddr->fd, &msg, 0); + } while (nwrite == -1 && errno == EINTR); + + if (nwrite == -1) { + if (LOG_ENABLED(INFO)) { + auto error = errno; + LOG(INFO) << "sendmsg failed: errno=" << error; + } + + return -errno; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "QUIC sent packet: local=" + << util::to_numeric_addr(local_sa, local_salen) + << " remote=" << util::to_numeric_addr(remote_sa, remote_salen) + << " ecn=" << log::hex << pi.ecn << log::dec << " " << nwrite + << " bytes"; + } + + return 0; +} + +int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen, + const uint8_t *server_id, uint8_t km_id, + const uint8_t *key) { + assert(cidlen == SHRPX_QUIC_SCIDLEN); + + if (RAND_bytes(cid.data, cidlen) != 1) { + return -1; + } + + cid.datalen = cidlen; + + cid.data[0] = (cid.data[0] & 0x3f) | km_id; + + auto p = cid.data + SHRPX_QUIC_CID_PREFIX_OFFSET; + + std::copy_n(server_id, SHRPX_QUIC_SERVER_IDLEN, p); + + return encrypt_quic_connection_id(p, p, key); +} + +int generate_quic_connection_id(ngtcp2_cid &cid, size_t cidlen, + const uint8_t *cid_prefix, uint8_t km_id, + const uint8_t *key) { + assert(cidlen == SHRPX_QUIC_SCIDLEN); + + if (RAND_bytes(cid.data, cidlen) != 1) { + return -1; + } + + cid.datalen = cidlen; + + cid.data[0] = (cid.data[0] & 0x3f) | km_id; + + auto p = cid.data + SHRPX_QUIC_CID_PREFIX_OFFSET; + + std::copy_n(cid_prefix, SHRPX_QUIC_CID_PREFIXLEN, p); + + return encrypt_quic_connection_id(p, p, key); +} + +int encrypt_quic_connection_id(uint8_t *dest, const uint8_t *src, + const uint8_t *key) { + auto ctx = EVP_CIPHER_CTX_new(); + auto d = defer(EVP_CIPHER_CTX_free, ctx); + + if (!EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), nullptr, key, nullptr)) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(ctx, 0); + + int len; + + if (!EVP_EncryptUpdate(ctx, dest, &len, src, SHRPX_QUIC_DECRYPTED_DCIDLEN) || + !EVP_EncryptFinal_ex(ctx, dest + len, &len)) { + return -1; + } + + return 0; +} + +int decrypt_quic_connection_id(uint8_t *dest, const uint8_t *src, + const uint8_t *key) { + auto ctx = EVP_CIPHER_CTX_new(); + auto d = defer(EVP_CIPHER_CTX_free, ctx); + + if (!EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), nullptr, key, nullptr)) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(ctx, 0); + + int len; + + if (!EVP_DecryptUpdate(ctx, dest, &len, src, SHRPX_QUIC_DECRYPTED_DCIDLEN) || + !EVP_DecryptFinal_ex(ctx, dest + len, &len)) { + return -1; + } + + return 0; +} + +int generate_quic_hashed_connection_id(ngtcp2_cid &dest, + const Address &remote_addr, + const Address &local_addr, + const ngtcp2_cid &cid) { + auto ctx = EVP_MD_CTX_new(); + auto d = defer(EVP_MD_CTX_free, ctx); + + std::array<uint8_t, 32> h; + unsigned int hlen = EVP_MD_size(EVP_sha256()); + + if (!EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) || + !EVP_DigestUpdate(ctx, &remote_addr.su.sa, remote_addr.len) || + !EVP_DigestUpdate(ctx, &local_addr.su.sa, local_addr.len) || + !EVP_DigestUpdate(ctx, cid.data, cid.datalen) || + !EVP_DigestFinal_ex(ctx, h.data(), &hlen)) { + return -1; + } + + assert(hlen == h.size()); + + std::copy_n(std::begin(h), sizeof(dest.data), std::begin(dest.data)); + dest.datalen = sizeof(dest.data); + + return 0; +} + +int generate_quic_stateless_reset_token(uint8_t *token, const ngtcp2_cid &cid, + const uint8_t *secret, + size_t secretlen) { + if (ngtcp2_crypto_generate_stateless_reset_token(token, secret, secretlen, + &cid) != 0) { + return -1; + } + + return 0; +} + +int generate_retry_token(uint8_t *token, size_t &tokenlen, uint32_t version, + const sockaddr *sa, socklen_t salen, + const ngtcp2_cid &retry_scid, const ngtcp2_cid &odcid, + const uint8_t *secret, size_t secretlen) { + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + auto stokenlen = ngtcp2_crypto_generate_retry_token( + token, secret, secretlen, version, sa, salen, &retry_scid, &odcid, t); + if (stokenlen < 0) { + return -1; + } + + tokenlen = stokenlen; + + return 0; +} + +int verify_retry_token(ngtcp2_cid &odcid, const uint8_t *token, size_t tokenlen, + uint32_t version, const ngtcp2_cid &dcid, + const sockaddr *sa, socklen_t salen, + const uint8_t *secret, size_t secretlen) { + + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (ngtcp2_crypto_verify_retry_token(&odcid, token, tokenlen, secret, + secretlen, version, sa, salen, &dcid, + 10 * NGTCP2_SECONDS, t) != 0) { + return -1; + } + + return 0; +} + +int generate_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa, + size_t salen, const uint8_t *secret, size_t secretlen) { + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + auto stokenlen = ngtcp2_crypto_generate_regular_token( + token, secret, secretlen, sa, salen, t); + if (stokenlen < 0) { + return -1; + } + + tokenlen = stokenlen; + + return 0; +} + +int verify_token(const uint8_t *token, size_t tokenlen, const sockaddr *sa, + socklen_t salen, const uint8_t *secret, size_t secretlen) { + auto t = std::chrono::duration_cast<std::chrono::nanoseconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (ngtcp2_crypto_verify_regular_token(token, tokenlen, secret, secretlen, sa, + salen, 3600 * NGTCP2_SECONDS, + t) != 0) { + return -1; + } + + return 0; +} + +int generate_quic_connection_id_encryption_key(uint8_t *key, size_t keylen, + const uint8_t *secret, + size_t secretlen, + const uint8_t *salt, + size_t saltlen) { + constexpr uint8_t info[] = "connection id encryption key"; + ngtcp2_crypto_md sha256; + ngtcp2_crypto_md_init( + &sha256, reinterpret_cast<void *>(const_cast<EVP_MD *>(EVP_sha256()))); + + if (ngtcp2_crypto_hkdf(key, keylen, &sha256, secret, secretlen, salt, saltlen, + info, str_size(info)) != 0) { + return -1; + } + + return 0; +} + +const QUICKeyingMaterial * +select_quic_keying_material(const QUICKeyingMaterials &qkms, uint8_t km_id) { + for (auto &qkm : qkms.keying_materials) { + if (km_id == qkm.id) { + return &qkm; + } + } + + return &qkms.keying_materials.front(); +} + +} // namespace shrpx diff --git a/src/shrpx_quic.h b/src/shrpx_quic.h new file mode 100644 index 0000000..b2f6087 --- /dev/null +++ b/src/shrpx_quic.h @@ -0,0 +1,138 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_QUIC_H +#define SHRPX_QUIC_H + +#include "shrpx.h" + +#include <stdint.h> + +#include <functional> + +#include <ngtcp2/ngtcp2.h> + +#include "network.h" + +using namespace nghttp2; + +namespace std { +template <> struct hash<ngtcp2_cid> { + std::size_t operator()(const ngtcp2_cid &cid) const noexcept { + // FNV-1a 64bits variant + constexpr uint64_t basis = 0xCBF29CE484222325ULL; + const uint8_t *p = cid.data, *end = cid.data + cid.datalen; + uint64_t h = basis; + + for (; p != end;) { + h ^= *p++; + h *= basis; + } + + return static_cast<size_t>(h); + } +}; +} // namespace std + +bool operator==(const ngtcp2_cid &lhs, const ngtcp2_cid &rhs); + +namespace shrpx { + +struct UpstreamAddr; +struct QUICKeyingMaterials; +struct QUICKeyingMaterial; + +constexpr size_t SHRPX_QUIC_SCIDLEN = 20; +constexpr size_t SHRPX_QUIC_SERVER_IDLEN = 4; +// SHRPX_QUIC_CID_PREFIXLEN includes SHRPX_QUIC_SERVER_IDLEN. +constexpr size_t SHRPX_QUIC_CID_PREFIXLEN = 8; +constexpr size_t SHRPX_QUIC_CID_PREFIX_OFFSET = 1; +constexpr size_t SHRPX_QUIC_DECRYPTED_DCIDLEN = 16; +constexpr size_t SHRPX_QUIC_CID_ENCRYPTION_KEYLEN = 16; +constexpr size_t SHRPX_QUIC_MAX_UDP_PAYLOAD_SIZE = 1472; +constexpr size_t SHRPX_QUIC_CONN_CLOSE_PKTLEN = 256; +constexpr size_t SHRPX_QUIC_STATELESS_RESET_BURST = 100; +constexpr size_t SHRPX_QUIC_SECRET_RESERVEDLEN = 4; +constexpr size_t SHRPX_QUIC_SECRETLEN = 32; +constexpr size_t SHRPX_QUIC_SALTLEN = 32; +constexpr uint8_t SHRPX_QUIC_DCID_KM_ID_MASK = 0xc0; + +ngtcp2_tstamp quic_timestamp(); + +int quic_send_packet(const UpstreamAddr *faddr, const sockaddr *remote_sa, + size_t remote_salen, const sockaddr *local_sa, + size_t local_salen, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen, size_t gso_size); + +int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen, + const uint8_t *server_id, uint8_t km_id, + const uint8_t *key); + +int generate_quic_connection_id(ngtcp2_cid &cid, size_t cidlen, + const uint8_t *cid_prefix, uint8_t km_id, + const uint8_t *key); + +int encrypt_quic_connection_id(uint8_t *dest, const uint8_t *src, + const uint8_t *key); + +int decrypt_quic_connection_id(uint8_t *dest, const uint8_t *src, + const uint8_t *key); + +int generate_quic_hashed_connection_id(ngtcp2_cid &dest, + const Address &remote_addr, + const Address &local_addr, + const ngtcp2_cid &cid); + +int generate_quic_stateless_reset_token(uint8_t *token, const ngtcp2_cid &cid, + const uint8_t *secret, + size_t secretlen); + +int generate_retry_token(uint8_t *token, size_t &tokenlen, uint32_t version, + const sockaddr *sa, socklen_t salen, + const ngtcp2_cid &retry_scid, const ngtcp2_cid &odcid, + const uint8_t *secret, size_t secretlen); + +int verify_retry_token(ngtcp2_cid &odcid, const uint8_t *token, size_t tokenlen, + uint32_t version, const ngtcp2_cid &dcid, + const sockaddr *sa, socklen_t salen, + const uint8_t *secret, size_t secretlen); + +int generate_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa, + size_t salen, const uint8_t *secret, size_t secretlen); + +int verify_token(const uint8_t *token, size_t tokenlen, const sockaddr *sa, + socklen_t salen, const uint8_t *secret, size_t secretlen); + +int generate_quic_connection_id_encryption_key(uint8_t *key, size_t keylen, + const uint8_t *secret, + size_t secretlen, + const uint8_t *salt, + size_t saltlen); + +const QUICKeyingMaterial * +select_quic_keying_material(const QUICKeyingMaterials &qkms, uint8_t km_id); + +} // namespace shrpx + +#endif // SHRPX_QUIC_H diff --git a/src/shrpx_quic_connection_handler.cc b/src/shrpx_quic_connection_handler.cc new file mode 100644 index 0000000..6287971 --- /dev/null +++ b/src/shrpx_quic_connection_handler.cc @@ -0,0 +1,761 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_quic_connection_handler.h" + +#include <openssl/rand.h> + +#include <ngtcp2/ngtcp2.h> +#include <ngtcp2/ngtcp2_crypto.h> + +#include "shrpx_worker.h" +#include "shrpx_client_handler.h" +#include "shrpx_log.h" +#include "shrpx_http3_upstream.h" +#include "shrpx_connection_handler.h" +#include "ssl_compat.h" + +namespace shrpx { + +namespace { +void stateless_reset_bucket_regen_timercb(struct ev_loop *loop, ev_timer *w, + int revents) { + auto quic_conn_handler = static_cast<QUICConnectionHandler *>(w->data); + + quic_conn_handler->on_stateless_reset_bucket_regen(); +} +} // namespace + +QUICConnectionHandler::QUICConnectionHandler(Worker *worker) + : worker_{worker}, + stateless_reset_bucket_{SHRPX_QUIC_STATELESS_RESET_BURST} { + ev_timer_init(&stateless_reset_bucket_regen_timer_, + stateless_reset_bucket_regen_timercb, 0., 1.); + stateless_reset_bucket_regen_timer_.data = this; +} + +QUICConnectionHandler::~QUICConnectionHandler() { + ev_timer_stop(worker_->get_loop(), &stateless_reset_bucket_regen_timer_); +} + +int QUICConnectionHandler::handle_packet(const UpstreamAddr *faddr, + const Address &remote_addr, + const Address &local_addr, + const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen) { + int rv; + ngtcp2_version_cid vc; + + rv = ngtcp2_pkt_decode_version_cid(&vc, data, datalen, SHRPX_QUIC_SCIDLEN); + switch (rv) { + case 0: + break; + case NGTCP2_ERR_VERSION_NEGOTIATION: + send_version_negotiation(faddr, vc.version, vc.dcid, vc.dcidlen, vc.scid, + vc.scidlen, remote_addr, local_addr); + + return 0; + default: + return 0; + } + + auto config = get_config(); + + ngtcp2_cid dcid_key; + ngtcp2_cid_init(&dcid_key, vc.dcid, vc.dcidlen); + + auto conn_handler = worker_->get_connection_handler(); + + ClientHandler *handler; + + auto &quicconf = config->quic; + + auto it = connections_.find(dcid_key); + if (it == std::end(connections_)) { + auto cwit = close_waits_.find(dcid_key); + if (cwit != std::end(close_waits_)) { + auto cw = (*cwit).second; + + cw->handle_packet(faddr, remote_addr, local_addr, pi, data, datalen); + + return 0; + } + + if (data[0] & 0x80) { + if (generate_quic_hashed_connection_id(dcid_key, remote_addr, local_addr, + dcid_key) != 0) { + return 0; + } + + it = connections_.find(dcid_key); + if (it == std::end(connections_)) { + auto cwit = close_waits_.find(dcid_key); + if (cwit != std::end(close_waits_)) { + auto cw = (*cwit).second; + + cw->handle_packet(faddr, remote_addr, local_addr, pi, data, datalen); + + return 0; + } + } + } + } + + if (it == std::end(connections_)) { + std::array<uint8_t, SHRPX_QUIC_DECRYPTED_DCIDLEN> decrypted_dcid; + + auto &qkms = conn_handler->get_quic_keying_materials(); + const QUICKeyingMaterial *qkm = nullptr; + + if (vc.dcidlen == SHRPX_QUIC_SCIDLEN) { + qkm = select_quic_keying_material( + *qkms.get(), vc.dcid[0] & SHRPX_QUIC_DCID_KM_ID_MASK); + + if (decrypt_quic_connection_id(decrypted_dcid.data(), + vc.dcid + SHRPX_QUIC_CID_PREFIX_OFFSET, + qkm->cid_encryption_key.data()) != 0) { + return 0; + } + + if (qkm != &qkms->keying_materials.front() || + !std::equal(std::begin(decrypted_dcid), + std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN, + worker_->get_cid_prefix())) { + auto quic_lwp = + conn_handler->match_quic_lingering_worker_process_cid_prefix( + decrypted_dcid.data(), decrypted_dcid.size()); + if (quic_lwp) { + if (conn_handler->forward_quic_packet_to_lingering_worker_process( + quic_lwp, remote_addr, local_addr, pi, data, datalen) == 0) { + return 0; + } + + return 0; + } + } + } + + // new connection + + auto &upstreamconf = config->conn.upstream; + if (worker_->get_worker_stat()->num_connections >= + upstreamconf.worker_connections) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Too many connections >=" + << upstreamconf.worker_connections; + } + + return 0; + } + + ngtcp2_pkt_hd hd; + ngtcp2_cid odcid, *podcid = nullptr; + const uint8_t *token = nullptr; + size_t tokenlen = 0; + ngtcp2_token_type token_type = NGTCP2_TOKEN_TYPE_UNKNOWN; + + switch (ngtcp2_accept(&hd, data, datalen)) { + case 0: { + // If we get Initial and it has the CID prefix of this worker, + // it is likely that client is intentionally use the prefix. + // Just drop it. + if (vc.dcidlen == SHRPX_QUIC_SCIDLEN) { + if (qkm != &qkms->keying_materials.front()) { + qkm = &qkms->keying_materials.front(); + + if (decrypt_quic_connection_id(decrypted_dcid.data(), + vc.dcid + SHRPX_QUIC_CID_PREFIX_OFFSET, + qkm->cid_encryption_key.data()) != 0) { + return 0; + } + } + + if (std::equal(std::begin(decrypted_dcid), + std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN, + worker_->get_cid_prefix())) { + return 0; + } + } + + if (worker_->get_graceful_shutdown()) { + send_connection_close(faddr, hd.version, hd.dcid, hd.scid, remote_addr, + local_addr, NGTCP2_CONNECTION_REFUSED, + datalen * 3); + return 0; + } + + if (hd.tokenlen == 0) { + if (quicconf.upstream.require_token) { + send_retry(faddr, vc.version, vc.dcid, vc.dcidlen, vc.scid, + vc.scidlen, remote_addr, local_addr, datalen * 3); + + return 0; + } + + break; + } + + switch (hd.token[0]) { + case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY: { + if (vc.dcidlen != SHRPX_QUIC_SCIDLEN) { + // Initial packets with Retry token must have DCID chosen by + // server. + return 0; + } + + auto qkm = select_quic_keying_material( + *qkms.get(), vc.dcid[0] & SHRPX_QUIC_DCID_KM_ID_MASK); + + if (verify_retry_token(odcid, hd.token, hd.tokenlen, hd.version, + hd.dcid, &remote_addr.su.sa, remote_addr.len, + qkm->secret.data(), qkm->secret.size()) != 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Failed to validate Retry token from remote=" + << util::to_numeric_addr(&remote_addr); + } + + // 2nd Retry packet is not allowed, so send CONNECTION_CLOSE + // with INVALID_TOKEN. + send_connection_close(faddr, hd.version, hd.dcid, hd.scid, + remote_addr, local_addr, NGTCP2_INVALID_TOKEN, + datalen * 3); + return 0; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Successfully validated Retry token from remote=" + << util::to_numeric_addr(&remote_addr); + } + + podcid = &odcid; + token = hd.token; + tokenlen = hd.tokenlen; + token_type = NGTCP2_TOKEN_TYPE_RETRY; + + break; + } + case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR: { + // If a token is a regular token, it must be at least + // NGTCP2_MIN_INITIAL_DCIDLEN bytes long. + if (vc.dcidlen < NGTCP2_MIN_INITIAL_DCIDLEN) { + return 0; + } + + if (hd.tokenlen != NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN + 1) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Failed to validate token from remote=" + << util::to_numeric_addr(&remote_addr); + } + + if (quicconf.upstream.require_token) { + send_retry(faddr, vc.version, vc.dcid, vc.dcidlen, vc.scid, + vc.scidlen, remote_addr, local_addr, datalen * 3); + + return 0; + } + + break; + } + + auto qkm = select_quic_keying_material( + *qkms.get(), hd.token[NGTCP2_CRYPTO_MAX_REGULAR_TOKENLEN]); + + if (verify_token(hd.token, hd.tokenlen - 1, &remote_addr.su.sa, + remote_addr.len, qkm->secret.data(), + qkm->secret.size()) != 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Failed to validate token from remote=" + << util::to_numeric_addr(&remote_addr); + } + + if (quicconf.upstream.require_token) { + send_retry(faddr, vc.version, vc.dcid, vc.dcidlen, vc.scid, + vc.scidlen, remote_addr, local_addr, datalen * 3); + + return 0; + } + + break; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Successfully validated token from remote=" + << util::to_numeric_addr(&remote_addr); + } + + token = hd.token; + tokenlen = hd.tokenlen; + token_type = NGTCP2_TOKEN_TYPE_NEW_TOKEN; + + break; + } + default: + if (quicconf.upstream.require_token) { + send_retry(faddr, vc.version, vc.dcid, vc.dcidlen, vc.scid, + vc.scidlen, remote_addr, local_addr, datalen * 3); + + return 0; + } + + break; + } + + break; + } + default: + if (!config->single_thread && !(data[0] & 0x80) && + vc.dcidlen == SHRPX_QUIC_SCIDLEN && + !std::equal(std::begin(decrypted_dcid), + std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN, + worker_->get_cid_prefix())) { + if (conn_handler->forward_quic_packet(faddr, remote_addr, local_addr, + pi, decrypted_dcid.data(), data, + datalen) == 0) { + return 0; + } + } + + if (!(data[0] & 0x80)) { + // TODO Must be rate limited + send_stateless_reset(faddr, vc.dcid, vc.dcidlen, remote_addr, + local_addr); + } + + return 0; + } + + handler = handle_new_connection(faddr, remote_addr, local_addr, hd, podcid, + token, tokenlen, token_type); + if (handler == nullptr) { + return 0; + } + } else { + handler = (*it).second; + } + + if (handler->read_quic(faddr, remote_addr, local_addr, pi, data, datalen) != + 0) { + delete handler; + return 0; + } + + handler->signal_write(); + + return 0; +} + +ClientHandler *QUICConnectionHandler::handle_new_connection( + const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_hd &hd, const ngtcp2_cid *odcid, + const uint8_t *token, size_t tokenlen, ngtcp2_token_type token_type) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> service; + int rv; + + rv = getnameinfo(&remote_addr.su.sa, remote_addr.len, host.data(), + host.size(), service.data(), service.size(), + NI_NUMERICHOST | NI_NUMERICSERV); + if (rv != 0) { + LOG(ERROR) << "getnameinfo() failed: " << gai_strerror(rv); + + return nullptr; + } + + auto ssl_ctx = worker_->get_quic_sv_ssl_ctx(); + + assert(ssl_ctx); + + auto ssl = tls::create_ssl(ssl_ctx); + if (ssl == nullptr) { + return nullptr; + } + +#ifdef NGHTTP2_GENUINE_OPENSSL + assert(SSL_is_quic(ssl)); +#endif // NGHTTP2_GENUINE_OPENSSL + + SSL_set_accept_state(ssl); + + auto config = get_config(); + auto &quicconf = config->quic; + + if (quicconf.upstream.early_data) { +#ifdef NGHTTP2_GENUINE_OPENSSL + SSL_set_quic_early_data_enabled(ssl, 1); +#elif defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + SSL_set_early_data_enabled(ssl, 1); +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + } + + // Disable TLS session ticket if we don't have working ticket + // keys. + if (!worker_->get_ticket_keys()) { + SSL_set_options(ssl, SSL_OP_NO_TICKET); + } + + auto handler = std::make_unique<ClientHandler>( + worker_, faddr->fd, ssl, StringRef{host.data()}, + StringRef{service.data()}, remote_addr.su.sa.sa_family, faddr); + + auto upstream = std::make_unique<Http3Upstream>(handler.get()); + if (upstream->init(faddr, remote_addr, local_addr, hd, odcid, token, tokenlen, + token_type) != 0) { + return nullptr; + } + + handler->setup_http3_upstream(std::move(upstream)); + + return handler.release(); +} + +namespace { +uint32_t generate_reserved_version(const Address &addr, uint32_t version) { + uint32_t h = 0x811C9DC5u; + const uint8_t *p = reinterpret_cast<const uint8_t *>(&addr.su.sa); + const uint8_t *ep = p + addr.len; + + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + + version = htonl(version); + p = (const uint8_t *)&version; + ep = p + sizeof(version); + + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + + h &= 0xf0f0f0f0u; + h |= 0x0a0a0a0au; + + return h; +} +} // namespace + +int QUICConnectionHandler::send_retry( + const UpstreamAddr *faddr, uint32_t version, const uint8_t *ini_dcid, + size_t ini_dcidlen, const uint8_t *ini_scid, size_t ini_scidlen, + const Address &remote_addr, const Address &local_addr, size_t max_pktlen) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> port; + + if (getnameinfo(&remote_addr.su.sa, remote_addr.len, host.data(), host.size(), + port.data(), port.size(), + NI_NUMERICHOST | NI_NUMERICSERV) != 0) { + return -1; + } + + auto config = get_config(); + auto &quicconf = config->quic; + + auto conn_handler = worker_->get_connection_handler(); + auto &qkms = conn_handler->get_quic_keying_materials(); + auto &qkm = qkms->keying_materials.front(); + + ngtcp2_cid retry_scid; + + if (generate_quic_retry_connection_id(retry_scid, SHRPX_QUIC_SCIDLEN, + quicconf.server_id.data(), qkm.id, + qkm.cid_encryption_key.data()) != 0) { + return -1; + } + + std::array<uint8_t, NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN> token; + size_t tokenlen; + + ngtcp2_cid idcid, iscid; + ngtcp2_cid_init(&idcid, ini_dcid, ini_dcidlen); + ngtcp2_cid_init(&iscid, ini_scid, ini_scidlen); + + if (generate_retry_token(token.data(), tokenlen, version, &remote_addr.su.sa, + remote_addr.len, retry_scid, idcid, + qkm.secret.data(), qkm.secret.size()) != 0) { + return -1; + } + + std::vector<uint8_t> buf; + buf.resize(std::min(max_pktlen, static_cast<size_t>(256))); + + auto nwrite = + ngtcp2_crypto_write_retry(buf.data(), buf.size(), version, &iscid, + &retry_scid, &idcid, token.data(), tokenlen); + if (nwrite < 0) { + LOG(ERROR) << "ngtcp2_crypto_write_retry: " << ngtcp2_strerror(nwrite); + return -1; + } + + buf.resize(nwrite); + + quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, + &local_addr.su.sa, local_addr.len, ngtcp2_pkt_info{}, + buf.data(), buf.size(), 0); + + if (generate_quic_hashed_connection_id(idcid, remote_addr, local_addr, + idcid) != 0) { + return -1; + } + + auto d = + static_cast<ev_tstamp>(NGTCP2_DEFAULT_INITIAL_RTT * 3) / NGTCP2_SECONDS; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Enter close-wait period " << d << "s with " << buf.size() + << " bytes sentinel packet"; + } + + auto cw = std::make_unique<CloseWait>(worker_, std::vector<ngtcp2_cid>{idcid}, + std::move(buf), d); + + add_close_wait(cw.release()); + + return 0; +} + +int QUICConnectionHandler::send_version_negotiation( + const UpstreamAddr *faddr, uint32_t version, const uint8_t *ini_dcid, + size_t ini_dcidlen, const uint8_t *ini_scid, size_t ini_scidlen, + const Address &remote_addr, const Address &local_addr) { + std::array<uint32_t, 2> sv{ + generate_reserved_version(remote_addr, version), + NGTCP2_PROTO_VER_V1, + }; + + std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf; + + uint8_t rand_byte; + util::random_bytes(&rand_byte, &rand_byte + 1, worker_->get_randgen()); + + auto nwrite = ngtcp2_pkt_write_version_negotiation( + buf.data(), buf.size(), rand_byte, ini_scid, ini_scidlen, ini_dcid, + ini_dcidlen, sv.data(), sv.size()); + if (nwrite < 0) { + LOG(ERROR) << "ngtcp2_pkt_write_version_negotiation: " + << ngtcp2_strerror(nwrite); + return -1; + } + + return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, + &local_addr.su.sa, local_addr.len, ngtcp2_pkt_info{}, + buf.data(), nwrite, 0); +} + +int QUICConnectionHandler::send_stateless_reset(const UpstreamAddr *faddr, + const uint8_t *dcid, + size_t dcidlen, + const Address &remote_addr, + const Address &local_addr) { + if (stateless_reset_bucket_ == 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Stateless Reset bucket has been depleted"; + } + + return 0; + } + + --stateless_reset_bucket_; + + if (!ev_is_active(&stateless_reset_bucket_regen_timer_)) { + ev_timer_again(worker_->get_loop(), &stateless_reset_bucket_regen_timer_); + } + + int rv; + std::array<uint8_t, NGTCP2_STATELESS_RESET_TOKENLEN> token; + ngtcp2_cid cid; + + ngtcp2_cid_init(&cid, dcid, dcidlen); + + auto conn_handler = worker_->get_connection_handler(); + auto &qkms = conn_handler->get_quic_keying_materials(); + auto &qkm = qkms->keying_materials.front(); + + rv = generate_quic_stateless_reset_token(token.data(), cid, qkm.secret.data(), + qkm.secret.size()); + if (rv != 0) { + return -1; + } + + std::array<uint8_t, NGTCP2_MIN_STATELESS_RESET_RANDLEN> rand_bytes; + + if (RAND_bytes(rand_bytes.data(), rand_bytes.size()) != 1) { + return -1; + } + + std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf; + + auto nwrite = + ngtcp2_pkt_write_stateless_reset(buf.data(), buf.size(), token.data(), + rand_bytes.data(), rand_bytes.size()); + if (nwrite < 0) { + LOG(ERROR) << "ngtcp2_pkt_write_stateless_reset: " + << ngtcp2_strerror(nwrite); + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Send stateless_reset to remote=" + << util::to_numeric_addr(&remote_addr) + << " dcid=" << util::format_hex(dcid, dcidlen); + } + + return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, + &local_addr.su.sa, local_addr.len, ngtcp2_pkt_info{}, + buf.data(), nwrite, 0); +} + +int QUICConnectionHandler::send_connection_close( + const UpstreamAddr *faddr, uint32_t version, const ngtcp2_cid &ini_dcid, + const ngtcp2_cid &ini_scid, const Address &remote_addr, + const Address &local_addr, uint64_t error_code, size_t max_pktlen) { + std::array<uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> buf; + + max_pktlen = std::min(max_pktlen, buf.size()); + + auto nwrite = ngtcp2_crypto_write_connection_close( + buf.data(), max_pktlen, version, &ini_scid, &ini_dcid, error_code, + nullptr, 0); + if (nwrite < 0) { + LOG(ERROR) << "ngtcp2_crypto_write_connection_close failed"; + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Send Initial CONNECTION_CLOSE with error_code=" << log::hex + << error_code << log::dec + << " to remote=" << util::to_numeric_addr(&remote_addr) + << " dcid=" << util::format_hex(ini_scid.data, ini_scid.datalen) + << " scid=" << util::format_hex(ini_dcid.data, ini_dcid.datalen); + } + + return quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, + &local_addr.su.sa, local_addr.len, ngtcp2_pkt_info{}, + buf.data(), nwrite, 0); +} + +void QUICConnectionHandler::add_connection_id(const ngtcp2_cid &cid, + ClientHandler *handler) { + connections_.emplace(cid, handler); +} + +void QUICConnectionHandler::remove_connection_id(const ngtcp2_cid &cid) { + connections_.erase(cid); +} + +void QUICConnectionHandler::add_close_wait(CloseWait *cw) { + for (auto &cid : cw->scids) { + close_waits_.emplace(cid, cw); + } +} + +void QUICConnectionHandler::remove_close_wait(const CloseWait *cw) { + for (auto &cid : cw->scids) { + close_waits_.erase(cid); + } +} + +void QUICConnectionHandler::on_stateless_reset_bucket_regen() { + assert(stateless_reset_bucket_ < SHRPX_QUIC_STATELESS_RESET_BURST); + + if (++stateless_reset_bucket_ == SHRPX_QUIC_STATELESS_RESET_BURST) { + ev_timer_stop(worker_->get_loop(), &stateless_reset_bucket_regen_timer_); + } +} + +static void close_wait_timeoutcb(struct ev_loop *loop, ev_timer *w, + int revents) { + auto cw = static_cast<CloseWait *>(w->data); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "close-wait period finished"; + } + + auto quic_conn_handler = cw->worker->get_quic_connection_handler(); + quic_conn_handler->remove_close_wait(cw); + + delete cw; +} + +CloseWait::CloseWait(Worker *worker, std::vector<ngtcp2_cid> scids, + std::vector<uint8_t> pkt, ev_tstamp period) + : worker{worker}, + scids{std::move(scids)}, + pkt{std::move(pkt)}, + bytes_recv{0}, + bytes_sent{0}, + num_pkts_recv{0}, + next_pkts_recv{1} { + ++worker->get_worker_stat()->num_close_waits; + + ev_timer_init(&timer, close_wait_timeoutcb, period, 0.); + timer.data = this; + + ev_timer_start(worker->get_loop(), &timer); +} + +CloseWait::~CloseWait() { + auto loop = worker->get_loop(); + + ev_timer_stop(loop, &timer); + + auto worker_stat = worker->get_worker_stat(); + --worker_stat->num_close_waits; + + if (worker->get_graceful_shutdown() && worker_stat->num_connections == 0 && + worker_stat->num_close_waits == 0) { + ev_break(loop); + } +} + +int CloseWait::handle_packet(const UpstreamAddr *faddr, + const Address &remote_addr, + const Address &local_addr, + const ngtcp2_pkt_info &pi, const uint8_t *data, + size_t datalen) { + if (pkt.empty()) { + return 0; + } + + ++num_pkts_recv; + bytes_recv += datalen; + + if (bytes_sent + pkt.size() > 3 * bytes_recv || + next_pkts_recv > num_pkts_recv) { + return 0; + } + + if (quic_send_packet(faddr, &remote_addr.su.sa, remote_addr.len, + &local_addr.su.sa, local_addr.len, ngtcp2_pkt_info{}, + pkt.data(), pkt.size(), 0) != 0) { + return -1; + } + + next_pkts_recv *= 2; + bytes_sent += pkt.size(); + + return 0; +} + +} // namespace shrpx diff --git a/src/shrpx_quic_connection_handler.h b/src/shrpx_quic_connection_handler.h new file mode 100644 index 0000000..29e73a4 --- /dev/null +++ b/src/shrpx_quic_connection_handler.h @@ -0,0 +1,142 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_QUIC_CONNECTION_HANDLER_H +#define SHRPX_QUIC_CONNECTION_HANDLER_H + +#include "shrpx.h" + +#include <memory> +#include <unordered_map> +#include <string> +#include <vector> + +#include <ngtcp2/ngtcp2.h> + +#include <ev.h> + +#include "shrpx_quic.h" +#include "network.h" + +using namespace nghttp2; + +namespace shrpx { + +struct UpstreamAddr; +class ClientHandler; +class Worker; + +// CloseWait handles packets received in close-wait (draining or +// closing period). +struct CloseWait { + CloseWait(Worker *worker, std::vector<ngtcp2_cid> scids, + std::vector<uint8_t> pkt, ev_tstamp period); + ~CloseWait(); + + int handle_packet(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen); + + Worker *worker; + // Source Connection IDs of the connection. + std::vector<ngtcp2_cid> scids; + // QUIC packet which is sent in response to the incoming packet. It + // might be empty. + std::vector<uint8_t> pkt; + // Close-wait (draining or closing period) timer. + ev_timer timer; + // The number of bytes received during close-wait period. + size_t bytes_recv; + // The number of bytes sent during close-wait period. + size_t bytes_sent; + // The number of packets received during close-wait period. + size_t num_pkts_recv; + // If the number of packets received reaches this number, send a + // QUIC packet. + size_t next_pkts_recv; +}; + +class QUICConnectionHandler { +public: + QUICConnectionHandler(Worker *worker); + ~QUICConnectionHandler(); + int handle_packet(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen); + // Send Retry packet. |ini_dcid| is the destination Connection ID + // which appeared in Client Initial packet and its length is + // |dcidlen|. |ini_scid| is the source Connection ID which appeared + // in Client Initial packet and its length is |scidlen|. + int send_retry(const UpstreamAddr *faddr, uint32_t version, + const uint8_t *ini_dcid, size_t ini_dcidlen, + const uint8_t *ini_scid, size_t ini_scidlen, + const Address &remote_addr, const Address &local_addr, + size_t max_pktlen); + // Send Version Negotiation packet. |ini_dcid| is the destination + // Connection ID which appeared in Client Initial packet and its + // length is |dcidlen|. |ini_scid| is the source Connection ID + // which appeared in Client Initial packet and its length is + // |scidlen|. + int send_version_negotiation(const UpstreamAddr *faddr, uint32_t version, + const uint8_t *ini_dcid, size_t ini_dcidlen, + const uint8_t *ini_scid, size_t ini_scidlen, + const Address &remote_addr, + const Address &local_addr); + int send_stateless_reset(const UpstreamAddr *faddr, const uint8_t *dcid, + size_t dcidlen, const Address &remote_addr, + const Address &local_addr); + // Send Initial CONNECTION_CLOSE. |ini_dcid| is the destination + // Connection ID which appeared in Client Initial packet. + // |ini_scid| is the source Connection ID which appeared in Client + // Initial packet. + int send_connection_close(const UpstreamAddr *faddr, uint32_t version, + const ngtcp2_cid &ini_dcid, + const ngtcp2_cid &ini_scid, + const Address &remote_addr, + const Address &local_addr, uint64_t error_code, + size_t max_pktlen); + ClientHandler * + handle_new_connection(const UpstreamAddr *faddr, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_hd &hd, + const ngtcp2_cid *odcid, const uint8_t *token, + size_t tokenlen, ngtcp2_token_type token_type); + void add_connection_id(const ngtcp2_cid &cid, ClientHandler *handler); + void remove_connection_id(const ngtcp2_cid &cid); + + void add_close_wait(CloseWait *cw); + void remove_close_wait(const CloseWait *cw); + + void on_stateless_reset_bucket_regen(); + +private: + Worker *worker_; + std::unordered_map<ngtcp2_cid, ClientHandler *> connections_; + std::unordered_map<ngtcp2_cid, CloseWait *> close_waits_; + ev_timer stateless_reset_bucket_regen_timer_; + size_t stateless_reset_bucket_; +}; + +} // namespace shrpx + +#endif // SHRPX_QUIC_CONNECTION_HANDLER_H diff --git a/src/shrpx_quic_listener.cc b/src/shrpx_quic_listener.cc new file mode 100644 index 0000000..9b9f120 --- /dev/null +++ b/src/shrpx_quic_listener.cc @@ -0,0 +1,132 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_quic_listener.h" +#include "shrpx_worker.h" +#include "shrpx_config.h" +#include "shrpx_log.h" + +namespace shrpx { + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revent) { + auto l = static_cast<QUICListener *>(w->data); + l->on_read(); +} +} // namespace + +QUICListener::QUICListener(const UpstreamAddr *faddr, Worker *worker) + : faddr_{faddr}, worker_{worker} { + ev_io_init(&rev_, readcb, faddr_->fd, EV_READ); + rev_.data = this; + ev_io_start(worker_->get_loop(), &rev_); +} + +QUICListener::~QUICListener() { + ev_io_stop(worker_->get_loop(), &rev_); + close(faddr_->fd); +} + +void QUICListener::on_read() { + sockaddr_union su; + std::array<uint8_t, 64_k> buf; + size_t pktcnt = 0; + iovec msg_iov{buf.data(), buf.size()}; + + msghdr msg{}; + msg.msg_name = &su; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + + uint8_t msg_ctrl[CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(in6_pktinfo)) + + CMSG_SPACE(sizeof(uint16_t))]; + msg.msg_control = msg_ctrl; + + auto quic_conn_handler = worker_->get_quic_connection_handler(); + + for (; pktcnt < 10;) { + msg.msg_namelen = sizeof(su); + msg.msg_controllen = sizeof(msg_ctrl); + + auto nread = recvmsg(faddr_->fd, &msg, 0); + if (nread == -1) { + return; + } + + Address local_addr{}; + if (util::msghdr_get_local_addr(local_addr, &msg, su.storage.ss_family) != + 0) { + ++pktcnt; + + continue; + } + + util::set_port(local_addr, faddr_->port); + + ngtcp2_pkt_info pi{ + .ecn = util::msghdr_get_ecn(&msg, su.storage.ss_family), + }; + + auto gso_size = util::msghdr_get_udp_gro(&msg); + if (gso_size == 0) { + gso_size = static_cast<size_t>(nread); + } + + auto data = buf.data(); + + for (;;) { + auto datalen = std::min(static_cast<size_t>(nread), gso_size); + + ++pktcnt; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "QUIC received packet: local=" + << util::to_numeric_addr(&local_addr) << " remote=" + << util::to_numeric_addr(&su.sa, msg.msg_namelen) + << " ecn=" << log::hex << pi.ecn << log::dec << " " << datalen + << " bytes"; + } + + if (datalen == 0) { + break; + } + + Address remote_addr; + remote_addr.su = su; + remote_addr.len = msg.msg_namelen; + + quic_conn_handler->handle_packet(faddr_, remote_addr, local_addr, pi, + data, datalen); + + nread -= datalen; + if (nread == 0) { + break; + } + + data += datalen; + } + } +} + +} // namespace shrpx diff --git a/src/shrpx_quic_listener.h b/src/shrpx_quic_listener.h new file mode 100644 index 0000000..3d70921 --- /dev/null +++ b/src/shrpx_quic_listener.h @@ -0,0 +1,51 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2021 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_QUIC_LISTENER_H +#define SHRPX_QUIC_LISTENER_H + +#include "shrpx.h" + +#include <ev.h> + +namespace shrpx { + +struct UpstreamAddr; +class Worker; + +class QUICListener { +public: + QUICListener(const UpstreamAddr *faddr, Worker *worker); + ~QUICListener(); + void on_read(); + +private: + const UpstreamAddr *faddr_; + Worker *worker_; + ev_io rev_; +}; + +} // namespace shrpx + +#endif // SHRPX_QUIC_LISTENER_H diff --git a/src/shrpx_rate_limit.cc b/src/shrpx_rate_limit.cc new file mode 100644 index 0000000..0d4f921 --- /dev/null +++ b/src/shrpx_rate_limit.cc @@ -0,0 +1,123 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_rate_limit.h" + +#include <limits> + +#include "shrpx_connection.h" +#include "shrpx_log.h" + +namespace shrpx { + +namespace { +void regencb(struct ev_loop *loop, ev_timer *w, int revents) { + auto r = static_cast<RateLimit *>(w->data); + r->regen(); +} +} // namespace + +RateLimit::RateLimit(struct ev_loop *loop, ev_io *w, size_t rate, size_t burst, + Connection *conn) + : w_(w), + loop_(loop), + conn_(conn), + rate_(rate), + burst_(burst), + avail_(burst), + startw_req_(false) { + ev_timer_init(&t_, regencb, 0., 1.); + t_.data = this; + if (rate_ > 0) { + ev_timer_again(loop_, &t_); + } +} + +RateLimit::~RateLimit() { ev_timer_stop(loop_, &t_); } + +size_t RateLimit::avail() const { + if (rate_ == 0) { + return std::numeric_limits<ssize_t>::max(); + } + return avail_; +} + +void RateLimit::drain(size_t n) { + if (rate_ == 0) { + return; + } + n = std::min(avail_, n); + avail_ -= n; + if (avail_ == 0) { + ev_io_stop(loop_, w_); + } +} + +void RateLimit::regen() { + if (rate_ == 0) { + return; + } + if (avail_ + rate_ > burst_) { + avail_ = burst_; + } else { + avail_ += rate_; + } + + if (w_->fd >= 0 && avail_ > 0 && startw_req_) { + ev_io_start(loop_, w_); + handle_tls_pending_read(); + } +} + +void RateLimit::startw() { + if (w_->fd < 0) { + return; + } + startw_req_ = true; + if (rate_ == 0 || avail_ > 0) { + ev_io_start(loop_, w_); + handle_tls_pending_read(); + return; + } +} + +void RateLimit::stopw() { + startw_req_ = false; + ev_io_stop(loop_, w_); +} + +void RateLimit::handle_tls_pending_read() { + if (!conn_ || !conn_->tls.ssl || + (SSL_pending(conn_->tls.ssl) == 0 && conn_->tls.rbuf.rleft() == 0 && + (!conn_->tls.initial_handshake_done || + conn_->tls.earlybuf.rleft() == 0))) { + return; + } + + // Note that ev_feed_event works without starting watcher, but we + // only call this function if watcher is active. + ev_feed_event(loop_, w_, EV_READ); +} + +} // namespace shrpx diff --git a/src/shrpx_rate_limit.h b/src/shrpx_rate_limit.h new file mode 100644 index 0000000..7502a27 --- /dev/null +++ b/src/shrpx_rate_limit.h @@ -0,0 +1,68 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_RATE_LIMIT_H +#define SHRPX_RATE_LIMIT_H + +#include "shrpx.h" + +#include <ev.h> + +#include <openssl/ssl.h> + +namespace shrpx { + +struct Connection; + +class RateLimit { +public: + // We need |conn| object to check that it has unread bytes for TLS + // connection. + RateLimit(struct ev_loop *loop, ev_io *w, size_t rate, size_t burst, + Connection *conn = nullptr); + ~RateLimit(); + size_t avail() const; + void drain(size_t n); + void regen(); + void startw(); + void stopw(); + // Feeds event if conn_->tls object has unread bytes. This is + // required since it is buffered in conn_->tls object, io event is + // not generated unless new incoming data is received. + void handle_tls_pending_read(); + +private: + ev_timer t_; + ev_io *w_; + struct ev_loop *loop_; + Connection *conn_; + size_t rate_; + size_t burst_; + size_t avail_; + bool startw_req_; +}; + +} // namespace shrpx + +#endif // SHRPX_RATE_LIMIT_H diff --git a/src/shrpx_router.cc b/src/shrpx_router.cc new file mode 100644 index 0000000..d3565db --- /dev/null +++ b/src/shrpx_router.cc @@ -0,0 +1,420 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_router.h" + +#include <algorithm> + +#include "shrpx_config.h" +#include "shrpx_log.h" + +namespace shrpx { + +RNode::RNode() : s(nullptr), len(0), index(-1), wildcard_index(-1) {} + +RNode::RNode(const char *s, size_t len, ssize_t index, ssize_t wildcard_index) + : s(s), len(len), index(index), wildcard_index(wildcard_index) {} + +Router::Router() : balloc_(1024, 1024), root_{} {} + +Router::~Router() {} + +namespace { +RNode *find_next_node(const RNode *node, char c) { + auto itr = std::lower_bound(std::begin(node->next), std::end(node->next), c, + [](const std::unique_ptr<RNode> &lhs, + const char c) { return lhs->s[0] < c; }); + if (itr == std::end(node->next) || (*itr)->s[0] != c) { + return nullptr; + } + + return (*itr).get(); +} +} // namespace + +namespace { +void add_next_node(RNode *node, std::unique_ptr<RNode> new_node) { + auto itr = std::lower_bound(std::begin(node->next), std::end(node->next), + new_node->s[0], + [](const std::unique_ptr<RNode> &lhs, + const char c) { return lhs->s[0] < c; }); + node->next.insert(itr, std::move(new_node)); +} +} // namespace + +void Router::add_node(RNode *node, const char *pattern, size_t patlen, + ssize_t index, ssize_t wildcard_index) { + auto pat = make_string_ref(balloc_, StringRef{pattern, patlen}); + auto new_node = + std::make_unique<RNode>(pat.c_str(), pat.size(), index, wildcard_index); + add_next_node(node, std::move(new_node)); +} + +size_t Router::add_route(const StringRef &pattern, size_t idx, bool wildcard) { + ssize_t index = -1, wildcard_index = -1; + if (wildcard) { + wildcard_index = idx; + } else { + index = idx; + } + + auto node = &root_; + size_t i = 0; + + for (;;) { + auto next_node = find_next_node(node, pattern[i]); + if (next_node == nullptr) { + add_node(node, pattern.c_str() + i, pattern.size() - i, index, + wildcard_index); + return idx; + } + + node = next_node; + + auto slen = pattern.size() - i; + auto s = pattern.c_str() + i; + auto n = std::min(node->len, slen); + size_t j; + for (j = 0; j < n && node->s[j] == s[j]; ++j) + ; + if (j == n) { + // The common prefix was matched + if (slen == node->len) { + // Complete match + if (index != -1) { + if (node->index != -1) { + // Return the existing index for duplicates. + return node->index; + } + node->index = index; + return idx; + } + + assert(wildcard_index != -1); + + if (node->wildcard_index != -1) { + return node->wildcard_index; + } + node->wildcard_index = wildcard_index; + return idx; + } + + if (slen > node->len) { + // We still have pattern to add + i += j; + + continue; + } + } + + if (node->len > j) { + // node must be split into 2 nodes. new_node is now the child + // of node. + auto new_node = std::make_unique<RNode>( + &node->s[j], node->len - j, node->index, node->wildcard_index); + std::swap(node->next, new_node->next); + + node->len = j; + node->index = -1; + node->wildcard_index = -1; + + add_next_node(node, std::move(new_node)); + + if (slen == j) { + node->index = index; + node->wildcard_index = wildcard_index; + return idx; + } + } + + i += j; + + assert(pattern.size() > i); + add_node(node, pattern.c_str() + i, pattern.size() - i, index, + wildcard_index); + + return idx; + } +} + +namespace { +const RNode *match_complete(size_t *offset, const RNode *node, + const char *first, const char *last) { + *offset = 0; + + if (first == last) { + return node; + } + + auto p = first; + + for (;;) { + auto next_node = find_next_node(node, *p); + if (next_node == nullptr) { + return nullptr; + } + + node = next_node; + + auto n = std::min(node->len, static_cast<size_t>(last - p)); + if (memcmp(node->s, p, n) != 0) { + return nullptr; + } + p += n; + if (p == last) { + *offset = n; + return node; + } + } +} +} // namespace + +namespace { +const RNode *match_partial(bool *pattern_is_wildcard, const RNode *node, + size_t offset, const char *first, const char *last) { + *pattern_is_wildcard = false; + + if (first == last) { + if (node->len == offset) { + return node; + } + return nullptr; + } + + auto p = first; + + const RNode *found_node = nullptr; + + if (offset > 0) { + auto n = std::min(node->len - offset, static_cast<size_t>(last - first)); + if (memcmp(node->s + offset, first, n) != 0) { + return nullptr; + } + + p += n; + + if (p == last) { + if (node->len == offset + n) { + if (node->index != -1) { + return node; + } + + // The last '/' handling, see below. + node = find_next_node(node, '/'); + if (node != nullptr && node->index != -1 && node->len == 1) { + return node; + } + + return nullptr; + } + + // The last '/' handling, see below. + if (node->index != -1 && offset + n + 1 == node->len && + node->s[node->len - 1] == '/') { + return node; + } + + return nullptr; + } + + if (node->wildcard_index != -1) { + found_node = node; + *pattern_is_wildcard = true; + } else if (node->index != -1 && node->s[node->len - 1] == '/') { + found_node = node; + *pattern_is_wildcard = false; + } + + assert(node->len == offset + n); + } + + for (;;) { + auto next_node = find_next_node(node, *p); + if (next_node == nullptr) { + return found_node; + } + + node = next_node; + + auto n = std::min(node->len, static_cast<size_t>(last - p)); + if (memcmp(node->s, p, n) != 0) { + return found_node; + } + + p += n; + + if (p == last) { + if (node->len == n) { + // Complete match with this node + if (node->index != -1) { + *pattern_is_wildcard = false; + return node; + } + + // The last '/' handling, see below. + node = find_next_node(node, '/'); + if (node != nullptr && node->index != -1 && node->len == 1) { + *pattern_is_wildcard = false; + return node; + } + + return found_node; + } + + // We allow match without trailing "/" at the end of pattern. + // So, if pattern ends with '/', and pattern and path matches + // without that slash, we consider they match to deal with + // request to the directory without trailing slash. That is if + // pattern is "/foo/" and path is "/foo", we consider they + // match. + if (node->index != -1 && n + 1 == node->len && node->s[n] == '/') { + *pattern_is_wildcard = false; + return node; + } + + return found_node; + } + + if (node->wildcard_index != -1) { + found_node = node; + *pattern_is_wildcard = true; + } else if (node->index != -1 && node->s[node->len - 1] == '/') { + // This is the case when pattern which ends with "/" is included + // in query. + found_node = node; + *pattern_is_wildcard = false; + } + + assert(node->len == n); + } +} +} // namespace + +ssize_t Router::match(const StringRef &host, const StringRef &path) const { + const RNode *node; + size_t offset; + + node = match_complete(&offset, &root_, std::begin(host), std::end(host)); + if (node == nullptr) { + return -1; + } + + bool pattern_is_wildcard; + node = match_partial(&pattern_is_wildcard, node, offset, std::begin(path), + std::end(path)); + if (node == nullptr || node == &root_) { + return -1; + } + + return pattern_is_wildcard ? node->wildcard_index : node->index; +} + +ssize_t Router::match(const StringRef &s) const { + const RNode *node; + size_t offset; + + node = match_complete(&offset, &root_, std::begin(s), std::end(s)); + if (node == nullptr) { + return -1; + } + + if (node->len != offset) { + return -1; + } + + return node->index; +} + +namespace { +const RNode *match_prefix(size_t *nread, const RNode *node, const char *first, + const char *last) { + if (first == last) { + return nullptr; + } + + auto p = first; + + for (;;) { + auto next_node = find_next_node(node, *p); + if (next_node == nullptr) { + return nullptr; + } + + node = next_node; + + auto n = std::min(node->len, static_cast<size_t>(last - p)); + if (memcmp(node->s, p, n) != 0) { + return nullptr; + } + + p += n; + + if (p != last) { + if (node->index != -1) { + *nread = p - first; + return node; + } + continue; + } + + if (node->len == n) { + *nread = p - first; + return node; + } + + return nullptr; + } +} +} // namespace + +ssize_t Router::match_prefix(size_t *nread, const RNode **last_node, + const StringRef &s) const { + if (*last_node == nullptr) { + *last_node = &root_; + } + + auto node = + ::shrpx::match_prefix(nread, *last_node, std::begin(s), std::end(s)); + if (node == nullptr) { + return -1; + } + + *last_node = node; + + return node->index; +} + +namespace { +void dump_node(const RNode *node, int depth) { + fprintf(stderr, "%*ss='%.*s', len=%zu, index=%zd\n", depth, "", + (int)node->len, node->s, node->len, node->index); + for (auto &nd : node->next) { + dump_node(nd.get(), depth + 4); + } +} +} // namespace + +void Router::dump() const { dump_node(&root_, 0); } + +} // namespace shrpx diff --git a/src/shrpx_router.h b/src/shrpx_router.h new file mode 100644 index 0000000..295db7e --- /dev/null +++ b/src/shrpx_router.h @@ -0,0 +1,110 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_ROUTER_H +#define SHRPX_ROUTER_H + +#include "shrpx.h" + +#include <vector> +#include <memory> + +#include "allocator.h" + +using namespace nghttp2; + +namespace shrpx { + +struct RNode { + RNode(); + RNode(const char *s, size_t len, ssize_t index, ssize_t wildcard_index); + RNode(RNode &&) = default; + RNode(const RNode &) = delete; + RNode &operator=(RNode &&) = default; + RNode &operator=(const RNode &) = delete; + + // Next RNode, sorted by s[0]. + std::vector<std::unique_ptr<RNode>> next; + // Stores pointer to the string this node represents. Not + // NULL-terminated. + const char *s; + // Length of |s| + size_t len; + // Index of pattern if match ends in this node. Note that we don't + // store duplicated pattern. + ssize_t index; + // Index of wildcard pattern if query includes this node as prefix + // and it still has suffix to match. Note that we don't store + // duplicated pattern. + ssize_t wildcard_index; +}; + +class Router { +public: + Router(); + ~Router(); + Router(Router &&) = default; + Router(const Router &) = delete; + Router &operator=(Router &&) = default; + Router &operator=(const Router &) = delete; + + // Adds route |pattern| with its |index|. If same pattern has + // already been added, the existing index is returned. If + // |wildcard| is true, |pattern| is considered as wildcard pattern, + // and all paths which have the |pattern| as prefix and are strictly + // longer than |pattern| match. The wildcard pattern only works + // with match(const StringRef&, const StringRef&). + size_t add_route(const StringRef &pattern, size_t index, + bool wildcard = false); + // Returns the matched index of pattern. -1 if there is no match. + ssize_t match(const StringRef &host, const StringRef &path) const; + // Returns the matched index of pattern |s|. -1 if there is no + // match. + ssize_t match(const StringRef &s) const; + // Returns the matched index of pattern if a pattern is a suffix of + // |s|, otherwise -1. If |*last_node| is not nullptr, it specifies + // the first node to start matching. If it is nullptr, match will + // start from scratch. When the match was found (the return value + // is not -1), |*nread| has the number of bytes matched in |s|, and + // |*last_node| has the last matched node. One can continue to + // match the longer pattern using the returned |*last_node| to the + // another invocation of this function until it returns -1. + ssize_t match_prefix(size_t *nread, const RNode **last_node, + const StringRef &s) const; + + void add_node(RNode *node, const char *pattern, size_t patlen, ssize_t index, + ssize_t wildcard_index); + + void dump() const; + +private: + BlockAllocator balloc_; + // The root node of Patricia tree. This is special node and its s + // field is nulptr, and len field is 0. + RNode root_; +}; + +} // namespace shrpx + +#endif // SHRPX_ROUTER_H diff --git a/src/shrpx_router_test.cc b/src/shrpx_router_test.cc new file mode 100644 index 0000000..21c2f51 --- /dev/null +++ b/src/shrpx_router_test.cc @@ -0,0 +1,184 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_router_test.h" + +#include <CUnit/CUnit.h> + +#include "shrpx_router.h" + +namespace shrpx { + +struct Pattern { + StringRef pattern; + size_t idx; + bool wildcard; +}; + +void test_shrpx_router_match(void) { + auto patterns = std::vector<Pattern>{ + {StringRef::from_lit("nghttp2.org/"), 0}, + {StringRef::from_lit("nghttp2.org/alpha"), 1}, + {StringRef::from_lit("nghttp2.org/alpha/"), 2}, + {StringRef::from_lit("nghttp2.org/alpha/bravo/"), 3}, + {StringRef::from_lit("www.nghttp2.org/alpha/"), 4}, + {StringRef::from_lit("/alpha"), 5}, + {StringRef::from_lit("example.com/alpha/"), 6}, + {StringRef::from_lit("nghttp2.org/alpha/bravo2/"), 7}, + {StringRef::from_lit("www2.nghttp2.org/alpha/"), 8}, + {StringRef::from_lit("www2.nghttp2.org/alpha2/"), 9}, + }; + + Router router; + + for (auto &p : patterns) { + router.add_route(p.pattern, p.idx); + } + + ssize_t idx; + + idx = router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/")); + + CU_ASSERT(0 == idx); + + idx = router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha")); + + CU_ASSERT(1 == idx); + + idx = router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/")); + + CU_ASSERT(2 == idx); + + idx = router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/charlie")); + + CU_ASSERT(2 == idx); + + idx = router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/bravo/")); + + CU_ASSERT(3 == idx); + + // matches pattern when last '/' is missing in path + idx = router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/bravo")); + + CU_ASSERT(3 == idx); + + idx = router.match(StringRef::from_lit("www2.nghttp2.org"), + StringRef::from_lit("/alpha")); + + CU_ASSERT(8 == idx); + + idx = router.match(StringRef{}, StringRef::from_lit("/alpha")); + + CU_ASSERT(5 == idx); +} + +void test_shrpx_router_match_wildcard(void) { + constexpr auto patterns = std::array<Pattern, 6>{{ + {StringRef::from_lit("nghttp2.org/"), 0}, + {StringRef::from_lit("nghttp2.org/"), 1, true}, + {StringRef::from_lit("nghttp2.org/alpha/"), 2}, + {StringRef::from_lit("nghttp2.org/alpha/"), 3, true}, + {StringRef::from_lit("nghttp2.org/bravo"), 4}, + {StringRef::from_lit("nghttp2.org/bravo"), 5, true}, + }}; + + Router router; + + for (auto &p : patterns) { + router.add_route(p.pattern, p.idx, p.wildcard); + } + + CU_ASSERT(0 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/"))); + + CU_ASSERT(1 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/a"))); + + CU_ASSERT(1 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/charlie"))); + + CU_ASSERT(2 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha"))); + + CU_ASSERT(2 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/"))); + + CU_ASSERT(3 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/b"))); + + CU_ASSERT(4 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/bravo"))); + + CU_ASSERT(5 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/bravocharlie"))); + + CU_ASSERT(5 == router.match(StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/bravo/"))); +} + +void test_shrpx_router_match_prefix(void) { + auto patterns = std::vector<Pattern>{ + {StringRef::from_lit("gro.2ptthgn."), 0}, + {StringRef::from_lit("gro.2ptthgn.www."), 1}, + {StringRef::from_lit("gro.2ptthgn.gmi."), 2}, + {StringRef::from_lit("gro.2ptthgn.gmi.ahpla."), 3}, + }; + + Router router; + + for (auto &p : patterns) { + router.add_route(p.pattern, p.idx); + } + + ssize_t idx; + const RNode *node; + size_t nread; + + node = nullptr; + + idx = router.match_prefix(&nread, &node, + StringRef::from_lit("gro.2ptthgn.gmi.ahpla.ovarb")); + + CU_ASSERT(0 == idx); + CU_ASSERT(12 == nread); + + idx = router.match_prefix(&nread, &node, + StringRef::from_lit("gmi.ahpla.ovarb")); + + CU_ASSERT(2 == idx); + CU_ASSERT(4 == nread); + + idx = router.match_prefix(&nread, &node, StringRef::from_lit("ahpla.ovarb")); + + CU_ASSERT(3 == idx); + CU_ASSERT(6 == nread); +} + +} // namespace shrpx diff --git a/src/shrpx_router_test.h b/src/shrpx_router_test.h new file mode 100644 index 0000000..d39cb87 --- /dev/null +++ b/src/shrpx_router_test.h @@ -0,0 +1,40 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_ROUTER_TEST_H +#define SHRPX_ROUTER_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_shrpx_router_match(void); +void test_shrpx_router_match_wildcard(void); +void test_shrpx_router_match_prefix(void); + +} // namespace shrpx + +#endif // SHRPX_ROUTER_TEST_H diff --git a/src/shrpx_signal.cc b/src/shrpx_signal.cc new file mode 100644 index 0000000..63fcc07 --- /dev/null +++ b/src/shrpx_signal.cc @@ -0,0 +1,138 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_signal.h" + +#include <cerrno> + +#include "shrpx_log.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +int shrpx_signal_block_all(sigset_t *oldset) { + sigset_t newset; + + sigfillset(&newset); + +#ifndef NOTHREADS + int rv; + + rv = pthread_sigmask(SIG_SETMASK, &newset, oldset); + + if (rv != 0) { + errno = rv; + return -1; + } + + return 0; +#else // NOTHREADS + return sigprocmask(SIG_SETMASK, &newset, oldset); +#endif // NOTHREADS +} + +int shrpx_signal_unblock_all() { + sigset_t newset; + + sigemptyset(&newset); + +#ifndef NOTHREADS + int rv; + + rv = pthread_sigmask(SIG_SETMASK, &newset, nullptr); + + if (rv != 0) { + errno = rv; + return -1; + } + + return 0; +#else // NOTHREADS + return sigprocmask(SIG_SETMASK, &newset, nullptr); +#endif // NOTHREADS +} + +int shrpx_signal_set(sigset_t *set) { +#ifndef NOTHREADS + int rv; + + rv = pthread_sigmask(SIG_SETMASK, set, nullptr); + + if (rv != 0) { + errno = rv; + return -1; + } + + return 0; +#else // NOTHREADS + return sigprocmask(SIG_SETMASK, set, nullptr); +#endif // NOTHREADS +} + +namespace { +template <typename Signals> +int signal_set_handler(void (*handler)(int), Signals &&sigs) { + struct sigaction act {}; + act.sa_handler = handler; + sigemptyset(&act.sa_mask); + int rv; + for (auto sig : sigs) { + rv = sigaction(sig, &act, nullptr); + if (rv != 0) { + return -1; + } + } + return 0; +} +} // namespace + +namespace { +constexpr auto main_proc_ign_signals = std::array<int, 1>{SIGPIPE}; +} // namespace + +namespace { +constexpr auto worker_proc_ign_signals = + std::array<int, 5>{REOPEN_LOG_SIGNAL, EXEC_BINARY_SIGNAL, + GRACEFUL_SHUTDOWN_SIGNAL, RELOAD_SIGNAL, SIGPIPE}; +} // namespace + +int shrpx_signal_set_main_proc_ign_handler() { + return signal_set_handler(SIG_IGN, main_proc_ign_signals); +} + +int shrpx_signal_unset_main_proc_ign_handler() { + return signal_set_handler(SIG_DFL, main_proc_ign_signals); +} + +int shrpx_signal_set_worker_proc_ign_handler() { + return signal_set_handler(SIG_IGN, worker_proc_ign_signals); +} + +int shrpx_signal_unset_worker_proc_ign_handler() { + return signal_set_handler(SIG_DFL, worker_proc_ign_signals); +} + +} // namespace shrpx diff --git a/src/shrpx_signal.h b/src/shrpx_signal.h new file mode 100644 index 0000000..152ca36 --- /dev/null +++ b/src/shrpx_signal.h @@ -0,0 +1,60 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_SIGNAL_H +#define SHRPX_SIGNAL_H + +#include "shrpx.h" + +#include <signal.h> + +namespace shrpx { + +constexpr int REOPEN_LOG_SIGNAL = SIGUSR1; +constexpr int EXEC_BINARY_SIGNAL = SIGUSR2; +constexpr int GRACEFUL_SHUTDOWN_SIGNAL = SIGQUIT; +constexpr int RELOAD_SIGNAL = SIGHUP; + +// Blocks all signals. The previous signal mask is stored into +// |oldset| if it is not nullptr. This function returns 0 if it +// succeeds, or -1. The errno will indicate the error. +int shrpx_signal_block_all(sigset_t *oldset); + +// Unblocks all signals. This function returns 0 if it succeeds, or +// -1. The errno will indicate the error. +int shrpx_signal_unblock_all(); + +// Sets signal mask |set|. This function returns 0 if it succeeds, or +// -1. The errno will indicate the error. +int shrpx_signal_set(sigset_t *set); + +int shrpx_signal_set_main_proc_ign_handler(); +int shrpx_signal_unset_main_proc_ign_handler(); + +int shrpx_signal_set_worker_proc_ign_handler(); +int shrpx_signal_unset_worker_proc_ign_handler(); + +} // namespace shrpx + +#endif // SHRPX_SIGNAL_H diff --git a/src/shrpx_tls.cc b/src/shrpx_tls.cc new file mode 100644 index 0000000..aa0c9f2 --- /dev/null +++ b/src/shrpx_tls.cc @@ -0,0 +1,2465 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_tls.h" + +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H +#include <netinet/tcp.h> +#include <pthread.h> +#include <sys/types.h> + +#include <vector> +#include <string> +#include <iomanip> + +#include <iostream> + +#include "ssl_compat.h" + +#include <openssl/crypto.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> +#include <openssl/rand.h> +#include <openssl/dh.h> +#ifndef OPENSSL_NO_OCSP +# include <openssl/ocsp.h> +#endif // OPENSSL_NO_OCSP +#if OPENSSL_3_0_0_API +# include <openssl/params.h> +# include <openssl/core_names.h> +# include <openssl/decoder.h> +#endif // OPENSSL_3_0_0_API +#ifdef NGHTTP2_OPENSSL_IS_BORINGSSL +# include <openssl/hmac.h> +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + +#include <nghttp2/nghttp2.h> + +#ifdef ENABLE_HTTP3 +# include <ngtcp2/ngtcp2.h> +# include <ngtcp2/ngtcp2_crypto.h> +# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# include <ngtcp2/ngtcp2_crypto_quictls.h> +# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +# include <ngtcp2/ngtcp2_crypto_boringssl.h> +# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +#endif // ENABLE_HTTP3 + +#include "shrpx_log.h" +#include "shrpx_client_handler.h" +#include "shrpx_config.h" +#include "shrpx_worker.h" +#include "shrpx_downstream_connection_pool.h" +#include "shrpx_http2_session.h" +#include "shrpx_memcached_request.h" +#include "shrpx_memcached_dispatcher.h" +#include "shrpx_connection_handler.h" +#ifdef ENABLE_HTTP3 +# include "shrpx_http3_upstream.h" +#endif // ENABLE_HTTP3 +#include "util.h" +#include "tls.h" +#include "template.h" +#include "timegm.h" + +using namespace nghttp2; +using namespace std::chrono_literals; + +namespace shrpx { + +namespace tls { + +namespace { +int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { + if (!preverify_ok) { + int err = X509_STORE_CTX_get_error(ctx); + int depth = X509_STORE_CTX_get_error_depth(ctx); + if (err == X509_V_ERR_CERT_HAS_EXPIRED && depth == 0 && + get_config()->tls.client_verify.tolerate_expired) { + LOG(INFO) << "The client certificate has expired, but is accepted by " + "configuration"; + return 1; + } + LOG(ERROR) << "client certificate verify error:num=" << err << ":" + << X509_verify_cert_error_string(err) << ":depth=" << depth; + } + return preverify_ok; +} +} // namespace + +int set_alpn_prefs(std::vector<unsigned char> &out, + const std::vector<StringRef> &protos) { + size_t len = 0; + + for (const auto &proto : protos) { + if (proto.size() > 255) { + LOG(FATAL) << "Too long ALPN identifier: " << proto.size(); + return -1; + } + + len += 1 + proto.size(); + } + + if (len > (1 << 16) - 1) { + LOG(FATAL) << "Too long ALPN identifier list: " << len; + return -1; + } + + out.resize(len); + auto ptr = out.data(); + + for (const auto &proto : protos) { + *ptr++ = proto.size(); + ptr = std::copy(std::begin(proto), std::end(proto), ptr); + } + + return 0; +} + +namespace { +int ssl_pem_passwd_cb(char *buf, int size, int rwflag, void *user_data) { + auto config = static_cast<Config *>(user_data); + auto len = static_cast<int>(config->tls.private_key_passwd.size()); + if (size < len + 1) { + LOG(ERROR) << "ssl_pem_passwd_cb: buf is too small " << size; + return 0; + } + // Copy string including last '\0'. + memcpy(buf, config->tls.private_key_passwd.c_str(), len + 1); + return len; +} +} // namespace + +namespace { +// *al is set to SSL_AD_UNRECOGNIZED_NAME by openssl, so we don't have +// to set it explicitly. +int servername_callback(SSL *ssl, int *al, void *arg) { + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + auto handler = static_cast<ClientHandler *>(conn->data); + auto worker = handler->get_worker(); + + auto rawhost = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + if (rawhost == nullptr) { + return SSL_TLSEXT_ERR_NOACK; + } + + auto len = strlen(rawhost); + // NI_MAXHOST includes terminal NULL. + if (len == 0 || len + 1 > NI_MAXHOST) { + return SSL_TLSEXT_ERR_NOACK; + } + + std::array<uint8_t, NI_MAXHOST> buf; + + auto end_buf = std::copy_n(rawhost, len, std::begin(buf)); + + util::inp_strlower(std::begin(buf), end_buf); + + auto hostname = StringRef{std::begin(buf), end_buf}; + +#ifdef ENABLE_HTTP3 + auto cert_tree = conn->proto == Proto::HTTP3 + ? worker->get_quic_cert_lookup_tree() + : worker->get_cert_lookup_tree(); +#else // !ENABLE_HTTP3 + auto cert_tree = worker->get_cert_lookup_tree(); +#endif // !ENABLE_HTTP3 + + auto idx = cert_tree->lookup(hostname); + if (idx == -1) { + return SSL_TLSEXT_ERR_NOACK; + } + + handler->set_tls_sni(hostname); + + auto conn_handler = worker->get_connection_handler(); + +#ifdef ENABLE_HTTP3 + const auto &ssl_ctx_list = conn->proto == Proto::HTTP3 + ? conn_handler->get_quic_indexed_ssl_ctx(idx) + : conn_handler->get_indexed_ssl_ctx(idx); +#else // !ENABLE_HTTP3 + const auto &ssl_ctx_list = conn_handler->get_indexed_ssl_ctx(idx); +#endif // !ENABLE_HTTP3 + + assert(!ssl_ctx_list.empty()); + +#ifdef NGHTTP2_GENUINE_OPENSSL + auto num_sigalgs = + SSL_get_sigalgs(ssl, 0, nullptr, nullptr, nullptr, nullptr, nullptr); + + for (idx = 0; idx < num_sigalgs; ++idx) { + int signhash; + + SSL_get_sigalgs(ssl, idx, nullptr, nullptr, &signhash, nullptr, nullptr); + switch (signhash) { + case NID_ecdsa_with_SHA256: + case NID_ecdsa_with_SHA384: + case NID_ecdsa_with_SHA512: + break; + default: + continue; + } + + break; + } + + if (idx == num_sigalgs) { + SSL_set_SSL_CTX(ssl, ssl_ctx_list[0]); + + return SSL_TLSEXT_ERR_OK; + } + + auto num_shared_curves = SSL_get_shared_curve(ssl, -1); + + for (auto i = 0; i < num_shared_curves; ++i) { + auto shared_curve = SSL_get_shared_curve(ssl, i); +# if OPENSSL_3_0_0_API + // It looks like only short name is defined in OpenSSL. No idea + // which one to use because it is unknown that which one + // EVP_PKEY_get_utf8_string_param("group") returns. + auto shared_curve_name = OBJ_nid2sn(shared_curve); + if (shared_curve_name == nullptr) { + continue; + } +# endif // OPENSSL_3_0_0_API + + for (auto ssl_ctx : ssl_ctx_list) { + auto cert = SSL_CTX_get0_certificate(ssl_ctx); + auto pubkey = X509_get0_pubkey(cert); + + if (EVP_PKEY_base_id(pubkey) != EVP_PKEY_EC) { + continue; + } + +# if OPENSSL_3_0_0_API + std::array<char, 64> curve_name; + if (!EVP_PKEY_get_utf8_string_param(pubkey, "group", curve_name.data(), + curve_name.size(), nullptr)) { + continue; + } + + if (strcmp(shared_curve_name, curve_name.data()) == 0) { + SSL_set_SSL_CTX(ssl, ssl_ctx); + return SSL_TLSEXT_ERR_OK; + } +# else // !OPENSSL_3_0_0_API + auto eckey = EVP_PKEY_get0_EC_KEY(pubkey); + if (eckey == nullptr) { + continue; + } + + auto ecgroup = EC_KEY_get0_group(eckey); + auto cert_curve = EC_GROUP_get_curve_name(ecgroup); + + if (shared_curve == cert_curve) { + SSL_set_SSL_CTX(ssl, ssl_ctx); + return SSL_TLSEXT_ERR_OK; + } +# endif // !OPENSSL_3_0_0_API + } + } +#endif // NGHTTP2_GENUINE_OPENSSL + + SSL_set_SSL_CTX(ssl, ssl_ctx_list[0]); + + return SSL_TLSEXT_ERR_OK; +} +} // namespace + +#ifndef NGHTTP2_OPENSSL_IS_BORINGSSL +namespace { +std::shared_ptr<std::vector<uint8_t>> +get_ocsp_data(TLSContextData *tls_ctx_data) { +# ifdef HAVE_ATOMIC_STD_SHARED_PTR + return std::atomic_load_explicit(&tls_ctx_data->ocsp_data, + std::memory_order_acquire); +# else // !HAVE_ATOMIC_STD_SHARED_PTR + std::lock_guard<std::mutex> g(tls_ctx_data->mu); + return tls_ctx_data->ocsp_data; +# endif // !HAVE_ATOMIC_STD_SHARED_PTR +} +} // namespace + +namespace { +int ocsp_resp_cb(SSL *ssl, void *arg) { + auto ssl_ctx = SSL_get_SSL_CTX(ssl); + auto tls_ctx_data = + static_cast<TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx)); + + auto data = get_ocsp_data(tls_ctx_data); + + if (!data) { + return SSL_TLSEXT_ERR_OK; + } + + auto buf = static_cast<uint8_t *>( + CRYPTO_malloc(data->size(), NGHTTP2_FILE_NAME, __LINE__)); + + if (!buf) { + return SSL_TLSEXT_ERR_OK; + } + + std::copy(std::begin(*data), std::end(*data), buf); + + SSL_set_tlsext_status_ocsp_resp(ssl, buf, data->size()); + + return SSL_TLSEXT_ERR_OK; +} +} // namespace +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + +constexpr auto MEMCACHED_SESSION_CACHE_KEY_PREFIX = + StringRef::from_lit("nghttpx:tls-session-cache:"); + +namespace { +int tls_session_client_new_cb(SSL *ssl, SSL_SESSION *session) { + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + if (conn->tls.client_session_cache == nullptr) { + return 0; + } + + try_cache_tls_session(conn->tls.client_session_cache, session, + std::chrono::steady_clock::now()); + + return 0; +} +} // namespace + +namespace { +int tls_session_new_cb(SSL *ssl, SSL_SESSION *session) { + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + auto handler = static_cast<ClientHandler *>(conn->data); + auto worker = handler->get_worker(); + auto dispatcher = worker->get_session_cache_memcached_dispatcher(); + auto &balloc = handler->get_block_allocator(); + +#ifdef TLS1_3_VERSION + if (SSL_version(ssl) == TLS1_3_VERSION) { + return 0; + } +#endif // TLS1_3_VERSION + + const unsigned char *id; + unsigned int idlen; + + id = SSL_SESSION_get_id(session, &idlen); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Memcached: cache session, id=" << util::format_hex(id, idlen); + } + + auto req = std::make_unique<MemcachedRequest>(); + req->op = MemcachedOp::ADD; + req->key = MEMCACHED_SESSION_CACHE_KEY_PREFIX.str(); + req->key += + util::format_hex(balloc, StringRef{id, static_cast<size_t>(idlen)}); + + auto sessionlen = i2d_SSL_SESSION(session, nullptr); + req->value.resize(sessionlen); + auto buf = &req->value[0]; + i2d_SSL_SESSION(session, &buf); + req->expiry = 12_h; + req->cb = [](MemcachedRequest *req, MemcachedResult res) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Memcached: session cache done. key=" << req->key + << ", status_code=" << static_cast<uint16_t>(res.status_code) + << ", value=" + << std::string(std::begin(res.value), std::end(res.value)); + } + if (res.status_code != MemcachedStatusCode::NO_ERROR) { + LOG(WARN) << "Memcached: failed to cache session key=" << req->key + << ", status_code=" << static_cast<uint16_t>(res.status_code) + << ", value=" + << std::string(std::begin(res.value), std::end(res.value)); + } + }; + assert(!req->canceled); + + dispatcher->add_request(std::move(req)); + + return 0; +} +} // namespace + +namespace { +SSL_SESSION *tls_session_get_cb(SSL *ssl, const unsigned char *id, int idlen, + int *copy) { + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + auto handler = static_cast<ClientHandler *>(conn->data); + auto worker = handler->get_worker(); + auto dispatcher = worker->get_session_cache_memcached_dispatcher(); + auto &balloc = handler->get_block_allocator(); + + if (idlen == 0) { + return nullptr; + } + + if (conn->tls.cached_session) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Memcached: found cached session, id=" + << util::format_hex(id, idlen); + } + + // This is required, without this, memory leak occurs. + *copy = 0; + + auto session = conn->tls.cached_session; + conn->tls.cached_session = nullptr; + return session; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Memcached: get cached session, id=" + << util::format_hex(id, idlen); + } + + auto req = std::make_unique<MemcachedRequest>(); + req->op = MemcachedOp::GET; + req->key = MEMCACHED_SESSION_CACHE_KEY_PREFIX.str(); + req->key += + util::format_hex(balloc, StringRef{id, static_cast<size_t>(idlen)}); + req->cb = [conn](MemcachedRequest *, MemcachedResult res) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Memcached: returned status code " + << static_cast<uint16_t>(res.status_code); + } + + // We might stop reading, so start it again + conn->rlimit.startw(); + ev_timer_again(conn->loop, &conn->rt); + + conn->wlimit.startw(); + ev_timer_again(conn->loop, &conn->wt); + + conn->tls.cached_session_lookup_req = nullptr; + if (res.status_code != MemcachedStatusCode::NO_ERROR) { + conn->tls.handshake_state = TLSHandshakeState::CANCEL_SESSION_CACHE; + return; + } + + const uint8_t *p = res.value.data(); + + auto session = d2i_SSL_SESSION(nullptr, &p, res.value.size()); + if (!session) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "cannot materialize session"; + } + conn->tls.handshake_state = TLSHandshakeState::CANCEL_SESSION_CACHE; + return; + } + + conn->tls.cached_session = session; + conn->tls.handshake_state = TLSHandshakeState::GOT_SESSION_CACHE; + }; + + conn->tls.handshake_state = TLSHandshakeState::WAIT_FOR_SESSION_CACHE; + conn->tls.cached_session_lookup_req = req.get(); + + dispatcher->add_request(std::move(req)); + + return nullptr; +} +} // namespace + +namespace { +int ticket_key_cb(SSL *ssl, unsigned char *key_name, unsigned char *iv, + EVP_CIPHER_CTX *ctx, +#if OPENSSL_3_0_0_API + EVP_MAC_CTX *hctx, +#else // !OPENSSL_3_0_0_API + HMAC_CTX *hctx, +#endif // !OPENSSL_3_0_0_API + int enc) { + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + auto handler = static_cast<ClientHandler *>(conn->data); + auto worker = handler->get_worker(); + auto ticket_keys = worker->get_ticket_keys(); + + if (!ticket_keys) { + // No ticket keys available. + return -1; + } + + auto &keys = ticket_keys->keys; + assert(!keys.empty()); + + if (enc) { + if (RAND_bytes(iv, EVP_MAX_IV_LENGTH) == 0) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "session ticket key: RAND_bytes failed"; + } + return -1; + } + + auto &key = keys[0]; + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "encrypt session ticket key: " + << util::format_hex(key.data.name); + } + + std::copy(std::begin(key.data.name), std::end(key.data.name), key_name); + + EVP_EncryptInit_ex(ctx, get_config()->tls.ticket.cipher, nullptr, + key.data.enc_key.data(), iv); +#if OPENSSL_3_0_0_API + std::array<OSSL_PARAM, 3> params{ + OSSL_PARAM_construct_octet_string( + OSSL_MAC_PARAM_KEY, key.data.hmac_key.data(), key.hmac_keylen), + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, + const_cast<char *>(EVP_MD_get0_name(key.hmac)), 0), + OSSL_PARAM_construct_end(), + }; + if (!EVP_MAC_CTX_set_params(hctx, params.data())) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "EVP_MAC_CTX_set_params failed"; + } + return -1; + } +#else // !OPENSSL_3_0_0_API + HMAC_Init_ex(hctx, key.data.hmac_key.data(), key.hmac_keylen, key.hmac, + nullptr); +#endif // !OPENSSL_3_0_0_API + return 1; + } + + size_t i; + for (i = 0; i < keys.size(); ++i) { + auto &key = keys[i]; + if (std::equal(std::begin(key.data.name), std::end(key.data.name), + key_name)) { + break; + } + } + + if (i == keys.size()) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "session ticket key " + << util::format_hex(key_name, 16) << " not found"; + } + return 0; + } + + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "decrypt session ticket key: " + << util::format_hex(key_name, 16); + } + + auto &key = keys[i]; +#if OPENSSL_3_0_0_API + std::array<OSSL_PARAM, 3> params{ + OSSL_PARAM_construct_octet_string( + OSSL_MAC_PARAM_KEY, key.data.hmac_key.data(), key.hmac_keylen), + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, const_cast<char *>(EVP_MD_get0_name(key.hmac)), + 0), + OSSL_PARAM_construct_end(), + }; + if (!EVP_MAC_CTX_set_params(hctx, params.data())) { + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "EVP_MAC_CTX_set_params failed"; + } + return -1; + } +#else // !OPENSSL_3_0_0_API + HMAC_Init_ex(hctx, key.data.hmac_key.data(), key.hmac_keylen, key.hmac, + nullptr); +#endif // !OPENSSL_3_0_0_API + EVP_DecryptInit_ex(ctx, key.cipher, nullptr, key.data.enc_key.data(), iv); + +#ifdef TLS1_3_VERSION + // If ticket_key_cb is not set, OpenSSL always renew ticket for + // TLSv1.3. + if (SSL_version(ssl) == TLS1_3_VERSION) { + return 2; + } +#endif // TLS1_3_VERSION + + return i == 0 ? 1 : 2; +} +} // namespace + +namespace { +void info_callback(const SSL *ssl, int where, int ret) { +#ifdef TLS1_3_VERSION + // TLSv1.3 has no renegotiation. + if (SSL_version(ssl) == TLS1_3_VERSION) { + return; + } +#endif // TLS1_3_VERSION + + // To mitigate possible DOS attack using lots of renegotiations, we + // disable renegotiation. Since OpenSSL does not provide an easy way + // to disable it, we check that renegotiation is started in this + // callback. + if (where & SSL_CB_HANDSHAKE_START) { + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + if (conn && conn->tls.initial_handshake_done) { + auto handler = static_cast<ClientHandler *>(conn->data); + if (LOG_ENABLED(INFO)) { + CLOG(INFO, handler) << "TLS renegotiation started"; + } + handler->start_immediate_shutdown(); + } + } +} +} // namespace + +namespace { +int alpn_select_proto_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + // We assume that get_config()->alpn_list contains ALPN protocol + // identifier sorted by preference order. So we just break when we + // found the first overlap. + for (const auto &target_proto_id : get_config()->tls.alpn_list) { + for (auto p = in, end = in + inlen; p < end;) { + auto proto_id = p + 1; + auto proto_len = *p; + + if (proto_id + proto_len <= end && + util::streq(target_proto_id, StringRef{proto_id, proto_len})) { + + *out = reinterpret_cast<const unsigned char *>(proto_id); + *outlen = proto_len; + + return SSL_TLSEXT_ERR_OK; + } + + p += 1 + proto_len; + } + } + + return SSL_TLSEXT_ERR_NOACK; +} +} // namespace + +#ifdef ENABLE_HTTP3 +namespace { +int quic_alpn_select_proto_cb(SSL *ssl, const unsigned char **out, + unsigned char *outlen, const unsigned char *in, + unsigned int inlen, void *arg) { + constexpr StringRef alpnlist[] = { + StringRef::from_lit("h3"), + StringRef::from_lit("h3-29"), + }; + + for (auto &alpn : alpnlist) { + for (auto p = in, end = in + inlen; p < end;) { + auto proto_id = p + 1; + auto proto_len = *p; + + if (alpn.size() == proto_len && + memcmp(alpn.byte(), proto_id, alpn.size()) == 0) { + *out = proto_id; + *outlen = proto_len; + + return SSL_TLSEXT_ERR_OK; + } + + p += 1 + proto_len; + } + } + + return SSL_TLSEXT_ERR_ALERT_FATAL; +} +} // namespace +#endif // ENABLE_HTTP3 + +#ifdef NGHTTP2_GENUINE_OPENSSL +namespace { +int sct_add_cb(SSL *ssl, unsigned int ext_type, unsigned int context, + const unsigned char **out, size_t *outlen, X509 *x, + size_t chainidx, int *al, void *add_arg) { + assert(ext_type == TLSEXT_TYPE_signed_certificate_timestamp); + + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + if (!conn->tls.sct_requested) { + return 0; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "sct_add_cb is called, chainidx=" << chainidx << ", x=" << x + << ", context=" << log::hex << context; + } + + // We only have SCTs for leaf certificate. + if (chainidx != 0) { + return 0; + } + + auto ssl_ctx = SSL_get_SSL_CTX(ssl); + auto tls_ctx_data = + static_cast<TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx)); + + *out = tls_ctx_data->sct_data.data(); + *outlen = tls_ctx_data->sct_data.size(); + + return 1; +} +} // namespace + +namespace { +void sct_free_cb(SSL *ssl, unsigned int ext_type, unsigned int context, + const unsigned char *out, void *add_arg) { + assert(ext_type == TLSEXT_TYPE_signed_certificate_timestamp); +} +} // namespace + +namespace { +int sct_parse_cb(SSL *ssl, unsigned int ext_type, unsigned int context, + const unsigned char *in, size_t inlen, X509 *x, + size_t chainidx, int *al, void *parse_arg) { + assert(ext_type == TLSEXT_TYPE_signed_certificate_timestamp); + // client SHOULD send 0 length extension_data, but it is still + // SHOULD, and not MUST. + + // For TLSv1.3 Certificate message, sct_add_cb is called even if + // client has not sent signed_certificate_timestamp extension in its + // ClientHello. Explicitly remember that client has included it + // here. + auto conn = static_cast<Connection *>(SSL_get_app_data(ssl)); + conn->tls.sct_requested = true; + + return 1; +} +} // namespace + +#endif // NGHTTP2_GENUINE_OPENSSL + +#ifndef OPENSSL_NO_PSK +namespace { +unsigned int psk_server_cb(SSL *ssl, const char *identity, unsigned char *psk, + unsigned int max_psk_len) { + auto config = get_config(); + auto &tlsconf = config->tls; + + auto it = tlsconf.psk_secrets.find(StringRef{identity}); + if (it == std::end(tlsconf.psk_secrets)) { + return 0; + } + + auto &secret = (*it).second; + if (secret.size() > max_psk_len) { + LOG(ERROR) << "The size of PSK secret is " << secret.size() + << ", but the acceptable maximum size is" << max_psk_len; + return 0; + } + + std::copy(std::begin(secret), std::end(secret), psk); + + return static_cast<unsigned int>(secret.size()); +} +} // namespace +#endif // !OPENSSL_NO_PSK + +#ifndef OPENSSL_NO_PSK +namespace { +unsigned int psk_client_cb(SSL *ssl, const char *hint, char *identity_out, + unsigned int max_identity_len, unsigned char *psk, + unsigned int max_psk_len) { + auto config = get_config(); + auto &tlsconf = config->tls; + + auto &identity = tlsconf.client.psk.identity; + auto &secret = tlsconf.client.psk.secret; + + if (identity.empty()) { + return 0; + } + + if (identity.size() + 1 > max_identity_len) { + LOG(ERROR) << "The size of PSK identity is " << identity.size() + << ", but the acceptable maximum size is " << max_identity_len; + return 0; + } + + if (secret.size() > max_psk_len) { + LOG(ERROR) << "The size of PSK secret is " << secret.size() + << ", but the acceptable maximum size is " << max_psk_len; + return 0; + } + + *std::copy(std::begin(identity), std::end(identity), identity_out) = '\0'; + std::copy(std::begin(secret), std::end(secret), psk); + + return static_cast<unsigned int>(secret.size()); +} +} // namespace +#endif // !OPENSSL_NO_PSK + +struct TLSProtocol { + StringRef name; + long int mask; +}; + +constexpr TLSProtocol TLS_PROTOS[] = { + TLSProtocol{StringRef::from_lit("TLSv1.2"), SSL_OP_NO_TLSv1_2}, + TLSProtocol{StringRef::from_lit("TLSv1.1"), SSL_OP_NO_TLSv1_1}, + TLSProtocol{StringRef::from_lit("TLSv1.0"), SSL_OP_NO_TLSv1}}; + +long int create_tls_proto_mask(const std::vector<StringRef> &tls_proto_list) { + long int res = 0; + + for (auto &supported : TLS_PROTOS) { + auto ok = false; + for (auto &name : tls_proto_list) { + if (util::strieq(supported.name, name)) { + ok = true; + break; + } + } + if (!ok) { + res |= supported.mask; + } + } + return res; +} + +SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file, + const std::vector<uint8_t> &sct_data +#ifdef HAVE_NEVERBLEED + , + neverbleed_t *nb +#endif // HAVE_NEVERBLEED +) { + auto ssl_ctx = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx) { + LOG(FATAL) << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + + auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION | + SSL_OP_SINGLE_ECDH_USE | SSL_OP_SINGLE_DH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE +#ifdef NGHTTP2_GENUINE_OPENSSL + // The reason for disabling built-in anti-replay in + // OpenSSL is that it only works if client gets back + // to the same server. The freshness check + // described in + // https://tools.ietf.org/html/rfc8446#section-8.3 + // is still performed. + | SSL_OP_NO_ANTI_REPLAY +#endif // NGHTTP2_GENUINE_OPENSSL + ; + + auto config = mod_config(); + auto &tlsconf = config->tls; + +#ifdef SSL_OP_ENABLE_KTLS + if (tlsconf.ktls) { + ssl_opts |= SSL_OP_ENABLE_KTLS; + } +#endif // SSL_OP_ENABLE_KTLS + + SSL_CTX_set_options(ssl_ctx, ssl_opts | tlsconf.tls_proto_mask); + + if (nghttp2::tls::ssl_ctx_set_proto_versions( + ssl_ctx, tlsconf.min_proto_version, tlsconf.max_proto_version) != 0) { + LOG(FATAL) << "Could not set TLS protocol version"; + DIE(); + } + + const unsigned char sid_ctx[] = "shrpx"; + SSL_CTX_set_session_id_context(ssl_ctx, sid_ctx, sizeof(sid_ctx) - 1); + SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER); + + if (!tlsconf.session_cache.memcached.host.empty()) { + SSL_CTX_sess_set_new_cb(ssl_ctx, tls_session_new_cb); + SSL_CTX_sess_set_get_cb(ssl_ctx, tls_session_get_cb); + } + + SSL_CTX_set_timeout(ssl_ctx, tlsconf.session_timeout.count()); + + if (SSL_CTX_set_cipher_list(ssl_ctx, tlsconf.ciphers.c_str()) == 0) { + LOG(FATAL) << "SSL_CTX_set_cipher_list " << tlsconf.ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_LIBRESSL) + if (SSL_CTX_set_ciphersuites(ssl_ctx, tlsconf.tls13_ciphers.c_str()) == 0) { + LOG(FATAL) << "SSL_CTX_set_ciphersuites " << tlsconf.tls13_ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_LIBRESSL + +#ifndef OPENSSL_NO_EC + if (SSL_CTX_set1_curves_list(ssl_ctx, tlsconf.ecdh_curves.c_str()) != 1) { + LOG(FATAL) << "SSL_CTX_set1_curves_list " << tlsconf.ecdh_curves + << " failed"; + DIE(); + } +#endif // OPENSSL_NO_EC + + if (!tlsconf.dh_param_file.empty()) { + // Read DH parameters from file + auto bio = BIO_new_file(tlsconf.dh_param_file.c_str(), "rb"); + if (bio == nullptr) { + LOG(FATAL) << "BIO_new_file() failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#if OPENSSL_3_0_0_API + EVP_PKEY *dh = nullptr; + auto dctx = OSSL_DECODER_CTX_new_for_pkey( + &dh, "PEM", nullptr, "DH", OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, + nullptr, nullptr); + + if (!OSSL_DECODER_from_bio(dctx, bio)) { + LOG(FATAL) << "OSSL_DECODER_from_bio() failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + + if (SSL_CTX_set0_tmp_dh_pkey(ssl_ctx, dh) != 1) { + LOG(FATAL) << "SSL_CTX_set0_tmp_dh_pkey failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#else // !OPENSSL_3_0_0_API + auto dh = PEM_read_bio_DHparams(bio, nullptr, nullptr, nullptr); + if (dh == nullptr) { + LOG(FATAL) << "PEM_read_bio_DHparams() failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + SSL_CTX_set_tmp_dh(ssl_ctx, dh); + DH_free(dh); +#endif // !OPENSSL_3_0_0_API + BIO_free(bio); + } + + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + if (SSL_CTX_set_default_verify_paths(ssl_ctx) != 1) { + LOG(WARN) << "Could not load system trusted ca certificates: " + << ERR_error_string(ERR_get_error(), nullptr); + } + + if (!tlsconf.cacert.empty()) { + if (SSL_CTX_load_verify_locations(ssl_ctx, tlsconf.cacert.c_str(), + nullptr) != 1) { + LOG(FATAL) << "Could not load trusted ca certificates from " + << tlsconf.cacert << ": " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + } + + if (!tlsconf.private_key_passwd.empty()) { + SSL_CTX_set_default_passwd_cb(ssl_ctx, ssl_pem_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(ssl_ctx, config); + } + +#ifndef HAVE_NEVERBLEED + if (SSL_CTX_use_PrivateKey_file(ssl_ctx, private_key_file, + SSL_FILETYPE_PEM) != 1) { + LOG(FATAL) << "SSL_CTX_use_PrivateKey_file failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#else // HAVE_NEVERBLEED + std::array<char, NEVERBLEED_ERRBUF_SIZE> errbuf; + if (neverbleed_load_private_key_file(nb, ssl_ctx, private_key_file, + errbuf.data()) != 1) { + LOG(FATAL) << "neverbleed_load_private_key_file failed: " << errbuf.data(); + DIE(); + } +#endif // HAVE_NEVERBLEED + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_file) != 1) { + LOG(FATAL) << "SSL_CTX_use_certificate_file failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + if (SSL_CTX_check_private_key(ssl_ctx) != 1) { + LOG(FATAL) << "SSL_CTX_check_private_key failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + if (tlsconf.client_verify.enabled) { + if (!tlsconf.client_verify.cacert.empty()) { + if (SSL_CTX_load_verify_locations( + ssl_ctx, tlsconf.client_verify.cacert.c_str(), nullptr) != 1) { + + LOG(FATAL) << "Could not load trusted ca certificates from " + << tlsconf.client_verify.cacert << ": " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + // It is heard that SSL_CTX_load_verify_locations() may leave + // error even though it returns success. See + // http://forum.nginx.org/read.php?29,242540 + ERR_clear_error(); + auto list = SSL_load_client_CA_file(tlsconf.client_verify.cacert.c_str()); + if (!list) { + LOG(FATAL) << "Could not load ca certificates from " + << tlsconf.client_verify.cacert << ": " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + SSL_CTX_set_client_CA_list(ssl_ctx, list); + } + SSL_CTX_set_verify(ssl_ctx, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_callback); + } + SSL_CTX_set_tlsext_servername_callback(ssl_ctx, servername_callback); +#if OPENSSL_3_0_0_API + SSL_CTX_set_tlsext_ticket_key_evp_cb(ssl_ctx, ticket_key_cb); +#else // !OPENSSL_3_0_0_API + SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, ticket_key_cb); +#endif // !OPENSSL_3_0_0_API +#ifndef NGHTTP2_OPENSSL_IS_BORINGSSL + SSL_CTX_set_tlsext_status_cb(ssl_ctx, ocsp_resp_cb); +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + SSL_CTX_set_info_callback(ssl_ctx, info_callback); + +#ifdef NGHTTP2_OPENSSL_IS_BORINGSSL + SSL_CTX_set_early_data_enabled(ssl_ctx, 1); +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + + // ALPN selection callback + SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, nullptr); + + auto tls_ctx_data = new TLSContextData(); + tls_ctx_data->cert_file = cert_file; + tls_ctx_data->sct_data = sct_data; + + SSL_CTX_set_app_data(ssl_ctx, tls_ctx_data); + +#ifdef NGHTTP2_GENUINE_OPENSSL + // SSL_extension_supported(TLSEXT_TYPE_signed_certificate_timestamp) + // returns 1, which means OpenSSL internally handles it. But + // OpenSSL handles signed_certificate_timestamp extension specially, + // and it lets custom handler to process the extension. + if (!sct_data.empty()) { + // It is not entirely clear to me that SSL_EXT_CLIENT_HELLO is + // required here. sct_parse_cb is called without + // SSL_EXT_CLIENT_HELLO being set. But the passed context value + // is SSL_EXT_CLIENT_HELLO. + if (SSL_CTX_add_custom_ext( + ssl_ctx, TLSEXT_TYPE_signed_certificate_timestamp, + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | + SSL_EXT_TLS1_3_CERTIFICATE | SSL_EXT_IGNORE_ON_RESUMPTION, + sct_add_cb, sct_free_cb, nullptr, sct_parse_cb, nullptr) != 1) { + LOG(FATAL) << "SSL_CTX_add_custom_ext failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + } +#elif defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + if (!tls_ctx_data->sct_data.empty() && + SSL_CTX_set_signed_cert_timestamp_list( + ssl_ctx, tls_ctx_data->sct_data.data(), + tls_ctx_data->sct_data.size()) != 1) { + LOG(FATAL) << "SSL_CTX_set_signed_cert_timestamp_list failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#endif // NGHTTP2_OPENSSL_IS_BORINGSSL + +#ifdef NGHTTP2_GENUINE_OPENSSL + if (SSL_CTX_set_max_early_data(ssl_ctx, tlsconf.max_early_data) != 1) { + LOG(FATAL) << "SSL_CTX_set_max_early_data failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + if (SSL_CTX_set_recv_max_early_data(ssl_ctx, tlsconf.max_early_data) != 1) { + LOG(FATAL) << "SSL_CTX_set_recv_max_early_data failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#endif // NGHTTP2_GENUINE_OPENSSL + +#ifndef OPENSSL_NO_PSK + SSL_CTX_set_psk_server_callback(ssl_ctx, psk_server_cb); +#endif // !LIBRESSL_NO_PSK + + return ssl_ctx; +} + +#ifdef ENABLE_HTTP3 +SSL_CTX *create_quic_ssl_context(const char *private_key_file, + const char *cert_file, + const std::vector<uint8_t> &sct_data +# ifdef HAVE_NEVERBLEED + , + neverbleed_t *nb +# endif // HAVE_NEVERBLEED +) { + auto ssl_ctx = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx) { + LOG(FATAL) << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + + constexpr auto ssl_opts = + (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION | SSL_OP_SINGLE_ECDH_USE | + SSL_OP_SINGLE_DH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE +# ifdef NGHTTP2_GENUINE_OPENSSL + // The reason for disabling built-in anti-replay in OpenSSL is + // that it only works if client gets back to the same server. + // The freshness check described in + // https://tools.ietf.org/html/rfc8446#section-8.3 is still + // performed. + | SSL_OP_NO_ANTI_REPLAY +# endif // NGHTTP2_GENUINE_OPENSSL + ; + + auto config = mod_config(); + auto &tlsconf = config->tls; + + SSL_CTX_set_options(ssl_ctx, ssl_opts); + +# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS + if (ngtcp2_crypto_quictls_configure_server_context(ssl_ctx) != 0) { + LOG(FATAL) << "ngtcp2_crypto_quictls_configure_server_context failed"; + DIE(); + } +# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL + if (ngtcp2_crypto_boringssl_configure_server_context(ssl_ctx) != 0) { + LOG(FATAL) << "ngtcp2_crypto_boringssl_configure_server_context failed"; + DIE(); + } +# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL + + const unsigned char sid_ctx[] = "shrpx"; + SSL_CTX_set_session_id_context(ssl_ctx, sid_ctx, sizeof(sid_ctx) - 1); + SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_OFF); + + SSL_CTX_set_timeout(ssl_ctx, tlsconf.session_timeout.count()); + + if (SSL_CTX_set_cipher_list(ssl_ctx, tlsconf.ciphers.c_str()) == 0) { + LOG(FATAL) << "SSL_CTX_set_cipher_list " << tlsconf.ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + +# if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_LIBRESSL) + if (SSL_CTX_set_ciphersuites(ssl_ctx, tlsconf.tls13_ciphers.c_str()) == 0) { + LOG(FATAL) << "SSL_CTX_set_ciphersuites " << tlsconf.tls13_ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +# endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_LIBRESSL + +# ifndef OPENSSL_NO_EC + if (SSL_CTX_set1_curves_list(ssl_ctx, tlsconf.ecdh_curves.c_str()) != 1) { + LOG(FATAL) << "SSL_CTX_set1_curves_list " << tlsconf.ecdh_curves + << " failed"; + DIE(); + } +# endif // OPENSSL_NO_EC + + if (!tlsconf.dh_param_file.empty()) { + // Read DH parameters from file + auto bio = BIO_new_file(tlsconf.dh_param_file.c_str(), "rb"); + if (bio == nullptr) { + LOG(FATAL) << "BIO_new_file() failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +# if OPENSSL_3_0_0_API + EVP_PKEY *dh = nullptr; + auto dctx = OSSL_DECODER_CTX_new_for_pkey( + &dh, "PEM", nullptr, "DH", OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, + nullptr, nullptr); + + if (!OSSL_DECODER_from_bio(dctx, bio)) { + LOG(FATAL) << "OSSL_DECODER_from_bio() failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + + if (SSL_CTX_set0_tmp_dh_pkey(ssl_ctx, dh) != 1) { + LOG(FATAL) << "SSL_CTX_set0_tmp_dh_pkey failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +# else // !OPENSSL_3_0_0_API + auto dh = PEM_read_bio_DHparams(bio, nullptr, nullptr, nullptr); + if (dh == nullptr) { + LOG(FATAL) << "PEM_read_bio_DHparams() failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + SSL_CTX_set_tmp_dh(ssl_ctx, dh); + DH_free(dh); +# endif // !OPENSSL_3_0_0_API + BIO_free(bio); + } + + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + if (SSL_CTX_set_default_verify_paths(ssl_ctx) != 1) { + LOG(WARN) << "Could not load system trusted ca certificates: " + << ERR_error_string(ERR_get_error(), nullptr); + } + + if (!tlsconf.cacert.empty()) { + if (SSL_CTX_load_verify_locations(ssl_ctx, tlsconf.cacert.c_str(), + nullptr) != 1) { + LOG(FATAL) << "Could not load trusted ca certificates from " + << tlsconf.cacert << ": " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + } + + if (!tlsconf.private_key_passwd.empty()) { + SSL_CTX_set_default_passwd_cb(ssl_ctx, ssl_pem_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(ssl_ctx, config); + } + +# ifndef HAVE_NEVERBLEED + if (SSL_CTX_use_PrivateKey_file(ssl_ctx, private_key_file, + SSL_FILETYPE_PEM) != 1) { + LOG(FATAL) << "SSL_CTX_use_PrivateKey_file failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +# else // HAVE_NEVERBLEED + std::array<char, NEVERBLEED_ERRBUF_SIZE> errbuf; + if (neverbleed_load_private_key_file(nb, ssl_ctx, private_key_file, + errbuf.data()) != 1) { + LOG(FATAL) << "neverbleed_load_private_key_file failed: " << errbuf.data(); + DIE(); + } +# endif // HAVE_NEVERBLEED + + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_file) != 1) { + LOG(FATAL) << "SSL_CTX_use_certificate_file failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + if (SSL_CTX_check_private_key(ssl_ctx) != 1) { + LOG(FATAL) << "SSL_CTX_check_private_key failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + if (tlsconf.client_verify.enabled) { + if (!tlsconf.client_verify.cacert.empty()) { + if (SSL_CTX_load_verify_locations( + ssl_ctx, tlsconf.client_verify.cacert.c_str(), nullptr) != 1) { + + LOG(FATAL) << "Could not load trusted ca certificates from " + << tlsconf.client_verify.cacert << ": " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + // It is heard that SSL_CTX_load_verify_locations() may leave + // error even though it returns success. See + // http://forum.nginx.org/read.php?29,242540 + ERR_clear_error(); + auto list = SSL_load_client_CA_file(tlsconf.client_verify.cacert.c_str()); + if (!list) { + LOG(FATAL) << "Could not load ca certificates from " + << tlsconf.client_verify.cacert << ": " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + SSL_CTX_set_client_CA_list(ssl_ctx, list); + } + SSL_CTX_set_verify(ssl_ctx, + SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT, + verify_callback); + } + SSL_CTX_set_tlsext_servername_callback(ssl_ctx, servername_callback); +# if OPENSSL_3_0_0_API + SSL_CTX_set_tlsext_ticket_key_evp_cb(ssl_ctx, ticket_key_cb); +# else // !OPENSSL_3_0_0_API + SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, ticket_key_cb); +# endif // !OPENSSL_3_0_0_API +# ifndef NGHTTP2_OPENSSL_IS_BORINGSSL + SSL_CTX_set_tlsext_status_cb(ssl_ctx, ocsp_resp_cb); +# endif // NGHTTP2_OPENSSL_IS_BORINGSSL + + // ALPN selection callback + SSL_CTX_set_alpn_select_cb(ssl_ctx, quic_alpn_select_proto_cb, nullptr); + + auto tls_ctx_data = new TLSContextData(); + tls_ctx_data->cert_file = cert_file; + tls_ctx_data->sct_data = sct_data; + + SSL_CTX_set_app_data(ssl_ctx, tls_ctx_data); + +# ifdef NGHTTP2_GENUINE_OPENSSL + // SSL_extension_supported(TLSEXT_TYPE_signed_certificate_timestamp) + // returns 1, which means OpenSSL internally handles it. But + // OpenSSL handles signed_certificate_timestamp extension specially, + // and it lets custom handler to process the extension. + if (!sct_data.empty()) { + // It is not entirely clear to me that SSL_EXT_CLIENT_HELLO is + // required here. sct_parse_cb is called without + // SSL_EXT_CLIENT_HELLO being set. But the passed context value + // is SSL_EXT_CLIENT_HELLO. + if (SSL_CTX_add_custom_ext( + ssl_ctx, TLSEXT_TYPE_signed_certificate_timestamp, + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | + SSL_EXT_TLS1_3_CERTIFICATE | SSL_EXT_IGNORE_ON_RESUMPTION, + sct_add_cb, sct_free_cb, nullptr, sct_parse_cb, nullptr) != 1) { + LOG(FATAL) << "SSL_CTX_add_custom_ext failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + } +# elif defined(NGHTTP2_OPENSSL_IS_BORINGSSL) + if (!tls_ctx_data->sct_data.empty() && + SSL_CTX_set_signed_cert_timestamp_list( + ssl_ctx, tls_ctx_data->sct_data.data(), + tls_ctx_data->sct_data.size()) != 1) { + LOG(FATAL) << "SSL_CTX_set_signed_cert_timestamp_list failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +# endif // NGHTTP2_OPENSSL_IS_BORINGSSL + +# ifdef NGHTTP2_GENUINE_OPENSSL + auto &quicconf = config->quic; + + if (quicconf.upstream.early_data && + SSL_CTX_set_max_early_data(ssl_ctx, + std::numeric_limits<uint32_t>::max()) != 1) { + LOG(FATAL) << "SSL_CTX_set_max_early_data failed: " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +# endif // NGHTTP2_GENUINE_OPENSSL + +# ifndef OPENSSL_NO_PSK + SSL_CTX_set_psk_server_callback(ssl_ctx, psk_server_cb); +# endif // !LIBRESSL_NO_PSK + + return ssl_ctx; +} +#endif // ENABLE_HTTP3 + +SSL_CTX *create_ssl_client_context( +#ifdef HAVE_NEVERBLEED + neverbleed_t *nb, +#endif // HAVE_NEVERBLEED + const StringRef &cacert, const StringRef &cert_file, + const StringRef &private_key_file) { + auto ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx) { + LOG(FATAL) << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + + auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; + + auto &tlsconf = get_config()->tls; + +#ifdef SSL_OP_ENABLE_KTLS + if (tlsconf.ktls) { + ssl_opts |= SSL_OP_ENABLE_KTLS; + } +#endif // SSL_OP_ENABLE_KTLS + + SSL_CTX_set_options(ssl_ctx, ssl_opts | tlsconf.tls_proto_mask); + + SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL_STORE); + SSL_CTX_sess_set_new_cb(ssl_ctx, tls_session_client_new_cb); + + if (nghttp2::tls::ssl_ctx_set_proto_versions( + ssl_ctx, tlsconf.min_proto_version, tlsconf.max_proto_version) != 0) { + LOG(FATAL) << "Could not set TLS protocol version"; + DIE(); + } + + if (SSL_CTX_set_cipher_list(ssl_ctx, tlsconf.client.ciphers.c_str()) == 0) { + LOG(FATAL) << "SSL_CTX_set_cipher_list " << tlsconf.client.ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_LIBRESSL) + if (SSL_CTX_set_ciphersuites(ssl_ctx, tlsconf.client.tls13_ciphers.c_str()) == + 0) { + LOG(FATAL) << "SSL_CTX_set_ciphersuites " << tlsconf.client.tls13_ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_LIBRESSL + + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + if (SSL_CTX_set_default_verify_paths(ssl_ctx) != 1) { + LOG(WARN) << "Could not load system trusted ca certificates: " + << ERR_error_string(ERR_get_error(), nullptr); + } + + if (!cacert.empty()) { + if (SSL_CTX_load_verify_locations(ssl_ctx, cacert.c_str(), nullptr) != 1) { + + LOG(FATAL) << "Could not load trusted ca certificates from " << cacert + << ": " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + } + + if (!tlsconf.insecure) { + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr); + } + + if (!cert_file.empty()) { + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_file.c_str()) != 1) { + + LOG(FATAL) << "Could not load client certificate from " << cert_file + << ": " << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } + } + + if (!private_key_file.empty()) { +#ifndef HAVE_NEVERBLEED + if (SSL_CTX_use_PrivateKey_file(ssl_ctx, private_key_file.c_str(), + SSL_FILETYPE_PEM) != 1) { + LOG(FATAL) << "Could not load client private key from " + << private_key_file << ": " + << ERR_error_string(ERR_get_error(), nullptr); + DIE(); + } +#else // HAVE_NEVERBLEED + std::array<char, NEVERBLEED_ERRBUF_SIZE> errbuf; + if (neverbleed_load_private_key_file(nb, ssl_ctx, private_key_file.c_str(), + errbuf.data()) != 1) { + LOG(FATAL) << "neverbleed_load_private_key_file: could not load client " + "private key from " + << private_key_file << ": " << errbuf.data(); + DIE(); + } +#endif // HAVE_NEVERBLEED + } + +#ifndef OPENSSL_NO_PSK + SSL_CTX_set_psk_client_callback(ssl_ctx, psk_client_cb); +#endif // !OPENSSL_NO_PSK + + return ssl_ctx; +} + +SSL *create_ssl(SSL_CTX *ssl_ctx) { + auto ssl = SSL_new(ssl_ctx); + if (!ssl) { + LOG(ERROR) << "SSL_new() failed: " + << ERR_error_string(ERR_get_error(), nullptr); + return nullptr; + } + + return ssl; +} + +ClientHandler *accept_connection(Worker *worker, int fd, sockaddr *addr, + int addrlen, const UpstreamAddr *faddr) { + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> service; + int rv; + + if (addr->sa_family == AF_UNIX) { + std::copy_n("localhost", sizeof("localhost"), std::begin(host)); + service[0] = '\0'; + } else { + rv = getnameinfo(addr, addrlen, host.data(), host.size(), service.data(), + service.size(), NI_NUMERICHOST | NI_NUMERICSERV); + if (rv != 0) { + LOG(ERROR) << "getnameinfo() failed: " << gai_strerror(rv); + + return nullptr; + } + + rv = util::make_socket_nodelay(fd); + if (rv == -1) { + LOG(WARN) << "Setting option TCP_NODELAY failed: errno=" << errno; + } + } + SSL *ssl = nullptr; + if (faddr->tls) { + auto ssl_ctx = worker->get_sv_ssl_ctx(); + + assert(ssl_ctx); + + ssl = create_ssl(ssl_ctx); + if (!ssl) { + return nullptr; + } + // Disable TLS session ticket if we don't have working ticket + // keys. + if (!worker->get_ticket_keys()) { + SSL_set_options(ssl, SSL_OP_NO_TICKET); + } + } + + return new ClientHandler(worker, fd, ssl, StringRef{host.data()}, + StringRef{service.data()}, addr->sa_family, faddr); +} + +bool tls_hostname_match(const StringRef &pattern, const StringRef &hostname) { + auto ptWildcard = std::find(std::begin(pattern), std::end(pattern), '*'); + if (ptWildcard == std::end(pattern)) { + return util::strieq(pattern, hostname); + } + + auto ptLeftLabelEnd = std::find(std::begin(pattern), std::end(pattern), '.'); + auto wildcardEnabled = true; + // Do case-insensitive match. At least 2 dots are required to enable + // wildcard match. Also wildcard must be in the left-most label. + // Don't attempt to match a presented identifier where the wildcard + // character is embedded within an A-label. + if (ptLeftLabelEnd == std::end(pattern) || + std::find(ptLeftLabelEnd + 1, std::end(pattern), '.') == + std::end(pattern) || + ptLeftLabelEnd < ptWildcard || util::istarts_with_l(pattern, "xn--")) { + wildcardEnabled = false; + } + + if (!wildcardEnabled) { + return util::strieq(pattern, hostname); + } + + auto hnLeftLabelEnd = + std::find(std::begin(hostname), std::end(hostname), '.'); + if (hnLeftLabelEnd == std::end(hostname) || + !util::strieq(StringRef{ptLeftLabelEnd, std::end(pattern)}, + StringRef{hnLeftLabelEnd, std::end(hostname)})) { + return false; + } + // Perform wildcard match. Here '*' must match at least one + // character. + if (hnLeftLabelEnd - std::begin(hostname) < + ptLeftLabelEnd - std::begin(pattern)) { + return false; + } + return util::istarts_with(StringRef{std::begin(hostname), hnLeftLabelEnd}, + StringRef{std::begin(pattern), ptWildcard}) && + util::iends_with(StringRef{std::begin(hostname), hnLeftLabelEnd}, + StringRef{ptWildcard + 1, ptLeftLabelEnd}); +} + +namespace { +// if return value is not empty, StringRef.c_str() must be freed using +// OPENSSL_free(). +StringRef get_common_name(X509 *cert) { + auto subjectname = X509_get_subject_name(cert); + if (!subjectname) { + LOG(WARN) << "Could not get X509 name object from the certificate."; + return StringRef{}; + } + int lastpos = -1; + for (;;) { + lastpos = X509_NAME_get_index_by_NID(subjectname, NID_commonName, lastpos); + if (lastpos == -1) { + break; + } + auto entry = X509_NAME_get_entry(subjectname, lastpos); + + unsigned char *p; + auto plen = ASN1_STRING_to_UTF8(&p, X509_NAME_ENTRY_get_data(entry)); + if (plen < 0) { + continue; + } + if (std::find(p, p + plen, '\0') != p + plen) { + // Embedded NULL is not permitted. + continue; + } + if (plen == 0) { + LOG(WARN) << "X509 name is empty"; + OPENSSL_free(p); + continue; + } + + return StringRef{p, static_cast<size_t>(plen)}; + } + return StringRef{}; +} +} // namespace + +int verify_numeric_hostname(X509 *cert, const StringRef &hostname, + const Address *addr) { + const void *saddr; + size_t saddrlen; + switch (addr->su.storage.ss_family) { + case AF_INET: + saddr = &addr->su.in.sin_addr; + saddrlen = sizeof(addr->su.in.sin_addr); + break; + case AF_INET6: + saddr = &addr->su.in6.sin6_addr; + saddrlen = sizeof(addr->su.in6.sin6_addr); + break; + default: + return -1; + } + + auto altnames = static_cast<GENERAL_NAMES *>( + X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr)); + if (altnames) { + auto altnames_deleter = defer(GENERAL_NAMES_free, altnames); + size_t n = sk_GENERAL_NAME_num(altnames); + auto ip_found = false; + for (size_t i = 0; i < n; ++i) { + auto altname = sk_GENERAL_NAME_value(altnames, i); + if (altname->type != GEN_IPADD) { + continue; + } + + auto ip_addr = altname->d.iPAddress->data; + if (!ip_addr) { + continue; + } + size_t ip_addrlen = altname->d.iPAddress->length; + + ip_found = true; + if (saddrlen == ip_addrlen && memcmp(saddr, ip_addr, ip_addrlen) == 0) { + return 0; + } + } + + if (ip_found) { + return -1; + } + } + + auto cn = get_common_name(cert); + if (cn.empty()) { + return -1; + } + + // cn is not NULL terminated + auto rv = util::streq(hostname, cn); + OPENSSL_free(const_cast<char *>(cn.c_str())); + + if (rv) { + return 0; + } + + return -1; +} + +int verify_dns_hostname(X509 *cert, const StringRef &hostname) { + auto altnames = static_cast<GENERAL_NAMES *>( + X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr)); + if (altnames) { + auto dns_found = false; + auto altnames_deleter = defer(GENERAL_NAMES_free, altnames); + size_t n = sk_GENERAL_NAME_num(altnames); + for (size_t i = 0; i < n; ++i) { + auto altname = sk_GENERAL_NAME_value(altnames, i); + if (altname->type != GEN_DNS) { + continue; + } + + auto name = ASN1_STRING_get0_data(altname->d.ia5); + if (!name) { + continue; + } + + auto len = ASN1_STRING_length(altname->d.ia5); + if (len == 0) { + continue; + } + if (std::find(name, name + len, '\0') != name + len) { + // Embedded NULL is not permitted. + continue; + } + + if (name[len - 1] == '.') { + --len; + if (len == 0) { + continue; + } + } + + dns_found = true; + + if (tls_hostname_match(StringRef{name, static_cast<size_t>(len)}, + hostname)) { + return 0; + } + } + + // RFC 6125, section 6.4.4. says that client MUST not seek a match + // for CN if a dns dNSName is found. + if (dns_found) { + return -1; + } + } + + auto cn = get_common_name(cert); + if (cn.empty()) { + return -1; + } + + if (cn[cn.size() - 1] == '.') { + if (cn.size() == 1) { + OPENSSL_free(const_cast<char *>(cn.c_str())); + + return -1; + } + cn = StringRef{cn.c_str(), cn.size() - 1}; + } + + auto rv = tls_hostname_match(cn, hostname); + OPENSSL_free(const_cast<char *>(cn.c_str())); + + return rv ? 0 : -1; +} + +namespace { +int verify_hostname(X509 *cert, const StringRef &hostname, + const Address *addr) { + if (util::numeric_host(hostname.c_str())) { + return verify_numeric_hostname(cert, hostname, addr); + } + + return verify_dns_hostname(cert, hostname); +} +} // namespace + +int check_cert(SSL *ssl, const Address *addr, const StringRef &host) { +#if OPENSSL_3_0_0_API + auto cert = SSL_get0_peer_certificate(ssl); +#else // !OPENSSL_3_0_0_API + auto cert = SSL_get_peer_certificate(ssl); +#endif // !OPENSSL_3_0_0_API + if (!cert) { + // By the protocol definition, TLS server always sends certificate + // if it has. If certificate cannot be retrieved, authentication + // without certificate is used, such as PSK. + return 0; + } +#if !OPENSSL_3_0_0_API + auto cert_deleter = defer(X509_free, cert); +#endif // !OPENSSL_3_0_0_API + + if (verify_hostname(cert, host, addr) != 0) { + LOG(ERROR) << "Certificate verification failed: hostname does not match"; + return -1; + } + return 0; +} + +int check_cert(SSL *ssl, const DownstreamAddr *addr, const Address *raddr) { + auto hostname = + addr->sni.empty() ? StringRef{addr->host} : StringRef{addr->sni}; + return check_cert(ssl, raddr, hostname); +} + +CertLookupTree::CertLookupTree() {} + +ssize_t CertLookupTree::add_cert(const StringRef &hostname, size_t idx) { + std::array<uint8_t, NI_MAXHOST> buf; + + // NI_MAXHOST includes terminal NULL byte + if (hostname.empty() || hostname.size() + 1 > buf.size()) { + return -1; + } + + auto wildcard_it = std::find(std::begin(hostname), std::end(hostname), '*'); + if (wildcard_it != std::end(hostname) && + wildcard_it + 1 != std::end(hostname)) { + auto wildcard_prefix = StringRef{std::begin(hostname), wildcard_it}; + auto wildcard_suffix = StringRef{wildcard_it + 1, std::end(hostname)}; + + auto rev_suffix = StringRef{std::begin(buf), + std::reverse_copy(std::begin(wildcard_suffix), + std::end(wildcard_suffix), + std::begin(buf))}; + + WildcardPattern *wpat; + + if (wildcard_patterns_.size() != + rev_wildcard_router_.add_route(rev_suffix, wildcard_patterns_.size())) { + auto wcidx = rev_wildcard_router_.match(rev_suffix); + + assert(wcidx != -1); + + wpat = &wildcard_patterns_[wcidx]; + } else { + wildcard_patterns_.emplace_back(); + wpat = &wildcard_patterns_.back(); + } + + auto rev_prefix = StringRef{std::begin(buf), + std::reverse_copy(std::begin(wildcard_prefix), + std::end(wildcard_prefix), + std::begin(buf))}; + + for (auto &p : wpat->rev_prefix) { + if (p.prefix == rev_prefix) { + return p.idx; + } + } + + wpat->rev_prefix.emplace_back(rev_prefix, idx); + + return idx; + } + + return router_.add_route(hostname, idx); +} + +ssize_t CertLookupTree::lookup(const StringRef &hostname) { + std::array<uint8_t, NI_MAXHOST> buf; + + // NI_MAXHOST includes terminal NULL byte + if (hostname.empty() || hostname.size() + 1 > buf.size()) { + return -1; + } + + // Always prefer exact match + auto idx = router_.match(hostname); + if (idx != -1) { + return idx; + } + + if (wildcard_patterns_.empty()) { + return -1; + } + + ssize_t best_idx = -1; + size_t best_prefixlen = 0; + const RNode *last_node = nullptr; + + auto rev_host = StringRef{ + std::begin(buf), std::reverse_copy(std::begin(hostname), + std::end(hostname), std::begin(buf))}; + + for (;;) { + size_t nread = 0; + + auto wcidx = + rev_wildcard_router_.match_prefix(&nread, &last_node, rev_host); + if (wcidx == -1) { + return best_idx; + } + + // '*' must match at least one byte + if (nread == rev_host.size()) { + return best_idx; + } + + rev_host = StringRef{std::begin(rev_host) + nread, std::end(rev_host)}; + + auto rev_prefix = StringRef{std::begin(rev_host) + 1, std::end(rev_host)}; + + auto &wpat = wildcard_patterns_[wcidx]; + for (auto &wprefix : wpat.rev_prefix) { + if (!util::ends_with(rev_prefix, wprefix.prefix)) { + continue; + } + + auto prefixlen = + wprefix.prefix.size() + + (reinterpret_cast<const uint8_t *>(&rev_host[0]) - &buf[0]); + + // Breaking a tie with longer suffix + if (prefixlen < best_prefixlen) { + continue; + } + + best_idx = wprefix.idx; + best_prefixlen = prefixlen; + } + } +} + +void CertLookupTree::dump() const { + std::cerr << "exact:" << std::endl; + router_.dump(); + std::cerr << "wildcard suffix (reversed):" << std::endl; + rev_wildcard_router_.dump(); +} + +int cert_lookup_tree_add_ssl_ctx( + CertLookupTree *lt, std::vector<std::vector<SSL_CTX *>> &indexed_ssl_ctx, + SSL_CTX *ssl_ctx) { + std::array<uint8_t, NI_MAXHOST> buf; + + auto cert = SSL_CTX_get0_certificate(ssl_ctx); + auto altnames = static_cast<GENERAL_NAMES *>( + X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr)); + if (altnames) { + auto altnames_deleter = defer(GENERAL_NAMES_free, altnames); + size_t n = sk_GENERAL_NAME_num(altnames); + auto dns_found = false; + for (size_t i = 0; i < n; ++i) { + auto altname = sk_GENERAL_NAME_value(altnames, i); + if (altname->type != GEN_DNS) { + continue; + } + + auto name = ASN1_STRING_get0_data(altname->d.ia5); + if (!name) { + continue; + } + + auto len = ASN1_STRING_length(altname->d.ia5); + if (len == 0) { + continue; + } + if (std::find(name, name + len, '\0') != name + len) { + // Embedded NULL is not permitted. + continue; + } + + if (name[len - 1] == '.') { + --len; + if (len == 0) { + continue; + } + } + + dns_found = true; + + if (static_cast<size_t>(len) + 1 > buf.size()) { + continue; + } + + auto end_buf = std::copy_n(name, len, std::begin(buf)); + util::inp_strlower(std::begin(buf), end_buf); + + auto idx = lt->add_cert(StringRef{std::begin(buf), end_buf}, + indexed_ssl_ctx.size()); + if (idx == -1) { + continue; + } + + if (static_cast<size_t>(idx) < indexed_ssl_ctx.size()) { + indexed_ssl_ctx[idx].push_back(ssl_ctx); + } else { + assert(static_cast<size_t>(idx) == indexed_ssl_ctx.size()); + indexed_ssl_ctx.emplace_back(std::vector<SSL_CTX *>{ssl_ctx}); + } + } + + // Don't bother CN if we have dNSName. + if (dns_found) { + return 0; + } + } + + auto cn = get_common_name(cert); + if (cn.empty()) { + return 0; + } + + if (cn[cn.size() - 1] == '.') { + if (cn.size() == 1) { + OPENSSL_free(const_cast<char *>(cn.c_str())); + + return 0; + } + + cn = StringRef{cn.c_str(), cn.size() - 1}; + } + + auto end_buf = std::copy(std::begin(cn), std::end(cn), std::begin(buf)); + + OPENSSL_free(const_cast<char *>(cn.c_str())); + + util::inp_strlower(std::begin(buf), end_buf); + + auto idx = + lt->add_cert(StringRef{std::begin(buf), end_buf}, indexed_ssl_ctx.size()); + if (idx == -1) { + return 0; + } + + if (static_cast<size_t>(idx) < indexed_ssl_ctx.size()) { + indexed_ssl_ctx[idx].push_back(ssl_ctx); + } else { + assert(static_cast<size_t>(idx) == indexed_ssl_ctx.size()); + indexed_ssl_ctx.emplace_back(std::vector<SSL_CTX *>{ssl_ctx}); + } + + return 0; +} + +bool in_proto_list(const std::vector<StringRef> &protos, + const StringRef &needle) { + for (auto &proto : protos) { + if (util::streq(proto, needle)) { + return true; + } + } + return false; +} + +bool upstream_tls_enabled(const ConnectionConfig &connconf) { +#ifdef ENABLE_HTTP3 + if (connconf.quic_listener.addrs.size()) { + return true; + } +#endif // ENABLE_HTTP3 + + const auto &faddrs = connconf.listener.addrs; + return std::any_of(std::begin(faddrs), std::end(faddrs), + [](const UpstreamAddr &faddr) { return faddr.tls; }); +} + +X509 *load_certificate(const char *filename) { + auto bio = BIO_new(BIO_s_file()); + if (!bio) { + fprintf(stderr, "BIO_new() failed\n"); + return nullptr; + } + auto bio_deleter = defer(BIO_vfree, bio); + if (!BIO_read_filename(bio, filename)) { + fprintf(stderr, "Could not read certificate file '%s'\n", filename); + return nullptr; + } + auto cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + if (!cert) { + fprintf(stderr, "Could not read X509 structure from file '%s'\n", filename); + return nullptr; + } + + return cert; +} + +SSL_CTX * +setup_server_ssl_context(std::vector<SSL_CTX *> &all_ssl_ctx, + std::vector<std::vector<SSL_CTX *>> &indexed_ssl_ctx, + CertLookupTree *cert_tree +#ifdef HAVE_NEVERBLEED + , + neverbleed_t *nb +#endif // HAVE_NEVERBLEED +) { + auto config = get_config(); + + if (!upstream_tls_enabled(config->conn)) { + return nullptr; + } + + auto &tlsconf = config->tls; + + auto ssl_ctx = create_ssl_context(tlsconf.private_key_file.c_str(), + tlsconf.cert_file.c_str(), tlsconf.sct_data +#ifdef HAVE_NEVERBLEED + , + nb +#endif // HAVE_NEVERBLEED + ); + + all_ssl_ctx.push_back(ssl_ctx); + + assert(cert_tree); + + if (cert_lookup_tree_add_ssl_ctx(cert_tree, indexed_ssl_ctx, ssl_ctx) == -1) { + LOG(FATAL) << "Failed to add default certificate."; + DIE(); + } + + for (auto &c : tlsconf.subcerts) { + auto ssl_ctx = create_ssl_context(c.private_key_file.c_str(), + c.cert_file.c_str(), c.sct_data +#ifdef HAVE_NEVERBLEED + , + nb +#endif // HAVE_NEVERBLEED + ); + all_ssl_ctx.push_back(ssl_ctx); + + if (cert_lookup_tree_add_ssl_ctx(cert_tree, indexed_ssl_ctx, ssl_ctx) == + -1) { + LOG(FATAL) << "Failed to add sub certificate."; + DIE(); + } + } + + return ssl_ctx; +} + +#ifdef ENABLE_HTTP3 +SSL_CTX *setup_quic_server_ssl_context( + std::vector<SSL_CTX *> &all_ssl_ctx, + std::vector<std::vector<SSL_CTX *>> &indexed_ssl_ctx, + CertLookupTree *cert_tree +# ifdef HAVE_NEVERBLEED + , + neverbleed_t *nb +# endif // HAVE_NEVERBLEED +) { + auto config = get_config(); + + if (!upstream_tls_enabled(config->conn)) { + return nullptr; + } + + auto &tlsconf = config->tls; + + auto ssl_ctx = + create_quic_ssl_context(tlsconf.private_key_file.c_str(), + tlsconf.cert_file.c_str(), tlsconf.sct_data +# ifdef HAVE_NEVERBLEED + , + nb +# endif // HAVE_NEVERBLEED + ); + + all_ssl_ctx.push_back(ssl_ctx); + + assert(cert_tree); + + if (cert_lookup_tree_add_ssl_ctx(cert_tree, indexed_ssl_ctx, ssl_ctx) == -1) { + LOG(FATAL) << "Failed to add default certificate."; + DIE(); + } + + for (auto &c : tlsconf.subcerts) { + auto ssl_ctx = create_quic_ssl_context(c.private_key_file.c_str(), + c.cert_file.c_str(), c.sct_data +# ifdef HAVE_NEVERBLEED + , + nb +# endif // HAVE_NEVERBLEED + ); + all_ssl_ctx.push_back(ssl_ctx); + + if (cert_lookup_tree_add_ssl_ctx(cert_tree, indexed_ssl_ctx, ssl_ctx) == + -1) { + LOG(FATAL) << "Failed to add sub certificate."; + DIE(); + } + } + + return ssl_ctx; +} +#endif // ENABLE_HTTP3 + +SSL_CTX *setup_downstream_client_ssl_context( +#ifdef HAVE_NEVERBLEED + neverbleed_t *nb +#endif // HAVE_NEVERBLEED +) { + auto &tlsconf = get_config()->tls; + + return create_ssl_client_context( +#ifdef HAVE_NEVERBLEED + nb, +#endif // HAVE_NEVERBLEED + tlsconf.cacert, tlsconf.client.cert_file, + tlsconf.client.private_key_file); +} + +void setup_downstream_http2_alpn(SSL *ssl) { + // ALPN advertisement + auto alpn = util::get_default_alpn(); + SSL_set_alpn_protos(ssl, alpn.data(), alpn.size()); +} + +void setup_downstream_http1_alpn(SSL *ssl) { + // ALPN advertisement + SSL_set_alpn_protos(ssl, NGHTTP2_H1_1_ALPN.byte(), NGHTTP2_H1_1_ALPN.size()); +} + +std::unique_ptr<CertLookupTree> create_cert_lookup_tree() { + auto config = get_config(); + if (!upstream_tls_enabled(config->conn)) { + return nullptr; + } + return std::make_unique<CertLookupTree>(); +} + +namespace { +std::vector<uint8_t> serialize_ssl_session(SSL_SESSION *session) { + auto len = i2d_SSL_SESSION(session, nullptr); + auto buf = std::vector<uint8_t>(len); + auto p = buf.data(); + i2d_SSL_SESSION(session, &p); + + return buf; +} +} // namespace + +void try_cache_tls_session(TLSSessionCache *cache, SSL_SESSION *session, + const std::chrono::steady_clock::time_point &t) { + if (cache->last_updated + 1min > t) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Client session cache entry is still fresh."; + } + return; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Update client cache entry " + << "timestamp = " << t.time_since_epoch().count(); + } + + cache->session_data = serialize_ssl_session(session); + cache->last_updated = t; +} + +SSL_SESSION *reuse_tls_session(const TLSSessionCache &cache) { + if (cache.session_data.empty()) { + return nullptr; + } + + auto p = cache.session_data.data(); + return d2i_SSL_SESSION(nullptr, &p, cache.session_data.size()); +} + +int proto_version_from_string(const StringRef &v) { +#ifdef TLS1_3_VERSION + if (util::strieq_l("TLSv1.3", v)) { + return TLS1_3_VERSION; + } +#endif // TLS1_3_VERSION + if (util::strieq_l("TLSv1.2", v)) { + return TLS1_2_VERSION; + } + if (util::strieq_l("TLSv1.1", v)) { + return TLS1_1_VERSION; + } + if (util::strieq_l("TLSv1.0", v)) { + return TLS1_VERSION; + } + return -1; +} + +int verify_ocsp_response(SSL_CTX *ssl_ctx, const uint8_t *ocsp_resp, + size_t ocsp_resplen) { +#ifndef OPENSSL_NO_OCSP + int rv; + + STACK_OF(X509) * chain_certs; + SSL_CTX_get0_chain_certs(ssl_ctx, &chain_certs); + + auto resp = d2i_OCSP_RESPONSE(nullptr, &ocsp_resp, ocsp_resplen); + if (resp == nullptr) { + LOG(ERROR) << "d2i_OCSP_RESPONSE failed"; + return -1; + } + auto resp_deleter = defer(OCSP_RESPONSE_free, resp); + + if (OCSP_response_status(resp) != OCSP_RESPONSE_STATUS_SUCCESSFUL) { + LOG(ERROR) << "OCSP response status is not successful"; + return -1; + } + + ERR_clear_error(); + + auto bs = OCSP_response_get1_basic(resp); + if (bs == nullptr) { + LOG(ERROR) << "OCSP_response_get1_basic failed: " + << ERR_error_string(ERR_get_error(), nullptr); + return -1; + } + auto bs_deleter = defer(OCSP_BASICRESP_free, bs); + + auto store = SSL_CTX_get_cert_store(ssl_ctx); + + ERR_clear_error(); + + rv = OCSP_basic_verify(bs, chain_certs, store, 0); + + if (rv != 1) { + LOG(ERROR) << "OCSP_basic_verify failed: " + << ERR_error_string(ERR_get_error(), nullptr); + return -1; + } + + auto sresp = OCSP_resp_get0(bs, 0); + if (sresp == nullptr) { + LOG(ERROR) << "OCSP response verification failed: no single response"; + return -1; + } + + auto certid = OCSP_SINGLERESP_get0_id(sresp); + assert(certid != nullptr); + + ASN1_INTEGER *serial; + rv = OCSP_id_get0_info(nullptr, nullptr, nullptr, &serial, + const_cast<OCSP_CERTID *>(certid)); + if (rv != 1) { + LOG(ERROR) << "OCSP_id_get0_info failed"; + return -1; + } + + if (serial == nullptr) { + LOG(ERROR) << "OCSP response does not contain serial number"; + return -1; + } + + auto cert = SSL_CTX_get0_certificate(ssl_ctx); + auto cert_serial = X509_get_serialNumber(cert); + + if (ASN1_INTEGER_cmp(cert_serial, serial)) { + LOG(ERROR) << "OCSP verification serial numbers do not match"; + return -1; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "OCSP verification succeeded"; + } +#endif // !OPENSSL_NO_OCSP + + return 0; +} + +ssize_t get_x509_fingerprint(uint8_t *dst, size_t dstlen, const X509 *x, + const EVP_MD *md) { + unsigned int len = dstlen; + if (X509_digest(x, md, dst, &len) != 1) { + return -1; + } + return len; +} + +namespace { +StringRef get_x509_name(BlockAllocator &balloc, X509_NAME *nm) { + auto b = BIO_new(BIO_s_mem()); + if (!b) { + return StringRef{}; + } + + auto b_deleter = defer(BIO_free, b); + + // Not documented, but it seems that X509_NAME_print_ex returns the + // number of bytes written into b. + auto slen = X509_NAME_print_ex(b, nm, 0, XN_FLAG_RFC2253); + if (slen <= 0) { + return StringRef{}; + } + + auto iov = make_byte_ref(balloc, slen + 1); + BIO_read(b, iov.base, slen); + iov.base[slen] = '\0'; + return StringRef{iov.base, static_cast<size_t>(slen)}; +} +} // namespace + +StringRef get_x509_subject_name(BlockAllocator &balloc, X509 *x) { + return get_x509_name(balloc, X509_get_subject_name(x)); +} + +StringRef get_x509_issuer_name(BlockAllocator &balloc, X509 *x) { + return get_x509_name(balloc, X509_get_issuer_name(x)); +} + +StringRef get_x509_serial(BlockAllocator &balloc, X509 *x) { + auto sn = X509_get_serialNumber(x); + auto bn = BN_new(); + auto bn_d = defer(BN_free, bn); + if (!ASN1_INTEGER_to_BN(sn, bn) || BN_num_bytes(bn) > 20) { + return StringRef{}; + } + + std::array<uint8_t, 20> b; + auto n = BN_bn2bin(bn, b.data()); + assert(n <= 20); + + return util::format_hex(balloc, StringRef{b.data(), static_cast<size_t>(n)}); +} + +namespace { +// Performs conversion from |at| to time_t. The result is stored in +// |t|. This function returns 0 if it succeeds, or -1. +int time_t_from_asn1_time(time_t &t, const ASN1_TIME *at) { + int rv; + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_LIBRESSL) + struct tm tm; + rv = ASN1_TIME_to_tm(at, &tm); + if (rv != 1) { + return -1; + } + + t = nghttp2_timegm(&tm); +#else // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_LIBRESSL + auto b = BIO_new(BIO_s_mem()); + if (!b) { + return -1; + } + + auto bio_deleter = defer(BIO_free, b); + + rv = ASN1_TIME_print(b, at); + if (rv != 1) { + return -1; + } + +# ifdef NGHTTP2_OPENSSL_IS_BORINGSSL + char *s; +# else + unsigned char *s; +# endif + auto slen = BIO_get_mem_data(b, &s); + auto tt = util::parse_openssl_asn1_time_print( + StringRef{s, static_cast<size_t>(slen)}); + if (tt == 0) { + return -1; + } + + t = tt; +#endif // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_LIBRESSL + + return 0; +} +} // namespace + +int get_x509_not_before(time_t &t, X509 *x) { + auto at = X509_get0_notBefore(x); + if (!at) { + return -1; + } + + return time_t_from_asn1_time(t, at); +} + +int get_x509_not_after(time_t &t, X509 *x) { + auto at = X509_get0_notAfter(x); + if (!at) { + return -1; + } + + return time_t_from_asn1_time(t, at); +} + +} // namespace tls + +} // namespace shrpx diff --git a/src/shrpx_tls.h b/src/shrpx_tls.h new file mode 100644 index 0000000..f740507 --- /dev/null +++ b/src/shrpx_tls.h @@ -0,0 +1,321 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_TLS_H +#define SHRPX_TLS_H + +#include "shrpx.h" + +#include <vector> +#include <mutex> + +#include <openssl/ssl.h> +#include <openssl/err.h> + +#include <ev.h> + +#ifdef HAVE_NEVERBLEED +# include <neverbleed.h> +#endif // HAVE_NEVERBLEED + +#include "network.h" +#include "shrpx_config.h" +#include "shrpx_router.h" + +namespace shrpx { + +class ClientHandler; +class Worker; +class DownstreamConnectionPool; +struct DownstreamAddr; +struct UpstreamAddr; + +namespace tls { + +struct TLSSessionCache { + // ASN1 representation of SSL_SESSION object. See + // i2d_SSL_SESSION(3SSL). + std::vector<uint8_t> session_data; + // The last time stamp when this cache entry is created or updated. + std::chrono::steady_clock::time_point last_updated; +}; + +// This struct stores the additional information per SSL_CTX. This is +// attached to SSL_CTX using SSL_CTX_set_app_data(). +struct TLSContextData { + // SCT data formatted so that this can be directly sent as + // extension_data of signed_certificate_timestamp. + std::vector<uint8_t> sct_data; +#ifndef HAVE_ATOMIC_STD_SHARED_PTR + // Protects ocsp_data; + std::mutex mu; +#endif // !HAVE_ATOMIC_STD_SHARED_PTR + // OCSP response + std::shared_ptr<std::vector<uint8_t>> ocsp_data; + + // Path to certificate file + const char *cert_file; +}; + +// Create server side SSL_CTX +SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file, + const std::vector<uint8_t> &sct_data + +#ifdef HAVE_NEVERBLEED + , + neverbleed_t *nb +#endif // HAVE_NEVERBLEED +); + +// Create client side SSL_CTX. This does not configure ALPN settings. +SSL_CTX *create_ssl_client_context( +#ifdef HAVE_NEVERBLEED + neverbleed_t *nb, +#endif // HAVE_NEVERBLEED + const StringRef &cacert, const StringRef &cert_file, + const StringRef &private_key_file); + +ClientHandler *accept_connection(Worker *worker, int fd, sockaddr *addr, + int addrlen, const UpstreamAddr *faddr); + +// Check peer's certificate against given |address| and |host|. +int check_cert(SSL *ssl, const Address *addr, const StringRef &host); +// Check peer's certificate against given host name described in +// |addr| and numeric address in |raddr|. Note that |raddr| might not +// point to &addr->addr. +int check_cert(SSL *ssl, const DownstreamAddr *addr, const Address *raddr); + +// Verify |cert| using numeric IP address. |hostname| and |addr| +// should contain the same numeric IP address. This function returns +// 0 if it succeeds, or -1. +int verify_numeric_hostname(X509 *cert, const StringRef &hostname, + const Address *addr); + +// Verify |cert| using DNS name hostname. This function returns 0 if +// it succeeds, or -1. +int verify_dns_hostname(X509 *cert, const StringRef &hostname); + +struct WildcardRevPrefix { + WildcardRevPrefix(const StringRef &prefix, size_t idx) + : prefix(std::begin(prefix), std::end(prefix)), idx(idx) {} + + // "Prefix" of wildcard pattern. It is reversed from original form. + // For example, if the original wildcard is "test*.nghttp2.org", + // prefix would be "tset". + ImmutableString prefix; + // The index of SSL_CTX. See ConnectionHandler::get_ssl_ctx(). + size_t idx; +}; + +struct WildcardPattern { + // Wildcard host sharing only suffix is probably rare, so we just do + // linear search. + std::vector<WildcardRevPrefix> rev_prefix; +}; + +class CertLookupTree { +public: + CertLookupTree(); + + // Adds hostname pattern |hostname| to the lookup tree, associating + // value |index|. When the queried host matches this pattern, + // |index| is returned. We support wildcard pattern. The left most + // '*' is considered as wildcard character, and it must match at + // least one character. If the same pattern has been already added, + // this function does not alter the tree, and returns the existing + // matching index. + // + // The caller should lower-case |hostname| since this function does + // do that, and lookup function performs case-sensitive match. + // + // TODO Treat wildcard pattern described as RFC 6125. + // + // This function returns the index. It returns -1 if it fails + // (e.g., hostname is too long). If the returned index equals to + // |index|, then hostname is added to the tree with the value + // |index|. If it is not -1, and does not equal to |index|, same + // hostname has already been added to the tree. + ssize_t add_cert(const StringRef &hostname, size_t index); + + // Looks up index using the given |hostname|. The exact match takes + // precedence over wildcard match. For wildcard match, longest + // match (sum of matched suffix and prefix length in bytes) is + // preferred, breaking a tie with longer suffix. + // + // The caller should lower-case |hostname| since this function + // performs case-sensitive match. + ssize_t lookup(const StringRef &hostname); + + // Dumps the contents of this lookup tree to stderr. + void dump() const; + +private: + // Exact match + Router router_; + // Wildcard reversed suffix match. The returned index is into + // wildcard_patterns_. + Router rev_wildcard_router_; + // Stores wildcard suffix patterns. + std::vector<WildcardPattern> wildcard_patterns_; +}; + +// Adds hostnames in certificate in |ssl_ctx| to lookup tree |lt|. +// The subjectAltNames and commonName are considered as eligible +// hostname. If there is at least one dNSName in subjectAltNames, +// commonName is not considered. |ssl_ctx| is also added to +// |indexed_ssl_ctx|. This function returns 0 if it succeeds, or -1. +int cert_lookup_tree_add_ssl_ctx( + CertLookupTree *lt, std::vector<std::vector<SSL_CTX *>> &indexed_ssl_ctx, + SSL_CTX *ssl_ctx); + +// Returns true if |proto| is included in the +// protocol list |protos|. +bool in_proto_list(const std::vector<StringRef> &protos, + const StringRef &proto); + +// Returns true if security requirement for HTTP/2 is fulfilled. +bool check_http2_requirement(SSL *ssl); + +// Returns SSL/TLS option mask to disable SSL/TLS protocol version not +// included in |tls_proto_list|. The returned mask can be directly +// passed to SSL_CTX_set_options(). +long int create_tls_proto_mask(const std::vector<StringRef> &tls_proto_list); + +int set_alpn_prefs(std::vector<unsigned char> &out, + const std::vector<StringRef> &protos); + +// Setups server side SSL_CTX. This function inspects get_config() +// and if upstream_no_tls is true, returns nullptr. Otherwise +// construct default SSL_CTX. If subcerts are available +// (get_config()->subcerts), caller should provide CertLookupTree +// object as |cert_tree| parameter, otherwise SNI does not work. All +// the created SSL_CTX is stored into |all_ssl_ctx|. They are also +// added to |indexed_ssl_ctx|. |cert_tree| uses its index to +// associate hostname to the SSL_CTX. +SSL_CTX * +setup_server_ssl_context(std::vector<SSL_CTX *> &all_ssl_ctx, + std::vector<std::vector<SSL_CTX *>> &indexed_ssl_ctx, + CertLookupTree *cert_tree +#ifdef HAVE_NEVERBLEED + , + neverbleed_t *nb +#endif // HAVE_NEVERBLEED +); + +#ifdef ENABLE_HTTP3 +SSL_CTX *setup_quic_server_ssl_context( + std::vector<SSL_CTX *> &all_ssl_ctx, + std::vector<std::vector<SSL_CTX *>> &indexed_ssl_ctx, + CertLookupTree *cert_tree +# ifdef HAVE_NEVERBLEED + , + neverbleed_t *nb +# endif // HAVE_NEVERBLEED +); +#endif // ENABLE_HTTP3 + +// Setups client side SSL_CTX. +SSL_CTX *setup_downstream_client_ssl_context( +#ifdef HAVE_NEVERBLEED + neverbleed_t *nb +#endif // HAVE_NEVERBLEED +); + +// Sets ALPN settings in |SSL| suitable for HTTP/2 use. +void setup_downstream_http2_alpn(SSL *ssl); +// Sets ALPN settings in |SSL| suitable for HTTP/1.1 use. +void setup_downstream_http1_alpn(SSL *ssl); + +// Creates CertLookupTree. If frontend is configured not to use TLS, +// this function returns nullptr. +std::unique_ptr<CertLookupTree> create_cert_lookup_tree(); + +SSL *create_ssl(SSL_CTX *ssl_ctx); + +// Returns true if SSL/TLS is enabled on upstream +bool upstream_tls_enabled(const ConnectionConfig &connconf); + +// Performs TLS hostname match. |pattern| can contain wildcard +// character '*', which matches prefix of target hostname. There are +// several restrictions to make wildcard work. The matching algorithm +// is based on RFC 6125. +bool tls_hostname_match(const StringRef &pattern, const StringRef &hostname); + +// Caches |session|. |session| is serialized into ASN1 +// representation, and stored. |t| is used as a time stamp. +// Depending on the existing cache's time stamp, |session| might not +// be cached. +void try_cache_tls_session(TLSSessionCache *cache, SSL_SESSION *session, + const std::chrono::steady_clock::time_point &t); + +// Returns cached session associated |addr|. If no cache entry is +// found associated to |addr|, nullptr will be returned. +SSL_SESSION *reuse_tls_session(const TLSSessionCache &addr); + +// Loads certificate form file |filename|. The caller should delete +// the returned object using X509_free(). +X509 *load_certificate(const char *filename); + +// Returns TLS version from |v|. The returned value is defined in +// OpenSSL header file. This function returns -1 if |v| is not valid +// TLS version string. +int proto_version_from_string(const StringRef &v); + +// Verifies OCSP response |ocsp_resp| of length |ocsp_resplen|. This +// function returns 0 if it succeeds, or -1. +int verify_ocsp_response(SSL_CTX *ssl_ctx, const uint8_t *ocsp_resp, + size_t ocsp_resplen); + +// Stores fingerprint of |x| in |dst| of length |dstlen|. |md| +// specifies hash function to use, and |dstlen| must be large enough +// to include hash value (e.g., 32 bytes for SHA-256). This function +// returns the number of bytes written in |dst|, or -1. +ssize_t get_x509_fingerprint(uint8_t *dst, size_t dstlen, const X509 *x, + const EVP_MD *md); + +// Returns subject name of |x|. If this function fails to get subject +// name, it returns an empty string. +StringRef get_x509_subject_name(BlockAllocator &balloc, X509 *x); + +// Returns issuer name of |x|. If this function fails to get issuer +// name, it returns an empty string. +StringRef get_x509_issuer_name(BlockAllocator &balloc, X509 *x); + +// Returns serial number of |x|. If this function fails to get serial +// number, it returns an empty string. number +StringRef get_x509_serial(BlockAllocator &balloc, X509 *x); + +// Fills NotBefore of |x| in |t|. This function returns 0 if it +// succeeds, or -1. +int get_x509_not_before(time_t &t, X509 *x); + +// Fills NotAfter of |x| in |t|. This function returns 0 if it +// succeeds, or -1. +int get_x509_not_after(time_t &t, X509 *x); + +} // namespace tls + +} // namespace shrpx + +#endif // SHRPX_TLS_H diff --git a/src/shrpx_tls_test.cc b/src/shrpx_tls_test.cc new file mode 100644 index 0000000..02fb168 --- /dev/null +++ b/src/shrpx_tls_test.cc @@ -0,0 +1,339 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_tls_test.h" + +#include <CUnit/CUnit.h> + +#include "shrpx_tls.h" +#include "shrpx_log.h" +#include "util.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +void test_shrpx_tls_create_lookup_tree(void) { + auto tree = std::make_unique<tls::CertLookupTree>(); + + constexpr StringRef hostnames[] = { + StringRef::from_lit("example.com"), // 0 + StringRef::from_lit("www.example.org"), // 1 + StringRef::from_lit("*www.example.org"), // 2 + StringRef::from_lit("xy*.host.domain"), // 3 + StringRef::from_lit("*yy.host.domain"), // 4 + StringRef::from_lit("nghttp2.sourceforge.net"), // 5 + StringRef::from_lit("sourceforge.net"), // 6 + StringRef::from_lit("sourceforge.net"), // 7, duplicate + StringRef::from_lit("*.foo.bar"), // 8, oo.bar is suffix of *.foo.bar + StringRef::from_lit("oo.bar") // 9 + }; + auto num = array_size(hostnames); + + for (size_t idx = 0; idx < num; ++idx) { + tree->add_cert(hostnames[idx], idx); + } + + tree->dump(); + + CU_ASSERT(0 == tree->lookup(hostnames[0])); + CU_ASSERT(1 == tree->lookup(hostnames[1])); + CU_ASSERT(2 == tree->lookup(StringRef::from_lit("2www.example.org"))); + CU_ASSERT(-1 == tree->lookup(StringRef::from_lit("www2.example.org"))); + CU_ASSERT(3 == tree->lookup(StringRef::from_lit("xy1.host.domain"))); + // Does not match *yy.host.domain, because * must match at least 1 + // character. + CU_ASSERT(-1 == tree->lookup(StringRef::from_lit("yy.host.domain"))); + CU_ASSERT(4 == tree->lookup(StringRef::from_lit("xyy.host.domain"))); + CU_ASSERT(-1 == tree->lookup(StringRef{})); + CU_ASSERT(5 == tree->lookup(hostnames[5])); + CU_ASSERT(6 == tree->lookup(hostnames[6])); + static constexpr char h6[] = "pdylay.sourceforge.net"; + for (int i = 0; i < 7; ++i) { + CU_ASSERT(-1 == tree->lookup(StringRef{h6 + i, str_size(h6) - i})); + } + CU_ASSERT(8 == tree->lookup(StringRef::from_lit("x.foo.bar"))); + CU_ASSERT(9 == tree->lookup(hostnames[9])); + + constexpr StringRef names[] = { + StringRef::from_lit("rab"), // 1 + StringRef::from_lit("zab"), // 2 + StringRef::from_lit("zzub"), // 3 + StringRef::from_lit("ab") // 4 + }; + num = array_size(names); + + tree = std::make_unique<tls::CertLookupTree>(); + for (size_t idx = 0; idx < num; ++idx) { + tree->add_cert(names[idx], idx); + } + for (size_t i = 0; i < num; ++i) { + CU_ASSERT((ssize_t)i == tree->lookup(names[i])); + } +} + +// We use cfssl to generate key pairs. +// +// CA self-signed key pairs generation: +// +// $ cfssl genkey -initca ca.nghttp2.org.csr.json | +// cfssljson -bare ca.nghttp2.org +// +// Create CSR: +// +// $ cfssl genkey test.nghttp2.org.csr.json | cfssljson -bare test.nghttp2.org +// $ cfssl genkey test.example.com.csr.json | cfssljson -bare test.example.com +// +// Sign CSR: +// +// $ cfssl sign -ca ca.nghttp2.org.pem -ca-key ca.nghttp2.org-key.pem +// -config=ca-config.json -profile=server test.nghttp2.org.csr | +// cfssljson -bare test.nghttp2.org +// +// $ cfssl sign -ca ca.nghttp2.org.pem -ca-key ca.nghttp2.org-key.pem +// -config=ca-config.json -profile=server test.example.com.csr | +// cfssljson -bare test.example.com +// +void test_shrpx_tls_cert_lookup_tree_add_ssl_ctx(void) { + int rv; + + static constexpr char nghttp2_certfile[] = + NGHTTP2_SRC_DIR "/test.nghttp2.org.pem"; + auto nghttp2_ssl_ctx = SSL_CTX_new(TLS_server_method()); + auto nghttp2_ssl_ctx_del = defer(SSL_CTX_free, nghttp2_ssl_ctx); + auto nghttp2_tls_ctx_data = std::make_unique<tls::TLSContextData>(); + nghttp2_tls_ctx_data->cert_file = nghttp2_certfile; + SSL_CTX_set_app_data(nghttp2_ssl_ctx, nghttp2_tls_ctx_data.get()); + rv = SSL_CTX_use_certificate_chain_file(nghttp2_ssl_ctx, nghttp2_certfile); + + CU_ASSERT(1 == rv); + + static constexpr char examples_certfile[] = + NGHTTP2_SRC_DIR "/test.example.com.pem"; + auto examples_ssl_ctx = SSL_CTX_new(TLS_server_method()); + auto examples_ssl_ctx_del = defer(SSL_CTX_free, examples_ssl_ctx); + auto examples_tls_ctx_data = std::make_unique<tls::TLSContextData>(); + examples_tls_ctx_data->cert_file = examples_certfile; + SSL_CTX_set_app_data(examples_ssl_ctx, examples_tls_ctx_data.get()); + rv = SSL_CTX_use_certificate_chain_file(examples_ssl_ctx, examples_certfile); + + CU_ASSERT(1 == rv); + + tls::CertLookupTree tree; + std::vector<std::vector<SSL_CTX *>> indexed_ssl_ctx; + + rv = tls::cert_lookup_tree_add_ssl_ctx(&tree, indexed_ssl_ctx, + nghttp2_ssl_ctx); + + CU_ASSERT(0 == rv); + + rv = tls::cert_lookup_tree_add_ssl_ctx(&tree, indexed_ssl_ctx, + examples_ssl_ctx); + + CU_ASSERT(0 == rv); + + CU_ASSERT(-1 == tree.lookup(StringRef::from_lit("not-used.nghttp2.org"))); + CU_ASSERT(0 == tree.lookup(StringRef::from_lit("test.nghttp2.org"))); + CU_ASSERT(1 == tree.lookup(StringRef::from_lit("w.test.nghttp2.org"))); + CU_ASSERT(2 == tree.lookup(StringRef::from_lit("www.test.nghttp2.org"))); + CU_ASSERT(3 == tree.lookup(StringRef::from_lit("test.example.com"))); +} + +template <size_t N, size_t M> +bool tls_hostname_match_wrapper(const char (&pattern)[N], + const char (&hostname)[M]) { + return tls::tls_hostname_match(StringRef{pattern, N}, StringRef{hostname, M}); +} + +void test_shrpx_tls_tls_hostname_match(void) { + CU_ASSERT(tls_hostname_match_wrapper("example.com", "example.com")); + CU_ASSERT(tls_hostname_match_wrapper("example.com", "EXAMPLE.com")); + + // check wildcard + CU_ASSERT(tls_hostname_match_wrapper("*.example.com", "www.example.com")); + CU_ASSERT(tls_hostname_match_wrapper("*w.example.com", "www.example.com")); + CU_ASSERT(tls_hostname_match_wrapper("www*.example.com", "www1.example.com")); + CU_ASSERT( + tls_hostname_match_wrapper("www*.example.com", "WWW12.EXAMPLE.com")); + // at least 2 dots are required after '*' + CU_ASSERT(!tls_hostname_match_wrapper("*.com", "example.com")); + CU_ASSERT(!tls_hostname_match_wrapper("*", "example.com")); + // '*' must be in left most label + CU_ASSERT( + !tls_hostname_match_wrapper("blog.*.example.com", "blog.my.example.com")); + // prefix is wrong + CU_ASSERT( + !tls_hostname_match_wrapper("client*.example.com", "server.example.com")); + // '*' must match at least one character + CU_ASSERT(!tls_hostname_match_wrapper("www*.example.com", "www.example.com")); + + CU_ASSERT(!tls_hostname_match_wrapper("example.com", "nghttp2.org")); + CU_ASSERT(!tls_hostname_match_wrapper("www.example.com", "example.com")); + CU_ASSERT(!tls_hostname_match_wrapper("example.com", "www.example.com")); +} + +static X509 *load_cert(const char *path) { + auto f = fopen(path, "r"); + auto cert = PEM_read_X509(f, nullptr, nullptr, nullptr); + + fclose(f); + + return cert; +} + +static Address parse_addr(const char *ipaddr) { + addrinfo hints{}; + + hints.ai_family = AF_UNSPEC; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + + addrinfo *res = nullptr; + + auto rv = getaddrinfo(ipaddr, "443", &hints, &res); + + CU_ASSERT(0 == rv); + CU_ASSERT(nullptr != res); + + Address addr; + addr.len = res->ai_addrlen; + memcpy(&addr.su, res->ai_addr, res->ai_addrlen); + + freeaddrinfo(res); + + return addr; +} + +void test_shrpx_tls_verify_numeric_hostname(void) { + { + // Successful IPv4 address match in SAN + static constexpr char ipaddr[] = "127.0.0.1"; + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/verify_hostname.crt"); + auto addr = parse_addr(ipaddr); + auto rv = + tls::verify_numeric_hostname(cert, StringRef::from_lit(ipaddr), &addr); + + CU_ASSERT(0 == rv); + + X509_free(cert); + } + + { + // Successful IPv6 address match in SAN + static constexpr char ipaddr[] = "::1"; + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/verify_hostname.crt"); + auto addr = parse_addr(ipaddr); + auto rv = + tls::verify_numeric_hostname(cert, StringRef::from_lit(ipaddr), &addr); + + CU_ASSERT(0 == rv); + + X509_free(cert); + } + + { + // Unsuccessful IPv4 address match in SAN + static constexpr char ipaddr[] = "192.168.0.127"; + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/verify_hostname.crt"); + auto addr = parse_addr(ipaddr); + auto rv = + tls::verify_numeric_hostname(cert, StringRef::from_lit(ipaddr), &addr); + + CU_ASSERT(-1 == rv); + + X509_free(cert); + } + + { + // CommonName is not used if SAN is available + static constexpr char ipaddr[] = "192.168.0.1"; + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/ipaddr.crt"); + auto addr = parse_addr(ipaddr); + auto rv = + tls::verify_numeric_hostname(cert, StringRef::from_lit(ipaddr), &addr); + + CU_ASSERT(-1 == rv); + + X509_free(cert); + } + + { + // Successful IPv4 address match in CommonName + static constexpr char ipaddr[] = "127.0.0.1"; + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/nosan_ip.crt"); + auto addr = parse_addr(ipaddr); + auto rv = + tls::verify_numeric_hostname(cert, StringRef::from_lit(ipaddr), &addr); + + CU_ASSERT(0 == rv); + + X509_free(cert); + } +} + +void test_shrpx_tls_verify_dns_hostname(void) { + { + // Successful exact DNS name match in SAN + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/verify_hostname.crt"); + auto rv = tls::verify_dns_hostname( + cert, StringRef::from_lit("nghttp2.example.com")); + + CU_ASSERT(0 == rv); + + X509_free(cert); + } + + { + // Successful wildcard DNS name match in SAN + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/verify_hostname.crt"); + auto rv = tls::verify_dns_hostname( + cert, StringRef::from_lit("www.nghttp2.example.com")); + + CU_ASSERT(0 == rv); + + X509_free(cert); + } + + { + // CommonName is not used if SAN is available. + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/verify_hostname.crt"); + auto rv = tls::verify_dns_hostname(cert, StringRef::from_lit("localhost")); + + CU_ASSERT(-1 == rv); + + X509_free(cert); + } + + { + // Successful DNS name match in CommonName + auto cert = load_cert(NGHTTP2_SRC_DIR "/testdata/nosan.crt"); + auto rv = tls::verify_dns_hostname(cert, StringRef::from_lit("localhost")); + + CU_ASSERT(0 == rv); + + X509_free(cert); + } +} + +} // namespace shrpx diff --git a/src/shrpx_tls_test.h b/src/shrpx_tls_test.h new file mode 100644 index 0000000..7edc742 --- /dev/null +++ b/src/shrpx_tls_test.h @@ -0,0 +1,42 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_TLS_TEST_H +#define SHRPX_TLS_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_shrpx_tls_create_lookup_tree(void); +void test_shrpx_tls_cert_lookup_tree_add_ssl_ctx(void); +void test_shrpx_tls_tls_hostname_match(void); +void test_shrpx_tls_verify_numeric_hostname(void); +void test_shrpx_tls_verify_dns_hostname(void); + +} // namespace shrpx + +#endif // SHRPX_TLS_TEST_H diff --git a/src/shrpx_upstream.h b/src/shrpx_upstream.h new file mode 100644 index 0000000..3da62c9 --- /dev/null +++ b/src/shrpx_upstream.h @@ -0,0 +1,112 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_UPSTREAM_H +#define SHRPX_UPSTREAM_H + +#include "shrpx.h" +#include "shrpx_io_control.h" +#include "memchunk.h" + +using namespace nghttp2; + +namespace shrpx { + +class ClientHandler; +class Downstream; +class DownstreamConnection; + +class Upstream { +public: + virtual ~Upstream() {} + virtual int on_read() = 0; + virtual int on_write() = 0; + virtual int on_timeout(Downstream *downstream) { return 0; }; + virtual int on_downstream_abort_request(Downstream *downstream, + unsigned int status_code) = 0; + // Called when the current request is aborted without forwarding it + // to backend, and it should be redirected to https URI. + virtual int + on_downstream_abort_request_with_https_redirect(Downstream *downstream) = 0; + virtual int downstream_read(DownstreamConnection *dconn) = 0; + virtual int downstream_write(DownstreamConnection *dconn) = 0; + virtual int downstream_eof(DownstreamConnection *dconn) = 0; + virtual int downstream_error(DownstreamConnection *dconn, int events) = 0; + virtual ClientHandler *get_client_handler() const = 0; + + virtual int on_downstream_header_complete(Downstream *downstream) = 0; + virtual int on_downstream_body(Downstream *downstream, const uint8_t *data, + size_t len, bool flush) = 0; + virtual int on_downstream_body_complete(Downstream *downstream) = 0; + + virtual void on_handler_delete() = 0; + // Called when downstream connection for |downstream| is reset. + // Currently this is only used by Http2Session. If |no_retry| is + // true, another connection attempt using new DownstreamConnection + // is not allowed. + virtual int on_downstream_reset(Downstream *downstream, bool no_retry) = 0; + + virtual void pause_read(IOCtrlReason reason) = 0; + virtual int resume_read(IOCtrlReason reason, Downstream *downstream, + size_t consumed) = 0; + virtual int send_reply(Downstream *downstream, const uint8_t *body, + size_t bodylen) = 0; + + // Starts server push. The |downstream| is an associated stream for + // the pushed resource. This function returns 0 if it succeeds, + // otherwise -1. + virtual int initiate_push(Downstream *downstream, const StringRef &uri) = 0; + + // Fills response data in |iov| whose capacity is |iovcnt|. Returns + // the number of iovs filled. + virtual int response_riovec(struct iovec *iov, int iovcnt) const = 0; + virtual void response_drain(size_t n) = 0; + virtual bool response_empty() const = 0; + + // Called when PUSH_PROMISE was started in downstream. The + // associated downstream is given as |downstream|. The promised + // stream ID is given as |promised_stream_id|. If upstream supports + // server push for the corresponding upstream connection, it should + // return Downstream object for pushed stream. Otherwise, returns + // nullptr. + virtual Downstream * + on_downstream_push_promise(Downstream *downstream, + int32_t promised_stream_id) = 0; + // Called when PUSH_PROMISE frame was completely received in + // downstream. The associated downstream is given as |downstream|. + // This function returns 0 if it succeeds, or -1. + virtual int + on_downstream_push_promise_complete(Downstream *downstream, + Downstream *promised_downstream) = 0; + // Returns true if server push is enabled in upstream connection. + virtual bool push_enabled() const = 0; + // Cancels promised downstream. This function is called when + // PUSH_PROMISE for |promised_downstream| is not submitted to + // upstream session. + virtual void cancel_premature_downstream(Downstream *promised_downstream) = 0; +}; + +} // namespace shrpx + +#endif // SHRPX_UPSTREAM_H diff --git a/src/shrpx_worker.cc b/src/shrpx_worker.cc new file mode 100644 index 0000000..4c069db --- /dev/null +++ b/src/shrpx_worker.cc @@ -0,0 +1,1347 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_worker.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <netinet/udp.h> + +#include <cstdio> +#include <memory> + +#include <openssl/rand.h> + +#ifdef HAVE_LIBBPF +# include <bpf/bpf.h> +# include <bpf/libbpf.h> +#endif // HAVE_LIBBPF + +#include "shrpx_tls.h" +#include "shrpx_log.h" +#include "shrpx_client_handler.h" +#include "shrpx_http2_session.h" +#include "shrpx_log_config.h" +#include "shrpx_memcached_dispatcher.h" +#ifdef HAVE_MRUBY +# include "shrpx_mruby.h" +#endif // HAVE_MRUBY +#ifdef ENABLE_HTTP3 +# include "shrpx_quic_listener.h" +#endif // ENABLE_HTTP3 +#include "shrpx_connection_handler.h" +#include "util.h" +#include "template.h" +#include "xsi_strerror.h" + +namespace shrpx { + +namespace { +void eventcb(struct ev_loop *loop, ev_async *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + worker->process_events(); +} +} // namespace + +namespace { +void mcpool_clear_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + if (worker->get_worker_stat()->num_connections != 0) { + return; + } + auto mcpool = worker->get_mcpool(); + if (mcpool->freelistsize == mcpool->poolsize) { + worker->get_mcpool()->clear(); + } +} +} // namespace + +namespace { +void proc_wev_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + worker->process_events(); +} +} // namespace + +DownstreamAddrGroup::DownstreamAddrGroup() : retired{false} {} + +DownstreamAddrGroup::~DownstreamAddrGroup() {} + +// DownstreamKey is used to index SharedDownstreamAddr in order to +// find the same configuration. +using DownstreamKey = std::tuple< + std::vector< + std::tuple<StringRef, StringRef, StringRef, size_t, size_t, Proto, + uint32_t, uint32_t, uint32_t, bool, bool, bool, bool>>, + bool, SessionAffinity, StringRef, StringRef, SessionAffinityCookieSecure, + SessionAffinityCookieStickiness, int64_t, int64_t, StringRef, bool>; + +namespace { +DownstreamKey +create_downstream_key(const std::shared_ptr<SharedDownstreamAddr> &shared_addr, + const StringRef &mruby_file) { + DownstreamKey dkey; + + auto &addrs = std::get<0>(dkey); + addrs.resize(shared_addr->addrs.size()); + auto p = std::begin(addrs); + for (auto &a : shared_addr->addrs) { + std::get<0>(*p) = a.host; + std::get<1>(*p) = a.sni; + std::get<2>(*p) = a.group; + std::get<3>(*p) = a.fall; + std::get<4>(*p) = a.rise; + std::get<5>(*p) = a.proto; + std::get<6>(*p) = a.port; + std::get<7>(*p) = a.weight; + std::get<8>(*p) = a.group_weight; + std::get<9>(*p) = a.host_unix; + std::get<10>(*p) = a.tls; + std::get<11>(*p) = a.dns; + std::get<12>(*p) = a.upgrade_scheme; + ++p; + } + std::sort(std::begin(addrs), std::end(addrs)); + + std::get<1>(dkey) = shared_addr->redirect_if_not_tls; + + auto &affinity = shared_addr->affinity; + std::get<2>(dkey) = affinity.type; + std::get<3>(dkey) = affinity.cookie.name; + std::get<4>(dkey) = affinity.cookie.path; + std::get<5>(dkey) = affinity.cookie.secure; + std::get<6>(dkey) = affinity.cookie.stickiness; + auto &timeout = shared_addr->timeout; + std::get<7>(dkey) = timeout.read; + std::get<8>(dkey) = timeout.write; + std::get<9>(dkey) = mruby_file; + std::get<10>(dkey) = shared_addr->dnf; + + return dkey; +} +} // namespace + +Worker::Worker(struct ev_loop *loop, SSL_CTX *sv_ssl_ctx, SSL_CTX *cl_ssl_ctx, + SSL_CTX *tls_session_cache_memcached_ssl_ctx, + tls::CertLookupTree *cert_tree, +#ifdef ENABLE_HTTP3 + SSL_CTX *quic_sv_ssl_ctx, tls::CertLookupTree *quic_cert_tree, + const uint8_t *cid_prefix, size_t cid_prefixlen, +# ifdef HAVE_LIBBPF + size_t index, +# endif // HAVE_LIBBPF +#endif // ENABLE_HTTP3 + const std::shared_ptr<TicketKeys> &ticket_keys, + ConnectionHandler *conn_handler, + std::shared_ptr<DownstreamConfig> downstreamconf) + : +#if defined(ENABLE_HTTP3) && defined(HAVE_LIBBPF) + index_{index}, +#endif // ENABLE_HTTP3 && HAVE_LIBBPF + randgen_(util::make_mt19937()), + worker_stat_{}, + dns_tracker_(loop, get_config()->conn.downstream->family), +#ifdef ENABLE_HTTP3 + quic_upstream_addrs_{get_config()->conn.quic_listener.addrs}, +#endif // ENABLE_HTTP3 + loop_(loop), + sv_ssl_ctx_(sv_ssl_ctx), + cl_ssl_ctx_(cl_ssl_ctx), + cert_tree_(cert_tree), + conn_handler_(conn_handler), +#ifdef ENABLE_HTTP3 + quic_sv_ssl_ctx_{quic_sv_ssl_ctx}, + quic_cert_tree_{quic_cert_tree}, + quic_conn_handler_{this}, +#endif // ENABLE_HTTP3 + ticket_keys_(ticket_keys), + connect_blocker_( + std::make_unique<ConnectBlocker>(randgen_, loop_, nullptr, nullptr)), + graceful_shutdown_(false) { +#ifdef ENABLE_HTTP3 + std::copy_n(cid_prefix, cid_prefixlen, std::begin(cid_prefix_)); +#endif // ENABLE_HTTP3 + + ev_async_init(&w_, eventcb); + w_.data = this; + ev_async_start(loop_, &w_); + + ev_timer_init(&mcpool_clear_timer_, mcpool_clear_cb, 0., 0.); + mcpool_clear_timer_.data = this; + + ev_timer_init(&proc_wev_timer_, proc_wev_cb, 0., 0.); + proc_wev_timer_.data = this; + + auto &session_cacheconf = get_config()->tls.session_cache; + + if (!session_cacheconf.memcached.host.empty()) { + session_cache_memcached_dispatcher_ = std::make_unique<MemcachedDispatcher>( + &session_cacheconf.memcached.addr, loop, + tls_session_cache_memcached_ssl_ctx, + StringRef{session_cacheconf.memcached.host}, &mcpool_, randgen_); + } + + replace_downstream_config(std::move(downstreamconf)); +} + +namespace { +void ensure_enqueue_addr( + std::priority_queue<WeightGroupEntry, std::vector<WeightGroupEntry>, + WeightGroupEntryGreater> &wgpq, + WeightGroup *wg, DownstreamAddr *addr) { + uint32_t cycle; + if (!wg->pq.empty()) { + auto &top = wg->pq.top(); + cycle = top.cycle; + } else { + cycle = 0; + } + + addr->cycle = cycle; + addr->pending_penalty = 0; + wg->pq.push(DownstreamAddrEntry{addr, addr->seq, addr->cycle}); + addr->queued = true; + + if (!wg->queued) { + if (!wgpq.empty()) { + auto &top = wgpq.top(); + cycle = top.cycle; + } else { + cycle = 0; + } + + wg->cycle = cycle; + wg->pending_penalty = 0; + wgpq.push(WeightGroupEntry{wg, wg->seq, wg->cycle}); + wg->queued = true; + } +} +} // namespace + +void Worker::replace_downstream_config( + std::shared_ptr<DownstreamConfig> downstreamconf) { + for (auto &g : downstream_addr_groups_) { + g->retired = true; + + auto &shared_addr = g->shared_addr; + for (auto &addr : shared_addr->addrs) { + addr.dconn_pool->remove_all(); + } + } + + downstreamconf_ = downstreamconf; + + // Making a copy is much faster with multiple thread on + // backendconfig API call. + auto groups = downstreamconf->addr_groups; + + downstream_addr_groups_ = + std::vector<std::shared_ptr<DownstreamAddrGroup>>(groups.size()); + + std::map<DownstreamKey, size_t> addr_groups_indexer; +#ifdef HAVE_MRUBY + // TODO It is a bit less efficient because + // mruby::create_mruby_context returns std::unique_ptr and we cannot + // use std::make_shared. + std::map<StringRef, std::shared_ptr<mruby::MRubyContext>> shared_mruby_ctxs; +#endif // HAVE_MRUBY + + for (size_t i = 0; i < groups.size(); ++i) { + auto &src = groups[i]; + auto &dst = downstream_addr_groups_[i]; + + dst = std::make_shared<DownstreamAddrGroup>(); + dst->pattern = + ImmutableString{std::begin(src.pattern), std::end(src.pattern)}; + + auto shared_addr = std::make_shared<SharedDownstreamAddr>(); + + shared_addr->addrs.resize(src.addrs.size()); + shared_addr->affinity.type = src.affinity.type; + if (src.affinity.type == SessionAffinity::COOKIE) { + shared_addr->affinity.cookie.name = + make_string_ref(shared_addr->balloc, src.affinity.cookie.name); + if (!src.affinity.cookie.path.empty()) { + shared_addr->affinity.cookie.path = + make_string_ref(shared_addr->balloc, src.affinity.cookie.path); + } + shared_addr->affinity.cookie.secure = src.affinity.cookie.secure; + shared_addr->affinity.cookie.stickiness = src.affinity.cookie.stickiness; + } + shared_addr->affinity_hash = src.affinity_hash; + shared_addr->affinity_hash_map = src.affinity_hash_map; + shared_addr->redirect_if_not_tls = src.redirect_if_not_tls; + shared_addr->dnf = src.dnf; + shared_addr->timeout.read = src.timeout.read; + shared_addr->timeout.write = src.timeout.write; + + for (size_t j = 0; j < src.addrs.size(); ++j) { + auto &src_addr = src.addrs[j]; + auto &dst_addr = shared_addr->addrs[j]; + + dst_addr.addr = src_addr.addr; + dst_addr.host = make_string_ref(shared_addr->balloc, src_addr.host); + dst_addr.hostport = + make_string_ref(shared_addr->balloc, src_addr.hostport); + dst_addr.port = src_addr.port; + dst_addr.host_unix = src_addr.host_unix; + dst_addr.weight = src_addr.weight; + dst_addr.group = make_string_ref(shared_addr->balloc, src_addr.group); + dst_addr.group_weight = src_addr.group_weight; + dst_addr.affinity_hash = src_addr.affinity_hash; + dst_addr.proto = src_addr.proto; + dst_addr.tls = src_addr.tls; + dst_addr.sni = make_string_ref(shared_addr->balloc, src_addr.sni); + dst_addr.fall = src_addr.fall; + dst_addr.rise = src_addr.rise; + dst_addr.dns = src_addr.dns; + dst_addr.upgrade_scheme = src_addr.upgrade_scheme; + } + +#ifdef HAVE_MRUBY + auto mruby_ctx_it = shared_mruby_ctxs.find(src.mruby_file); + if (mruby_ctx_it == std::end(shared_mruby_ctxs)) { + shared_addr->mruby_ctx = mruby::create_mruby_context(src.mruby_file); + assert(shared_addr->mruby_ctx); + shared_mruby_ctxs.emplace(src.mruby_file, shared_addr->mruby_ctx); + } else { + shared_addr->mruby_ctx = (*mruby_ctx_it).second; + } +#endif // HAVE_MRUBY + + // share the connection if patterns have the same set of backend + // addresses. + + auto dkey = create_downstream_key(shared_addr, src.mruby_file); + auto it = addr_groups_indexer.find(dkey); + + if (it == std::end(addr_groups_indexer)) { + auto shared_addr_ptr = shared_addr.get(); + + for (auto &addr : shared_addr->addrs) { + addr.connect_blocker = std::make_unique<ConnectBlocker>( + randgen_, loop_, nullptr, [shared_addr_ptr, &addr]() { + if (!addr.queued) { + if (!addr.wg) { + return; + } + ensure_enqueue_addr(shared_addr_ptr->pq, addr.wg, &addr); + } + }); + + addr.live_check = std::make_unique<LiveCheck>(loop_, cl_ssl_ctx_, this, + &addr, randgen_); + } + + size_t seq = 0; + for (auto &addr : shared_addr->addrs) { + addr.dconn_pool = std::make_unique<DownstreamConnectionPool>(); + addr.seq = seq++; + } + + util::shuffle(std::begin(shared_addr->addrs), + std::end(shared_addr->addrs), randgen_, + [](auto i, auto j) { std::swap((*i).seq, (*j).seq); }); + + if (shared_addr->affinity.type == SessionAffinity::NONE) { + std::map<StringRef, WeightGroup *> wgs; + size_t num_wgs = 0; + for (auto &addr : shared_addr->addrs) { + if (wgs.find(addr.group) == std::end(wgs)) { + ++num_wgs; + wgs.emplace(addr.group, nullptr); + } + } + + shared_addr->wgs = std::vector<WeightGroup>(num_wgs); + + for (auto &addr : shared_addr->addrs) { + auto &wg = wgs[addr.group]; + if (wg == nullptr) { + wg = &shared_addr->wgs[--num_wgs]; + wg->seq = num_wgs; + } + + wg->weight = addr.group_weight; + wg->pq.push(DownstreamAddrEntry{&addr, addr.seq, addr.cycle}); + addr.queued = true; + addr.wg = wg; + } + + assert(num_wgs == 0); + + for (auto &kv : wgs) { + shared_addr->pq.push( + WeightGroupEntry{kv.second, kv.second->seq, kv.second->cycle}); + kv.second->queued = true; + } + } + + dst->shared_addr = std::move(shared_addr); + + addr_groups_indexer.emplace(std::move(dkey), i); + } else { + auto &g = *(std::begin(downstream_addr_groups_) + (*it).second); + if (LOG_ENABLED(INFO)) { + LOG(INFO) << dst->pattern << " shares the same backend group with " + << g->pattern; + } + dst->shared_addr = g->shared_addr; + } + } +} + +Worker::~Worker() { + ev_async_stop(loop_, &w_); + ev_timer_stop(loop_, &mcpool_clear_timer_); + ev_timer_stop(loop_, &proc_wev_timer_); +} + +void Worker::schedule_clear_mcpool() { + // libev manual says: "If the watcher is already active nothing will + // happen." Since we don't change any timeout here, we don't have + // to worry about querying ev_is_active. + ev_timer_start(loop_, &mcpool_clear_timer_); +} + +void Worker::wait() { +#ifndef NOTHREADS + fut_.get(); +#endif // !NOTHREADS +} + +void Worker::run_async() { +#ifndef NOTHREADS + fut_ = std::async(std::launch::async, [this] { + (void)reopen_log_files(get_config()->logging); + ev_run(loop_); + delete_log_config(); + }); +#endif // !NOTHREADS +} + +void Worker::send(WorkerEvent event) { + { + std::lock_guard<std::mutex> g(m_); + + q_.emplace_back(std::move(event)); + } + + ev_async_send(loop_, &w_); +} + +void Worker::process_events() { + WorkerEvent wev; + { + std::lock_guard<std::mutex> g(m_); + + // Process event one at a time. This is important for + // WorkerEventType::NEW_CONNECTION event since accepting large + // number of new connections at once may delay time to 1st byte + // for existing connections. + + if (q_.empty()) { + ev_timer_stop(loop_, &proc_wev_timer_); + return; + } + + wev = std::move(q_.front()); + q_.pop_front(); + } + + ev_timer_start(loop_, &proc_wev_timer_); + + auto config = get_config(); + + auto worker_connections = config->conn.upstream.worker_connections; + + switch (wev.type) { + case WorkerEventType::NEW_CONNECTION: { + if (LOG_ENABLED(INFO)) { + WLOG(INFO, this) << "WorkerEvent: client_fd=" << wev.client_fd + << ", addrlen=" << wev.client_addrlen; + } + + if (worker_stat_.num_connections >= worker_connections) { + + if (LOG_ENABLED(INFO)) { + WLOG(INFO, this) << "Too many connections >= " << worker_connections; + } + + close(wev.client_fd); + + break; + } + + auto client_handler = + tls::accept_connection(this, wev.client_fd, &wev.client_addr.sa, + wev.client_addrlen, wev.faddr); + if (!client_handler) { + if (LOG_ENABLED(INFO)) { + WLOG(ERROR, this) << "ClientHandler creation failed"; + } + close(wev.client_fd); + break; + } + + if (LOG_ENABLED(INFO)) { + WLOG(INFO, this) << "CLIENT_HANDLER:" << client_handler << " created"; + } + + break; + } + case WorkerEventType::REOPEN_LOG: + WLOG(NOTICE, this) << "Reopening log files: worker process (thread " << this + << ")"; + + reopen_log_files(config->logging); + + break; + case WorkerEventType::GRACEFUL_SHUTDOWN: + WLOG(NOTICE, this) << "Graceful shutdown commencing"; + + graceful_shutdown_ = true; + + if (worker_stat_.num_connections == 0 && + worker_stat_.num_close_waits == 0) { + ev_break(loop_); + + return; + } + + break; + case WorkerEventType::REPLACE_DOWNSTREAM: + WLOG(NOTICE, this) << "Replace downstream"; + + replace_downstream_config(wev.downstreamconf); + + break; +#ifdef ENABLE_HTTP3 + case WorkerEventType::QUIC_PKT_FORWARD: { + const UpstreamAddr *faddr; + + if (wev.quic_pkt->upstream_addr_index == static_cast<size_t>(-1)) { + faddr = find_quic_upstream_addr(wev.quic_pkt->local_addr); + if (faddr == nullptr) { + LOG(ERROR) << "No suitable upstream address found"; + + break; + } + } else if (quic_upstream_addrs_.size() <= + wev.quic_pkt->upstream_addr_index) { + LOG(ERROR) << "upstream_addr_index is too large"; + + break; + } else { + faddr = &quic_upstream_addrs_[wev.quic_pkt->upstream_addr_index]; + } + + quic_conn_handler_.handle_packet( + faddr, wev.quic_pkt->remote_addr, wev.quic_pkt->local_addr, + wev.quic_pkt->pi, wev.quic_pkt->data.data(), wev.quic_pkt->data.size()); + + break; + } +#endif // ENABLE_HTTP3 + default: + if (LOG_ENABLED(INFO)) { + WLOG(INFO, this) << "unknown event type " << static_cast<int>(wev.type); + } + } +} + +tls::CertLookupTree *Worker::get_cert_lookup_tree() const { return cert_tree_; } + +#ifdef ENABLE_HTTP3 +tls::CertLookupTree *Worker::get_quic_cert_lookup_tree() const { + return quic_cert_tree_; +} +#endif // ENABLE_HTTP3 + +std::shared_ptr<TicketKeys> Worker::get_ticket_keys() { +#ifdef HAVE_ATOMIC_STD_SHARED_PTR + return std::atomic_load_explicit(&ticket_keys_, std::memory_order_acquire); +#else // !HAVE_ATOMIC_STD_SHARED_PTR + std::lock_guard<std::mutex> g(ticket_keys_m_); + return ticket_keys_; +#endif // !HAVE_ATOMIC_STD_SHARED_PTR +} + +void Worker::set_ticket_keys(std::shared_ptr<TicketKeys> ticket_keys) { +#ifdef HAVE_ATOMIC_STD_SHARED_PTR + // This is single writer + std::atomic_store_explicit(&ticket_keys_, std::move(ticket_keys), + std::memory_order_release); +#else // !HAVE_ATOMIC_STD_SHARED_PTR + std::lock_guard<std::mutex> g(ticket_keys_m_); + ticket_keys_ = std::move(ticket_keys); +#endif // !HAVE_ATOMIC_STD_SHARED_PTR +} + +WorkerStat *Worker::get_worker_stat() { return &worker_stat_; } + +struct ev_loop *Worker::get_loop() const { return loop_; } + +SSL_CTX *Worker::get_sv_ssl_ctx() const { return sv_ssl_ctx_; } + +SSL_CTX *Worker::get_cl_ssl_ctx() const { return cl_ssl_ctx_; } + +#ifdef ENABLE_HTTP3 +SSL_CTX *Worker::get_quic_sv_ssl_ctx() const { return quic_sv_ssl_ctx_; } +#endif // ENABLE_HTTP3 + +void Worker::set_graceful_shutdown(bool f) { graceful_shutdown_ = f; } + +bool Worker::get_graceful_shutdown() const { return graceful_shutdown_; } + +MemchunkPool *Worker::get_mcpool() { return &mcpool_; } + +MemcachedDispatcher *Worker::get_session_cache_memcached_dispatcher() { + return session_cache_memcached_dispatcher_.get(); +} + +std::mt19937 &Worker::get_randgen() { return randgen_; } + +#ifdef HAVE_MRUBY +int Worker::create_mruby_context() { + mruby_ctx_ = mruby::create_mruby_context(StringRef{get_config()->mruby_file}); + if (!mruby_ctx_) { + return -1; + } + + return 0; +} + +mruby::MRubyContext *Worker::get_mruby_context() const { + return mruby_ctx_.get(); +} +#endif // HAVE_MRUBY + +std::vector<std::shared_ptr<DownstreamAddrGroup>> & +Worker::get_downstream_addr_groups() { + return downstream_addr_groups_; +} + +ConnectBlocker *Worker::get_connect_blocker() const { + return connect_blocker_.get(); +} + +const DownstreamConfig *Worker::get_downstream_config() const { + return downstreamconf_.get(); +} + +ConnectionHandler *Worker::get_connection_handler() const { + return conn_handler_; +} + +#ifdef ENABLE_HTTP3 +QUICConnectionHandler *Worker::get_quic_connection_handler() { + return &quic_conn_handler_; +} +#endif // ENABLE_HTTP3 + +DNSTracker *Worker::get_dns_tracker() { return &dns_tracker_; } + +#ifdef ENABLE_HTTP3 +# ifdef HAVE_LIBBPF +bool Worker::should_attach_bpf() const { + auto config = get_config(); + auto &quicconf = config->quic; + auto &apiconf = config->api; + + if (quicconf.bpf.disabled) { + return false; + } + + if (!config->single_thread && apiconf.enabled) { + return index_ == 1; + } + + return index_ == 0; +} + +bool Worker::should_update_bpf_map() const { + auto config = get_config(); + auto &quicconf = config->quic; + + return !quicconf.bpf.disabled; +} + +uint32_t Worker::compute_sk_index() const { + auto config = get_config(); + auto &apiconf = config->api; + + if (!config->single_thread && apiconf.enabled) { + return index_ - 1; + } + + return index_; +} +# endif // HAVE_LIBBPF + +int Worker::setup_quic_server_socket() { + size_t n = 0; + + for (auto &addr : quic_upstream_addrs_) { + assert(!addr.host_unix); + if (create_quic_server_socket(addr) != 0) { + return -1; + } + + // Make sure that each endpoint has a unique address. + for (size_t i = 0; i < n; ++i) { + const auto &a = quic_upstream_addrs_[i]; + + if (addr.hostport == a.hostport) { + LOG(FATAL) + << "QUIC frontend endpoint must be unique: a duplicate found for " + << addr.hostport; + + return -1; + } + } + + ++n; + + quic_listeners_.emplace_back(std::make_unique<QUICListener>(&addr, this)); + } + + return 0; +} + +int Worker::create_quic_server_socket(UpstreamAddr &faddr) { + std::array<char, STRERROR_BUFSIZE> errbuf; + int fd = -1; + int rv; + + auto service = util::utos(faddr.port); + addrinfo hints{}; + hints.ai_family = faddr.family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; +# ifdef AI_ADDRCONFIG + hints.ai_flags |= AI_ADDRCONFIG; +# endif // AI_ADDRCONFIG + + auto node = + faddr.host == StringRef::from_lit("*") ? nullptr : faddr.host.c_str(); + + addrinfo *res, *rp; + rv = getaddrinfo(node, service.c_str(), &hints, &res); +# ifdef AI_ADDRCONFIG + if (rv != 0) { + // Retry without AI_ADDRCONFIG + hints.ai_flags &= ~AI_ADDRCONFIG; + rv = getaddrinfo(node, service.c_str(), &hints, &res); + } +# endif // AI_ADDRCONFIG + if (rv != 0) { + LOG(FATAL) << "Unable to get IPv" << (faddr.family == AF_INET ? "4" : "6") + << " address for " << faddr.host << ", port " << faddr.port + << ": " << gai_strerror(rv); + return -1; + } + + auto res_d = defer(freeaddrinfo, res); + + std::array<char, NI_MAXHOST> host; + + for (rp = res; rp; rp = rp->ai_next) { + rv = getnameinfo(rp->ai_addr, rp->ai_addrlen, host.data(), host.size(), + nullptr, 0, NI_NUMERICHOST); + if (rv != 0) { + LOG(WARN) << "getnameinfo() failed: " << gai_strerror(rv); + continue; + } + +# ifdef SOCK_NONBLOCK + fd = socket(rp->ai_family, rp->ai_socktype | SOCK_NONBLOCK | SOCK_CLOEXEC, + rp->ai_protocol); + if (fd == -1) { + auto error = errno; + LOG(WARN) << "socket() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + continue; + } +# else // !SOCK_NONBLOCK + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd == -1) { + auto error = errno; + LOG(WARN) << "socket() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + continue; + } + util::make_socket_nonblocking(fd); + util::make_socket_closeonexec(fd); +# endif // !SOCK_NONBLOCK + + int val = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set SO_REUSEADDR option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set SO_REUSEPORT option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + + if (faddr.family == AF_INET6) { +# ifdef IPV6_V6ONLY + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set IPV6_V6ONLY option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } +# endif // IPV6_V6ONLY + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) + << "Failed to set IPV6_RECVPKTINFO option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_RECVTCLASS, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set IPV6_RECVTCLASS option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + +# if defined(IPV6_MTU_DISCOVER) && defined(IPV6_PMTUDISC_DO) + int mtu_disc = IPV6_PMTUDISC_DO; + if (setsockopt(fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, &mtu_disc, + static_cast<socklen_t>(sizeof(mtu_disc))) == -1) { + auto error = errno; + LOG(WARN) + << "Failed to set IPV6_MTU_DISCOVER option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } +# endif // defined(IPV6_MTU_DISCOVER) && defined(IP_PMTUDISC_DO) + } else { + if (setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set IP_PKTINFO option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + + if (setsockopt(fd, IPPROTO_IP, IP_RECVTOS, &val, + static_cast<socklen_t>(sizeof(val))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set IP_RECVTOS option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + +# if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO) + int mtu_disc = IP_PMTUDISC_DO; + if (setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, &mtu_disc, + static_cast<socklen_t>(sizeof(mtu_disc))) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set IP_MTU_DISCOVER option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } +# endif // defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO) + } + +# ifdef UDP_GRO + if (setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, sizeof(val)) == -1) { + auto error = errno; + LOG(WARN) << "Failed to set UDP_GRO option to listener socket: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } +# endif // UDP_GRO + + if (bind(fd, rp->ai_addr, rp->ai_addrlen) == -1) { + auto error = errno; + LOG(WARN) << "bind() syscall failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + continue; + } + +# ifdef HAVE_LIBBPF + auto config = get_config(); + + auto &quic_bpf_refs = conn_handler_->get_quic_bpf_refs(); + + if (should_attach_bpf()) { + auto &bpfconf = config->quic.bpf; + + auto obj = bpf_object__open_file(bpfconf.prog_file.c_str(), nullptr); + if (!obj) { + auto error = errno; + LOG(FATAL) << "Failed to open bpf object file: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + rv = bpf_object__load(obj); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Failed to load bpf object file: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + auto prog = bpf_object__find_program_by_name(obj, "select_reuseport"); + if (!prog) { + auto error = errno; + LOG(FATAL) << "Failed to find sk_reuseport program: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + auto &ref = quic_bpf_refs[faddr.index]; + + ref.obj = obj; + + ref.reuseport_array = + bpf_object__find_map_by_name(obj, "reuseport_array"); + if (!ref.reuseport_array) { + auto error = errno; + LOG(FATAL) << "Failed to get reuseport_array: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + ref.cid_prefix_map = bpf_object__find_map_by_name(obj, "cid_prefix_map"); + if (!ref.cid_prefix_map) { + auto error = errno; + LOG(FATAL) << "Failed to get cid_prefix_map: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + auto sk_info = bpf_object__find_map_by_name(obj, "sk_info"); + if (!sk_info) { + auto error = errno; + LOG(FATAL) << "Failed to get sk_info: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + constexpr uint32_t zero = 0; + uint64_t num_socks = config->num_worker; + + rv = bpf_map__update_elem(sk_info, &zero, sizeof(zero), &num_socks, + sizeof(num_socks), BPF_ANY); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Failed to update sk_info: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + constexpr uint32_t key_high_idx = 1; + constexpr uint32_t key_low_idx = 2; + + auto &qkms = conn_handler_->get_quic_keying_materials(); + auto &qkm = qkms->keying_materials.front(); + + rv = bpf_map__update_elem(sk_info, &key_high_idx, sizeof(key_high_idx), + qkm.cid_encryption_key.data(), + qkm.cid_encryption_key.size() / 2, BPF_ANY); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Failed to update key_high_idx sk_info: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + rv = bpf_map__update_elem(sk_info, &key_low_idx, sizeof(key_low_idx), + qkm.cid_encryption_key.data() + + qkm.cid_encryption_key.size() / 2, + qkm.cid_encryption_key.size() / 2, BPF_ANY); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Failed to update key_low_idx sk_info: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + auto prog_fd = bpf_program__fd(prog); + + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF, &prog_fd, + static_cast<socklen_t>(sizeof(prog_fd))) == -1) { + LOG(FATAL) << "Failed to attach bpf program: " + << xsi_strerror(errno, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + } + + if (should_update_bpf_map()) { + const auto &ref = quic_bpf_refs[faddr.index]; + auto sk_index = compute_sk_index(); + + rv = bpf_map__update_elem(ref.reuseport_array, &sk_index, + sizeof(sk_index), &fd, sizeof(fd), BPF_NOEXIST); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Failed to update reuseport_array: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + + rv = bpf_map__update_elem(ref.cid_prefix_map, cid_prefix_.data(), + cid_prefix_.size(), &sk_index, sizeof(sk_index), + BPF_NOEXIST); + if (rv != 0) { + auto error = errno; + LOG(FATAL) << "Failed to update cid_prefix_map: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + close(fd); + return -1; + } + } +# endif // HAVE_LIBBPF + + break; + } + + if (!rp) { + LOG(FATAL) << "Listening " << (faddr.family == AF_INET ? "IPv4" : "IPv6") + << " socket failed"; + + return -1; + } + + faddr.fd = fd; + faddr.hostport = util::make_http_hostport(mod_config()->balloc, + StringRef{host.data()}, faddr.port); + + LOG(NOTICE) << "Listening on " << faddr.hostport << ", quic"; + + return 0; +} + +const uint8_t *Worker::get_cid_prefix() const { return cid_prefix_.data(); } + +const UpstreamAddr *Worker::find_quic_upstream_addr(const Address &local_addr) { + std::array<char, NI_MAXHOST> host; + + auto rv = getnameinfo(&local_addr.su.sa, local_addr.len, host.data(), + host.size(), nullptr, 0, NI_NUMERICHOST); + if (rv != 0) { + LOG(ERROR) << "getnameinfo: " << gai_strerror(rv); + + return nullptr; + } + + uint16_t port; + + switch (local_addr.su.sa.sa_family) { + case AF_INET: + port = htons(local_addr.su.in.sin_port); + + break; + case AF_INET6: + port = htons(local_addr.su.in6.sin6_port); + + break; + default: + assert(0); + abort(); + } + + std::array<char, util::max_hostport> hostport_buf; + + auto hostport = util::make_http_hostport(std::begin(hostport_buf), + StringRef{host.data()}, port); + const UpstreamAddr *fallback_faddr = nullptr; + + for (auto &faddr : quic_upstream_addrs_) { + if (faddr.hostport == hostport) { + return &faddr; + } + + if (faddr.port != port || faddr.family != local_addr.su.sa.sa_family) { + continue; + } + + if (faddr.port == 443 || faddr.port == 80) { + switch (faddr.family) { + case AF_INET: + if (util::streq(faddr.hostport, StringRef::from_lit("0.0.0.0"))) { + fallback_faddr = &faddr; + } + + break; + case AF_INET6: + if (util::streq(faddr.hostport, StringRef::from_lit("[::]"))) { + fallback_faddr = &faddr; + } + + break; + default: + assert(0); + } + } else { + switch (faddr.family) { + case AF_INET: + if (util::starts_with(faddr.hostport, + StringRef::from_lit("0.0.0.0:"))) { + fallback_faddr = &faddr; + } + + break; + case AF_INET6: + if (util::starts_with(faddr.hostport, StringRef::from_lit("[::]:"))) { + fallback_faddr = &faddr; + } + + break; + default: + assert(0); + } + } + } + + return fallback_faddr; +} +#endif // ENABLE_HTTP3 + +namespace { +size_t match_downstream_addr_group_host( + const RouterConfig &routerconf, const StringRef &host, + const StringRef &path, + const std::vector<std::shared_ptr<DownstreamAddrGroup>> &groups, + size_t catch_all, BlockAllocator &balloc) { + + const auto &router = routerconf.router; + const auto &rev_wildcard_router = routerconf.rev_wildcard_router; + const auto &wildcard_patterns = routerconf.wildcard_patterns; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Perform mapping selection, using host=" << host + << ", path=" << path; + } + + auto group = router.match(host, path); + if (group != -1) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Found pattern with query " << host << path + << ", matched pattern=" << groups[group]->pattern; + } + return group; + } + + if (!wildcard_patterns.empty() && !host.empty()) { + auto rev_host_src = make_byte_ref(balloc, host.size() - 1); + auto ep = + std::copy(std::begin(host) + 1, std::end(host), rev_host_src.base); + std::reverse(rev_host_src.base, ep); + auto rev_host = StringRef{rev_host_src.base, ep}; + + ssize_t best_group = -1; + const RNode *last_node = nullptr; + + for (;;) { + size_t nread = 0; + auto wcidx = + rev_wildcard_router.match_prefix(&nread, &last_node, rev_host); + if (wcidx == -1) { + break; + } + + rev_host = StringRef{std::begin(rev_host) + nread, std::end(rev_host)}; + + auto &wc = wildcard_patterns[wcidx]; + auto group = wc.router.match(StringRef{}, path); + if (group != -1) { + // We sorted wildcard_patterns in a way that first match is the + // longest host pattern. + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Found wildcard pattern with query " << host << path + << ", matched pattern=" << groups[group]->pattern; + } + + best_group = group; + } + } + + if (best_group != -1) { + return best_group; + } + } + + group = router.match(StringRef::from_lit(""), path); + if (group != -1) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Found pattern with query " << path + << ", matched pattern=" << groups[group]->pattern; + } + return group; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "None match. Use catch-all pattern"; + } + return catch_all; +} +} // namespace + +size_t match_downstream_addr_group( + const RouterConfig &routerconf, const StringRef &hostport, + const StringRef &raw_path, + const std::vector<std::shared_ptr<DownstreamAddrGroup>> &groups, + size_t catch_all, BlockAllocator &balloc) { + if (std::find(std::begin(hostport), std::end(hostport), '/') != + std::end(hostport)) { + // We use '/' specially, and if '/' is included in host, it breaks + // our code. Select catch-all case. + return catch_all; + } + + auto fragment = std::find(std::begin(raw_path), std::end(raw_path), '#'); + auto query = std::find(std::begin(raw_path), fragment, '?'); + auto path = StringRef{std::begin(raw_path), query}; + + if (path.empty() || path[0] != '/') { + path = StringRef::from_lit("/"); + } + + if (hostport.empty()) { + return match_downstream_addr_group_host(routerconf, hostport, path, groups, + catch_all, balloc); + } + + StringRef host; + if (hostport[0] == '[') { + // assume this is IPv6 numeric address + auto p = std::find(std::begin(hostport), std::end(hostport), ']'); + if (p == std::end(hostport)) { + return catch_all; + } + if (p + 1 < std::end(hostport) && *(p + 1) != ':') { + return catch_all; + } + host = StringRef{std::begin(hostport), p + 1}; + } else { + auto p = std::find(std::begin(hostport), std::end(hostport), ':'); + if (p == std::begin(hostport)) { + return catch_all; + } + host = StringRef{std::begin(hostport), p}; + } + + if (std::find_if(std::begin(host), std::end(host), [](char c) { + return 'A' <= c || c <= 'Z'; + }) != std::end(host)) { + auto low_host = make_byte_ref(balloc, host.size() + 1); + auto ep = std::copy(std::begin(host), std::end(host), low_host.base); + *ep = '\0'; + util::inp_strlower(low_host.base, ep); + host = StringRef{low_host.base, ep}; + } + return match_downstream_addr_group_host(routerconf, host, path, groups, + catch_all, balloc); +} + +void downstream_failure(DownstreamAddr *addr, const Address *raddr) { + const auto &connect_blocker = addr->connect_blocker; + + if (connect_blocker->in_offline()) { + return; + } + + connect_blocker->on_failure(); + + if (addr->fall == 0) { + return; + } + + auto fail_count = connect_blocker->get_fail_count(); + + if (fail_count >= addr->fall) { + if (raddr) { + LOG(WARN) << "Could not connect to " << util::to_numeric_addr(raddr) + << " " << fail_count + << " times in a row; considered as offline"; + } else { + LOG(WARN) << "Could not connect to " << addr->host << ":" << addr->port + << " " << fail_count + << " times in a row; considered as offline"; + } + + connect_blocker->offline(); + + if (addr->rise) { + addr->live_check->schedule(); + } + } +} + +#ifdef ENABLE_HTTP3 +int create_cid_prefix(uint8_t *cid_prefix, const uint8_t *server_id) { + auto p = std::copy_n(server_id, SHRPX_QUIC_SERVER_IDLEN, cid_prefix); + + if (RAND_bytes(p, SHRPX_QUIC_CID_PREFIXLEN - SHRPX_QUIC_SERVER_IDLEN) != 1) { + return -1; + } + + return 0; +} +#endif // ENABLE_HTTP3 + +} // namespace shrpx diff --git a/src/shrpx_worker.h b/src/shrpx_worker.h new file mode 100644 index 0000000..3cc7b57 --- /dev/null +++ b/src/shrpx_worker.h @@ -0,0 +1,480 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_WORKER_H +#define SHRPX_WORKER_H + +#include "shrpx.h" + +#include <mutex> +#include <vector> +#include <random> +#include <unordered_map> +#include <deque> +#include <thread> +#include <queue> +#ifndef NOTHREADS +# include <future> +#endif // NOTHREADS + +#include <openssl/ssl.h> +#include <openssl/err.h> + +#include <ev.h> + +#include "shrpx_config.h" +#include "shrpx_downstream_connection_pool.h" +#include "memchunk.h" +#include "shrpx_tls.h" +#include "shrpx_live_check.h" +#include "shrpx_connect_blocker.h" +#include "shrpx_dns_tracker.h" +#ifdef ENABLE_HTTP3 +# include "shrpx_quic_connection_handler.h" +# include "shrpx_quic.h" +#endif // ENABLE_HTTP3 +#include "allocator.h" + +using namespace nghttp2; + +namespace shrpx { + +class Http2Session; +class ConnectBlocker; +class MemcachedDispatcher; +struct UpstreamAddr; +class ConnectionHandler; +#ifdef ENABLE_HTTP3 +class QUICListener; +#endif // ENABLE_HTTP3 + +#ifdef HAVE_MRUBY +namespace mruby { + +class MRubyContext; + +} // namespace mruby +#endif // HAVE_MRUBY + +namespace tls { +class CertLookupTree; +} // namespace tls + +struct WeightGroup; + +struct DownstreamAddr { + Address addr; + // backend address. If |host_unix| is true, this is UNIX domain + // socket path. + StringRef host; + StringRef hostport; + // backend port. 0 if |host_unix| is true. + uint16_t port; + // true if |host| contains UNIX domain socket path. + bool host_unix; + + // sni field to send remote server if TLS is enabled. + StringRef sni; + + std::unique_ptr<ConnectBlocker> connect_blocker; + std::unique_ptr<LiveCheck> live_check; + // Connection pool for this particular address if session affinity + // is enabled + std::unique_ptr<DownstreamConnectionPool> dconn_pool; + size_t fall; + size_t rise; + // Client side TLS session cache + tls::TLSSessionCache tls_session_cache; + // List of Http2Session which is not fully utilized (i.e., the + // server advertised maximum concurrency is not reached). We will + // coalesce as much stream as possible in one Http2Session to fully + // utilize TCP connection. + DList<Http2Session> http2_extra_freelist; + WeightGroup *wg; + // total number of streams created in HTTP/2 connections for this + // address. + size_t num_dconn; + // the sequence number of this address to randomize the order access + // threads. + size_t seq; + // Application protocol used in this backend + Proto proto; + // cycle is used to prioritize this address. Lower value takes + // higher priority. + uint32_t cycle; + // penalty which is applied to the next cycle calculation. + uint32_t pending_penalty; + // Weight of this address inside a weight group. Its range is [1, + // 256], inclusive. + uint32_t weight; + // name of group which this address belongs to. + StringRef group; + // Weight of the weight group which this address belongs to. Its + // range is [1, 256], inclusive. + uint32_t group_weight; + // affinity hash for this address. It is assigned when strict + // stickiness is enabled. + uint32_t affinity_hash; + // true if TLS is used in this backend + bool tls; + // true if dynamic DNS is enabled + bool dns; + // true if :scheme pseudo header field should be upgraded to secure + // variant (e.g., "https") when forwarding request to a backend + // connected by TLS connection. + bool upgrade_scheme; + // true if this address is queued. + bool queued; +}; + +constexpr uint32_t MAX_DOWNSTREAM_ADDR_WEIGHT = 256; + +struct DownstreamAddrEntry { + DownstreamAddr *addr; + size_t seq; + uint32_t cycle; +}; + +struct DownstreamAddrEntryGreater { + bool operator()(const DownstreamAddrEntry &lhs, + const DownstreamAddrEntry &rhs) const { + auto d = lhs.cycle - rhs.cycle; + if (d == 0) { + return rhs.seq < lhs.seq; + } + return d <= 2 * MAX_DOWNSTREAM_ADDR_WEIGHT - 1; + } +}; + +struct WeightGroup { + std::priority_queue<DownstreamAddrEntry, std::vector<DownstreamAddrEntry>, + DownstreamAddrEntryGreater> + pq; + size_t seq; + uint32_t weight; + uint32_t cycle; + uint32_t pending_penalty; + // true if this object is queued. + bool queued; +}; + +struct WeightGroupEntry { + WeightGroup *wg; + size_t seq; + uint32_t cycle; +}; + +struct WeightGroupEntryGreater { + bool operator()(const WeightGroupEntry &lhs, + const WeightGroupEntry &rhs) const { + auto d = lhs.cycle - rhs.cycle; + if (d == 0) { + return rhs.seq < lhs.seq; + } + return d <= 2 * MAX_DOWNSTREAM_ADDR_WEIGHT - 1; + } +}; + +struct SharedDownstreamAddr { + SharedDownstreamAddr() + : balloc(1024, 1024), + affinity{SessionAffinity::NONE}, + redirect_if_not_tls{false}, + dnf{false}, + timeout{} {} + + SharedDownstreamAddr(const SharedDownstreamAddr &) = delete; + SharedDownstreamAddr(SharedDownstreamAddr &&) = delete; + SharedDownstreamAddr &operator=(const SharedDownstreamAddr &) = delete; + SharedDownstreamAddr &operator=(SharedDownstreamAddr &&) = delete; + + BlockAllocator balloc; + std::vector<DownstreamAddr> addrs; + std::vector<WeightGroup> wgs; + std::priority_queue<WeightGroupEntry, std::vector<WeightGroupEntry>, + WeightGroupEntryGreater> + pq; + // Bunch of session affinity hash. Only used if affinity == + // SessionAffinity::IP. + std::vector<AffinityHash> affinity_hash; + // Maps affinity hash of each DownstreamAddr to its index in addrs. + // It is only assigned when strict stickiness is enabled. + std::unordered_map<uint32_t, size_t> affinity_hash_map; +#ifdef HAVE_MRUBY + std::shared_ptr<mruby::MRubyContext> mruby_ctx; +#endif // HAVE_MRUBY + // Configuration for session affinity + AffinityConfig affinity; + // Session affinity + // true if this group requires that client connection must be TLS, + // and the request must be redirected to https URI. + bool redirect_if_not_tls; + // true if a request should not be forwarded to a backend. + bool dnf; + // Timeouts for backend connection. + struct { + ev_tstamp read; + ev_tstamp write; + } timeout; +}; + +struct DownstreamAddrGroup { + DownstreamAddrGroup(); + ~DownstreamAddrGroup(); + + DownstreamAddrGroup(const DownstreamAddrGroup &) = delete; + DownstreamAddrGroup(DownstreamAddrGroup &&) = delete; + DownstreamAddrGroup &operator=(const DownstreamAddrGroup &) = delete; + DownstreamAddrGroup &operator=(DownstreamAddrGroup &&) = delete; + + ImmutableString pattern; + std::shared_ptr<SharedDownstreamAddr> shared_addr; + // true if this group is no longer used for new request. If this is + // true, the connection made using one of address in shared_addr + // must not be pooled. + bool retired; +}; + +struct WorkerStat { + size_t num_connections; + size_t num_close_waits; +}; + +#ifdef ENABLE_HTTP3 +struct QUICPacket { + QUICPacket(size_t upstream_addr_index, const Address &remote_addr, + const Address &local_addr, const ngtcp2_pkt_info &pi, + const uint8_t *data, size_t datalen) + : upstream_addr_index{upstream_addr_index}, + remote_addr{remote_addr}, + local_addr{local_addr}, + pi{pi}, + data{data, data + datalen} {} + QUICPacket() : upstream_addr_index{}, remote_addr{}, local_addr{}, pi{} {} + size_t upstream_addr_index; + Address remote_addr; + Address local_addr; + ngtcp2_pkt_info pi; + std::vector<uint8_t> data; +}; +#endif // ENABLE_HTTP3 + +enum class WorkerEventType { + NEW_CONNECTION = 0x01, + REOPEN_LOG = 0x02, + GRACEFUL_SHUTDOWN = 0x03, + REPLACE_DOWNSTREAM = 0x04, +#ifdef ENABLE_HTTP3 + QUIC_PKT_FORWARD = 0x05, +#endif // ENABLE_HTTP3 +}; + +struct WorkerEvent { + WorkerEventType type; + struct { + sockaddr_union client_addr; + size_t client_addrlen; + int client_fd; + const UpstreamAddr *faddr; + }; + std::shared_ptr<TicketKeys> ticket_keys; + std::shared_ptr<DownstreamConfig> downstreamconf; +#ifdef ENABLE_HTTP3 + std::unique_ptr<QUICPacket> quic_pkt; +#endif // ENABLE_HTTP3 +}; + +class Worker { +public: + Worker(struct ev_loop *loop, SSL_CTX *sv_ssl_ctx, SSL_CTX *cl_ssl_ctx, + SSL_CTX *tls_session_cache_memcached_ssl_ctx, + tls::CertLookupTree *cert_tree, +#ifdef ENABLE_HTTP3 + SSL_CTX *quic_sv_ssl_ctx, tls::CertLookupTree *quic_cert_tree, + const uint8_t *cid_prefix, size_t cid_prefixlen, +# ifdef HAVE_LIBBPF + size_t index, +# endif // HAVE_LIBBPF +#endif // ENABLE_HTTP3 + const std::shared_ptr<TicketKeys> &ticket_keys, + ConnectionHandler *conn_handler, + std::shared_ptr<DownstreamConfig> downstreamconf); + ~Worker(); + void run_async(); + void wait(); + void process_events(); + void send(WorkerEvent event); + + tls::CertLookupTree *get_cert_lookup_tree() const; +#ifdef ENABLE_HTTP3 + tls::CertLookupTree *get_quic_cert_lookup_tree() const; +#endif // ENABLE_HTTP3 + + // These 2 functions make a lock m_ to get/set ticket keys + // atomically. + std::shared_ptr<TicketKeys> get_ticket_keys(); + void set_ticket_keys(std::shared_ptr<TicketKeys> ticket_keys); + + WorkerStat *get_worker_stat(); + struct ev_loop *get_loop() const; + SSL_CTX *get_sv_ssl_ctx() const; + SSL_CTX *get_cl_ssl_ctx() const; +#ifdef ENABLE_HTTP3 + SSL_CTX *get_quic_sv_ssl_ctx() const; +#endif // ENABLE_HTTP3 + + void set_graceful_shutdown(bool f); + bool get_graceful_shutdown() const; + + MemchunkPool *get_mcpool(); + void schedule_clear_mcpool(); + + MemcachedDispatcher *get_session_cache_memcached_dispatcher(); + + std::mt19937 &get_randgen(); + +#ifdef HAVE_MRUBY + int create_mruby_context(); + + mruby::MRubyContext *get_mruby_context() const; +#endif // HAVE_MRUBY + + std::vector<std::shared_ptr<DownstreamAddrGroup>> & + get_downstream_addr_groups(); + + ConnectBlocker *get_connect_blocker() const; + + const DownstreamConfig *get_downstream_config() const; + + void + replace_downstream_config(std::shared_ptr<DownstreamConfig> downstreamconf); + + ConnectionHandler *get_connection_handler() const; + +#ifdef ENABLE_HTTP3 + QUICConnectionHandler *get_quic_connection_handler(); + + int setup_quic_server_socket(); + + const uint8_t *get_cid_prefix() const; + +# ifdef HAVE_LIBBPF + bool should_attach_bpf() const; + + bool should_update_bpf_map() const; + + uint32_t compute_sk_index() const; +# endif // HAVE_LIBBPF + + int create_quic_server_socket(UpstreamAddr &addr); + + // Returns a pointer to UpstreamAddr which matches |local_addr|. + const UpstreamAddr *find_quic_upstream_addr(const Address &local_addr); +#endif // ENABLE_HTTP3 + + DNSTracker *get_dns_tracker(); + +private: +#ifndef NOTHREADS + std::future<void> fut_; +#endif // NOTHREADS +#if defined(ENABLE_HTTP3) && defined(HAVE_LIBBPF) + // Unique index of this worker. + size_t index_; +#endif // ENABLE_HTTP3 && HAVE_LIBBPF + std::mutex m_; + std::deque<WorkerEvent> q_; + std::mt19937 randgen_; + ev_async w_; + ev_timer mcpool_clear_timer_; + ev_timer proc_wev_timer_; + MemchunkPool mcpool_; + WorkerStat worker_stat_; + DNSTracker dns_tracker_; + +#ifdef ENABLE_HTTP3 + std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN> cid_prefix_; + std::vector<UpstreamAddr> quic_upstream_addrs_; + std::vector<std::unique_ptr<QUICListener>> quic_listeners_; +#endif // ENABLE_HTTP3 + + std::shared_ptr<DownstreamConfig> downstreamconf_; + std::unique_ptr<MemcachedDispatcher> session_cache_memcached_dispatcher_; +#ifdef HAVE_MRUBY + std::unique_ptr<mruby::MRubyContext> mruby_ctx_; +#endif // HAVE_MRUBY + struct ev_loop *loop_; + + // Following fields are shared across threads if + // get_config()->tls_ctx_per_worker == true. + SSL_CTX *sv_ssl_ctx_; + SSL_CTX *cl_ssl_ctx_; + tls::CertLookupTree *cert_tree_; + ConnectionHandler *conn_handler_; +#ifdef ENABLE_HTTP3 + SSL_CTX *quic_sv_ssl_ctx_; + tls::CertLookupTree *quic_cert_tree_; + + QUICConnectionHandler quic_conn_handler_; +#endif // ENABLE_HTTP3 + +#ifndef HAVE_ATOMIC_STD_SHARED_PTR + std::mutex ticket_keys_m_; +#endif // !HAVE_ATOMIC_STD_SHARED_PTR + std::shared_ptr<TicketKeys> ticket_keys_; + std::vector<std::shared_ptr<DownstreamAddrGroup>> downstream_addr_groups_; + // Worker level blocker for downstream connection. For example, + // this is used when file descriptor is exhausted. + std::unique_ptr<ConnectBlocker> connect_blocker_; + + bool graceful_shutdown_; +}; + +// Selects group based on request's |hostport| and |path|. |hostport| +// is the value taken from :authority or host header field, and may +// contain port. The |path| may contain query part. We require the +// catch-all pattern in place, so this function always selects one +// group. The catch-all group index is given in |catch_all|. All +// patterns are given in |groups|. +size_t match_downstream_addr_group( + const RouterConfig &routerconfig, const StringRef &hostport, + const StringRef &path, + const std::vector<std::shared_ptr<DownstreamAddrGroup>> &groups, + size_t catch_all, BlockAllocator &balloc); + +// Calls this function if connecting to backend failed. |raddr| is +// the actual address used to connect to backend, and it could be +// nullptr. This function may schedule live check. +void downstream_failure(DownstreamAddr *addr, const Address *raddr); + +#ifdef ENABLE_HTTP3 +// Creates unpredictable SHRPX_QUIC_CID_PREFIXLEN bytes sequence which +// is used as a prefix of QUIC Connection ID. This function returns +// -1 on failure. |server_id| must be 2 bytes long. +int create_cid_prefix(uint8_t *cid_prefix, const uint8_t *server_id); +#endif // ENABLE_HTTP3 + +} // namespace shrpx + +#endif // SHRPX_WORKER_H diff --git a/src/shrpx_worker_process.cc b/src/shrpx_worker_process.cc new file mode 100644 index 0000000..05eac02 --- /dev/null +++ b/src/shrpx_worker_process.cc @@ -0,0 +1,701 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_worker_process.h" + +#include <sys/types.h> +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <sys/resource.h> +#include <sys/wait.h> +#include <grp.h> + +#include <cinttypes> +#include <cstdlib> + +#include <openssl/rand.h> + +#include <ev.h> + +#include <ares.h> + +#include "shrpx_config.h" +#include "shrpx_connection_handler.h" +#include "shrpx_log_config.h" +#include "shrpx_worker.h" +#include "shrpx_accept_handler.h" +#include "shrpx_http2_upstream.h" +#include "shrpx_http2_session.h" +#include "shrpx_memcached_dispatcher.h" +#include "shrpx_memcached_request.h" +#include "shrpx_process.h" +#include "shrpx_tls.h" +#include "shrpx_log.h" +#include "util.h" +#include "app_helper.h" +#include "template.h" +#include "xsi_strerror.h" + +using namespace nghttp2; + +namespace shrpx { + +namespace { +void drop_privileges( +#ifdef HAVE_NEVERBLEED + neverbleed_t *nb +#endif // HAVE_NEVERBLEED +) { + std::array<char, STRERROR_BUFSIZE> errbuf; + auto config = get_config(); + + if (getuid() == 0 && config->uid != 0) { +#ifdef HAVE_NEVERBLEED + if (nb) { + neverbleed_setuidgid(nb, config->user.c_str(), 1); + } +#endif // HAVE_NEVERBLEED + + if (initgroups(config->user.c_str(), config->gid) != 0) { + auto error = errno; + LOG(FATAL) << "Could not change supplementary groups: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + exit(EXIT_FAILURE); + } + if (setgid(config->gid) != 0) { + auto error = errno; + LOG(FATAL) << "Could not change gid: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + exit(EXIT_FAILURE); + } + if (setuid(config->uid) != 0) { + auto error = errno; + LOG(FATAL) << "Could not change uid: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + exit(EXIT_FAILURE); + } + if (setuid(0) != -1) { + LOG(FATAL) << "Still have root privileges?"; + exit(EXIT_FAILURE); + } + } +} +} // namespace + +namespace { +void graceful_shutdown(ConnectionHandler *conn_handler) { + if (conn_handler->get_graceful_shutdown()) { + return; + } + + LOG(NOTICE) << "Graceful shutdown signal received"; + + conn_handler->set_graceful_shutdown(true); + + // TODO What happens for the connections not established in the + // kernel? + conn_handler->accept_pending_connection(); + conn_handler->delete_acceptor(); + + conn_handler->graceful_shutdown_worker(); + + auto single_worker = conn_handler->get_single_worker(); + if (single_worker) { + auto worker_stat = single_worker->get_worker_stat(); + if (worker_stat->num_connections == 0 && + worker_stat->num_close_waits == 0) { + ev_break(conn_handler->get_loop()); + } + + return; + } +} +} // namespace + +namespace { +void reopen_log(ConnectionHandler *conn_handler) { + LOG(NOTICE) << "Reopening log files: worker process (thread main)"; + + auto config = get_config(); + auto &loggingconf = config->logging; + + (void)reopen_log_files(loggingconf); + redirect_stderr_to_errorlog(loggingconf); + + conn_handler->worker_reopen_log_files(); +} +} // namespace + +namespace { +void ipc_readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn_handler = static_cast<ConnectionHandler *>(w->data); + std::array<uint8_t, 1024> buf; + ssize_t nread; + while ((nread = read(w->fd, buf.data(), buf.size())) == -1 && errno == EINTR) + ; + if (nread == -1) { + auto error = errno; + LOG(ERROR) << "Failed to read data from ipc channel: errno=" << error; + return; + } + + if (nread == 0) { + // IPC socket closed. Perform immediate shutdown. + LOG(FATAL) << "IPC socket is closed. Perform immediate shutdown."; + nghttp2_Exit(EXIT_FAILURE); + } + + for (ssize_t i = 0; i < nread; ++i) { + switch (buf[i]) { + case SHRPX_IPC_GRACEFUL_SHUTDOWN: + graceful_shutdown(conn_handler); + break; + case SHRPX_IPC_REOPEN_LOG: + reopen_log(conn_handler); + break; + } + } +} +} // namespace + +#ifdef ENABLE_HTTP3 +namespace { +void quic_ipc_readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto conn_handler = static_cast<ConnectionHandler *>(w->data); + + if (conn_handler->quic_ipc_read() != 0) { + LOG(ERROR) << "Failed to read data from QUIC IPC channel"; + + return; + } +} +} // namespace +#endif // ENABLE_HTTP3 + +namespace { +int generate_ticket_key(TicketKey &ticket_key) { + ticket_key.cipher = get_config()->tls.ticket.cipher; + ticket_key.hmac = EVP_sha256(); + ticket_key.hmac_keylen = EVP_MD_size(ticket_key.hmac); + + assert(static_cast<size_t>(EVP_CIPHER_key_length(ticket_key.cipher)) <= + ticket_key.data.enc_key.size()); + assert(ticket_key.hmac_keylen <= ticket_key.data.hmac_key.size()); + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "enc_keylen=" << EVP_CIPHER_key_length(ticket_key.cipher) + << ", hmac_keylen=" << ticket_key.hmac_keylen; + } + + if (RAND_bytes(reinterpret_cast<unsigned char *>(&ticket_key.data), + sizeof(ticket_key.data)) == 0) { + return -1; + } + + return 0; +} +} // namespace + +namespace { +void renew_ticket_key_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto conn_handler = static_cast<ConnectionHandler *>(w->data); + const auto &old_ticket_keys = conn_handler->get_ticket_keys(); + + auto ticket_keys = std::make_shared<TicketKeys>(); + LOG(NOTICE) << "Renew new ticket keys"; + + // If old_ticket_keys is not empty, it should contain at least 2 + // keys: one for encryption, and last one for the next encryption + // key but decryption only. The keys in between are old keys and + // decryption only. The next key is provided to ensure to mitigate + // possible problem when one worker encrypt new key, but one worker, + // which did not take the that key yet, and cannot decrypt it. + // + // We keep keys for get_config()->tls_session_timeout seconds. The + // default is 12 hours. Thus the maximum ticket vector size is 12. + if (old_ticket_keys) { + auto &old_keys = old_ticket_keys->keys; + auto &new_keys = ticket_keys->keys; + + assert(!old_keys.empty()); + + auto max_tickets = + static_cast<size_t>(std::chrono::duration_cast<std::chrono::hours>( + get_config()->tls.session_timeout) + .count()); + + new_keys.resize(std::min(max_tickets, old_keys.size() + 1)); + std::copy_n(std::begin(old_keys), new_keys.size() - 1, + std::begin(new_keys) + 1); + } else { + ticket_keys->keys.resize(1); + } + + auto &new_key = ticket_keys->keys[0]; + + if (generate_ticket_key(new_key) != 0) { + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "failed to generate ticket key"; + } + conn_handler->set_ticket_keys(nullptr); + conn_handler->set_ticket_keys_to_worker(nullptr); + return; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "ticket keys generation done"; + assert(ticket_keys->keys.size() >= 1); + LOG(INFO) << 0 << " enc+dec: " + << util::format_hex(ticket_keys->keys[0].data.name); + for (size_t i = 1; i < ticket_keys->keys.size(); ++i) { + auto &key = ticket_keys->keys[i]; + LOG(INFO) << i << " dec: " << util::format_hex(key.data.name); + } + } + + conn_handler->set_ticket_keys(ticket_keys); + conn_handler->set_ticket_keys_to_worker(ticket_keys); +} +} // namespace + +namespace { +void memcached_get_ticket_key_cb(struct ev_loop *loop, ev_timer *w, + int revents) { + auto conn_handler = static_cast<ConnectionHandler *>(w->data); + auto dispatcher = conn_handler->get_tls_ticket_key_memcached_dispatcher(); + + auto req = std::make_unique<MemcachedRequest>(); + req->key = "nghttpx:tls-ticket-key"; + req->op = MemcachedOp::GET; + req->cb = [conn_handler, w](MemcachedRequest *req, MemcachedResult res) { + switch (res.status_code) { + case MemcachedStatusCode::NO_ERROR: + break; + case MemcachedStatusCode::EXT_NETWORK_ERROR: + conn_handler->on_tls_ticket_key_network_error(w); + return; + default: + conn_handler->on_tls_ticket_key_not_found(w); + return; + } + + // |version (4bytes)|len (2bytes)|key (variable length)|... + // (len, key) pairs are repeated as necessary. + + auto &value = res.value; + if (value.size() < 4) { + LOG(WARN) << "Memcached: tls ticket key value is too small: got " + << value.size(); + conn_handler->on_tls_ticket_key_not_found(w); + return; + } + auto p = value.data(); + auto version = util::get_uint32(p); + // Currently supported version is 1. + if (version != 1) { + LOG(WARN) << "Memcached: tls ticket key version: want 1, got " << version; + conn_handler->on_tls_ticket_key_not_found(w); + return; + } + + auto end = p + value.size(); + p += 4; + + auto &ticketconf = get_config()->tls.ticket; + + size_t expectedlen; + size_t enc_keylen; + size_t hmac_keylen; + if (ticketconf.cipher == EVP_aes_128_cbc()) { + expectedlen = 48; + enc_keylen = 16; + hmac_keylen = 16; + } else if (ticketconf.cipher == EVP_aes_256_cbc()) { + expectedlen = 80; + enc_keylen = 32; + hmac_keylen = 32; + } else { + return; + } + + auto ticket_keys = std::make_shared<TicketKeys>(); + + for (; p != end;) { + if (end - p < 2) { + LOG(WARN) << "Memcached: tls ticket key data is too small"; + conn_handler->on_tls_ticket_key_not_found(w); + return; + } + auto len = util::get_uint16(p); + p += 2; + if (len != expectedlen) { + LOG(WARN) << "Memcached: wrong tls ticket key size: want " + << expectedlen << ", got " << len; + conn_handler->on_tls_ticket_key_not_found(w); + return; + } + if (p + len > end) { + LOG(WARN) << "Memcached: too short tls ticket key payload: want " << len + << ", got " << (end - p); + conn_handler->on_tls_ticket_key_not_found(w); + return; + } + auto key = TicketKey(); + key.cipher = ticketconf.cipher; + key.hmac = EVP_sha256(); + key.hmac_keylen = hmac_keylen; + + std::copy_n(p, key.data.name.size(), std::begin(key.data.name)); + p += key.data.name.size(); + + std::copy_n(p, enc_keylen, std::begin(key.data.enc_key)); + p += enc_keylen; + + std::copy_n(p, hmac_keylen, std::begin(key.data.hmac_key)); + p += hmac_keylen; + + ticket_keys->keys.push_back(std::move(key)); + } + + conn_handler->on_tls_ticket_key_get_success(ticket_keys, w); + }; + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Memcached: tls ticket key get request sent"; + } + + dispatcher->add_request(std::move(req)); +} + +} // namespace + +#ifdef HAVE_NEVERBLEED +namespace { +void nb_child_cb(struct ev_loop *loop, ev_child *w, int revents) { + log_chld(w->rpid, w->rstatus, "neverbleed process"); + + ev_child_stop(loop, w); + + LOG(FATAL) << "neverbleed process exited; aborting now"; + + nghttp2_Exit(EXIT_FAILURE); +} +} // namespace +#endif // HAVE_NEVERBLEED + +namespace { +int send_ready_event(int ready_ipc_fd) { + std::array<char, STRERROR_BUFSIZE> errbuf; + auto pid = getpid(); + ssize_t nwrite; + + while ((nwrite = write(ready_ipc_fd, &pid, sizeof(pid))) == -1 && + errno == EINTR) + ; + + if (nwrite < 0) { + auto error = errno; + + LOG(ERROR) << "Writing PID to ready IPC channel failed: " + << xsi_strerror(error, errbuf.data(), errbuf.size()); + + return -1; + } + + return 0; +} +} // namespace + +int worker_process_event_loop(WorkerProcessConfig *wpconf) { + int rv; + std::array<char, STRERROR_BUFSIZE> errbuf; + (void)errbuf; + + auto config = get_config(); + + if (reopen_log_files(config->logging) != 0) { + LOG(FATAL) << "Failed to open log file"; + return -1; + } + + rv = ares_library_init(ARES_LIB_INIT_ALL); + if (rv != 0) { + LOG(FATAL) << "ares_library_init failed: " << ares_strerror(rv); + return -1; + } + + auto loop = EV_DEFAULT; + + auto gen = util::make_mt19937(); + +#ifdef HAVE_NEVERBLEED + std::array<char, NEVERBLEED_ERRBUF_SIZE> nb_errbuf; + auto nb = std::make_unique<neverbleed_t>(); + if (neverbleed_init(nb.get(), nb_errbuf.data()) != 0) { + LOG(FATAL) << "neverbleed_init failed: " << nb_errbuf.data(); + return -1; + } + + LOG(NOTICE) << "neverbleed process [" << nb->daemon_pid << "] spawned"; + + ev_child nb_childev; + + ev_child_init(&nb_childev, nb_child_cb, nb->daemon_pid, 0); + nb_childev.data = nullptr; + ev_child_start(loop, &nb_childev); +#endif // HAVE_NEVERBLEED + + auto conn_handler = std::make_unique<ConnectionHandler>(loop, gen); + +#ifdef HAVE_NEVERBLEED + conn_handler->set_neverbleed(nb.get()); +#endif // HAVE_NEVERBLEED + +#ifdef ENABLE_HTTP3 + conn_handler->set_quic_ipc_fd(wpconf->quic_ipc_fd); + conn_handler->set_quic_lingering_worker_processes( + wpconf->quic_lingering_worker_processes); +#endif // ENABLE_HTTP3 + + for (auto &addr : config->conn.listener.addrs) { + conn_handler->add_acceptor( + std::make_unique<AcceptHandler>(&addr, conn_handler.get())); + } + + MemchunkPool mcpool; + + ev_timer renew_ticket_key_timer; + if (tls::upstream_tls_enabled(config->conn)) { + auto &ticketconf = config->tls.ticket; + auto &memcachedconf = ticketconf.memcached; + + if (!memcachedconf.host.empty()) { + SSL_CTX *ssl_ctx = nullptr; + + if (memcachedconf.tls) { + ssl_ctx = conn_handler->create_tls_ticket_key_memcached_ssl_ctx(); + } + + conn_handler->set_tls_ticket_key_memcached_dispatcher( + std::make_unique<MemcachedDispatcher>( + &ticketconf.memcached.addr, loop, ssl_ctx, + StringRef{memcachedconf.host}, &mcpool, gen)); + + ev_timer_init(&renew_ticket_key_timer, memcached_get_ticket_key_cb, 0., + 0.); + renew_ticket_key_timer.data = conn_handler.get(); + // Get first ticket keys. + memcached_get_ticket_key_cb(loop, &renew_ticket_key_timer, 0); + } else { + bool auto_tls_ticket_key = true; + if (!ticketconf.files.empty()) { + if (!ticketconf.cipher_given) { + LOG(WARN) + << "It is strongly recommended to specify " + "--tls-ticket-key-cipher=aes-128-cbc (or " + "tls-ticket-key-cipher=aes-128-cbc in configuration file) " + "when --tls-ticket-key-file is used for the smooth " + "transition when the default value of --tls-ticket-key-cipher " + "becomes aes-256-cbc"; + } + auto ticket_keys = read_tls_ticket_key_file( + ticketconf.files, ticketconf.cipher, EVP_sha256()); + if (!ticket_keys) { + LOG(WARN) << "Use internal session ticket key generator"; + } else { + conn_handler->set_ticket_keys(std::move(ticket_keys)); + auto_tls_ticket_key = false; + } + } + if (auto_tls_ticket_key) { + // Generate new ticket key every 1hr. + ev_timer_init(&renew_ticket_key_timer, renew_ticket_key_cb, 0., 1_h); + renew_ticket_key_timer.data = conn_handler.get(); + ev_timer_again(loop, &renew_ticket_key_timer); + + // Generate first session ticket key before running workers. + renew_ticket_key_cb(loop, &renew_ticket_key_timer, 0); + } + } + } + +#ifdef ENABLE_HTTP3 + auto &quicconf = config->quic; + + std::shared_ptr<QUICKeyingMaterials> qkms; + + if (!quicconf.upstream.secret_file.empty()) { + qkms = read_quic_secret_file(quicconf.upstream.secret_file); + if (!qkms) { + LOG(WARN) << "Use QUIC keying materials generated internally"; + } + } + + if (!qkms) { + qkms = std::make_shared<QUICKeyingMaterials>(); + qkms->keying_materials.resize(1); + + auto &qkm = qkms->keying_materials.front(); + + if (RAND_bytes(qkm.reserved.data(), qkm.reserved.size()) != 1) { + LOG(ERROR) << "Failed to generate QUIC secret reserved data"; + return -1; + } + + if (RAND_bytes(qkm.secret.data(), qkm.secret.size()) != 1) { + LOG(ERROR) << "Failed to generate QUIC secret"; + return -1; + } + + if (RAND_bytes(qkm.salt.data(), qkm.salt.size()) != 1) { + LOG(ERROR) << "Failed to generate QUIC salt"; + return -1; + } + } + + for (auto &qkm : qkms->keying_materials) { + if (generate_quic_connection_id_encryption_key( + qkm.cid_encryption_key.data(), qkm.cid_encryption_key.size(), + qkm.secret.data(), qkm.secret.size(), qkm.salt.data(), + qkm.salt.size()) != 0) { + LOG(ERROR) << "Failed to generate QUIC Connection ID encryption key"; + return -1; + } + } + + conn_handler->set_quic_keying_materials(std::move(qkms)); + + conn_handler->set_cid_prefixes(wpconf->cid_prefixes); + conn_handler->set_quic_lingering_worker_processes( + wpconf->quic_lingering_worker_processes); +#endif // ENABLE_HTTP3 + + if (config->single_thread) { + rv = conn_handler->create_single_worker(); + if (rv != 0) { + return -1; + } + } else { +#ifndef NOTHREADS + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGCHLD); + + rv = pthread_sigmask(SIG_BLOCK, &set, nullptr); + if (rv != 0) { + LOG(ERROR) << "Blocking SIGCHLD failed: " + << xsi_strerror(rv, errbuf.data(), errbuf.size()); + return -1; + } +#endif // !NOTHREADS + + rv = conn_handler->create_worker_thread(config->num_worker); + if (rv != 0) { + return -1; + } + +#ifndef NOTHREADS + rv = pthread_sigmask(SIG_UNBLOCK, &set, nullptr); + if (rv != 0) { + LOG(ERROR) << "Unblocking SIGCHLD failed: " + << xsi_strerror(rv, errbuf.data(), errbuf.size()); + return -1; + } +#endif // !NOTHREADS + } + +#if defined(ENABLE_HTTP3) && defined(HAVE_LIBBPF) + conn_handler->unload_bpf_objects(); +#endif // defined(ENABLE_HTTP3) && defined(HAVE_LIBBPF) + + drop_privileges( +#ifdef HAVE_NEVERBLEED + nb.get() +#endif // HAVE_NEVERBLEED + ); + + ev_io ipcev; + ev_io_init(&ipcev, ipc_readcb, wpconf->ipc_fd, EV_READ); + ipcev.data = conn_handler.get(); + ev_io_start(loop, &ipcev); + +#ifdef ENABLE_HTTP3 + ev_io quic_ipcev; + ev_io_init(&quic_ipcev, quic_ipc_readcb, wpconf->quic_ipc_fd, EV_READ); + quic_ipcev.data = conn_handler.get(); + ev_io_start(loop, &quic_ipcev); +#endif // ENABLE_HTTP3 + + if (tls::upstream_tls_enabled(config->conn) && !config->tls.ocsp.disabled) { + if (config->tls.ocsp.startup) { + conn_handler->set_enable_acceptor_on_ocsp_completion(true); + conn_handler->disable_acceptor(); + } + + conn_handler->proceed_next_cert_ocsp(); + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "Entering event loop"; + } + + if (send_ready_event(wpconf->ready_ipc_fd) != 0) { + return -1; + } + + ev_run(loop, 0); + + conn_handler->cancel_ocsp_update(); + + // Destroy SSL_CTX held in conn_handler before killing neverbleed + // daemon. Otherwise priv_rsa_finish yields "write error" and + // worker process aborts. + conn_handler.reset(); + +#ifdef HAVE_NEVERBLEED + assert(nb->daemon_pid > 0); + + rv = kill(nb->daemon_pid, SIGTERM); + if (rv != 0) { + auto error = errno; + LOG(ERROR) << "Could not send signal to neverbleed daemon: errno=" << error; + } + + while ((rv = waitpid(nb->daemon_pid, nullptr, 0)) == -1 && errno == EINTR) + ; + if (rv == -1) { + auto error = errno; + LOG(ERROR) << "Error occurred while we were waiting for the completion " + "of neverbleed process: errno=" + << error; + } +#endif // HAVE_NEVERBLEED + + ares_library_cleanup(); + + return 0; +} + +} // namespace shrpx diff --git a/src/shrpx_worker_process.h b/src/shrpx_worker_process.h new file mode 100644 index 0000000..f432503 --- /dev/null +++ b/src/shrpx_worker_process.h @@ -0,0 +1,67 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_WORKER_PROCESS_H +#define SHRPX_WORKER_PROCESS_H + +#include "shrpx.h" + +#include <vector> +#include <array> + +#include "shrpx_connection_handler.h" +#ifdef ENABLE_HTTP3 +# include "shrpx_quic.h" +#endif // ENABLE_HTTP3 + +namespace shrpx { + +class ConnectionHandler; + +struct WorkerProcessConfig { + // IPC socket to read event from main process + int ipc_fd; + // IPC socket to tell that a worker process is ready for service. + int ready_ipc_fd; + // IPv4 or UNIX domain socket, or -1 if not used + int server_fd; + // IPv6 socket, or -1 if not used + int server_fd6; +#ifdef ENABLE_HTTP3 + // CID prefixes for the new worker process. + std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>> cid_prefixes; + // IPC socket to read forwarded QUIC UDP datagram from the current + // worker process. + int quic_ipc_fd; + // Lingering worker processes which were created before this worker + // process to forward QUIC UDP datagram during reload. + std::vector<QUICLingeringWorkerProcess> quic_lingering_worker_processes; +#endif // ENABLE_HTTP3 +}; + +int worker_process_event_loop(WorkerProcessConfig *wpconf); + +} // namespace shrpx + +#endif // SHRPX_WORKER_PROCESS_H diff --git a/src/shrpx_worker_test.cc b/src/shrpx_worker_test.cc new file mode 100644 index 0000000..7c5c329 --- /dev/null +++ b/src/shrpx_worker_test.cc @@ -0,0 +1,247 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "shrpx_worker_test.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H + +#include <cstdlib> + +#include <CUnit/CUnit.h> + +#include "shrpx_worker.h" +#include "shrpx_connect_blocker.h" +#include "shrpx_log.h" + +namespace shrpx { + +void test_shrpx_worker_match_downstream_addr_group(void) { + auto groups = std::vector<std::shared_ptr<DownstreamAddrGroup>>(); + for (auto &s : {"nghttp2.org/", "nghttp2.org/alpha/bravo/", + "nghttp2.org/alpha/charlie", "nghttp2.org/delta%3A", + "www.nghttp2.org/", "[::1]/", "nghttp2.org/alpha/bravo/delta", + // Check that match is done in the single node + "example.com/alpha/bravo", "192.168.0.1/alpha/", "/golf/"}) { + auto g = std::make_shared<DownstreamAddrGroup>(); + g->pattern = ImmutableString(s); + groups.push_back(std::move(g)); + } + + BlockAllocator balloc(1024, 1024); + RouterConfig routerconf; + + auto &router = routerconf.router; + auto &wcrouter = routerconf.rev_wildcard_router; + auto &wp = routerconf.wildcard_patterns; + + for (size_t i = 0; i < groups.size(); ++i) { + auto &g = groups[i]; + router.add_route(StringRef{g->pattern}, i); + } + + CU_ASSERT(0 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/"), groups, 255, balloc)); + + // port is removed + CU_ASSERT(0 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org:8080"), + StringRef::from_lit("/"), groups, 255, balloc)); + + // host is case-insensitive + CU_ASSERT(4 == match_downstream_addr_group( + routerconf, StringRef::from_lit("WWW.nghttp2.org"), + StringRef::from_lit("/alpha"), groups, 255, balloc)); + + CU_ASSERT(1 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/bravo/"), groups, 255, + balloc)); + + // /alpha/bravo also matches /alpha/bravo/ + CU_ASSERT(1 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/bravo"), groups, 255, balloc)); + + // path part is case-sensitive + CU_ASSERT(0 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/Alpha/bravo"), groups, 255, balloc)); + + CU_ASSERT(1 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/bravo/charlie"), groups, 255, + balloc)); + + CU_ASSERT(2 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/charlie"), groups, 255, + balloc)); + + // pattern which does not end with '/' must match its entirely. So + // this matches to group 0, not group 2. + CU_ASSERT(0 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/charlie/"), groups, 255, + balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("example.org"), + StringRef::from_lit("/"), groups, 255, balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit(""), + StringRef::from_lit("/"), groups, 255, balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit(""), + StringRef::from_lit("alpha"), groups, 255, balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("foo/bar"), + StringRef::from_lit("/"), groups, 255, balloc)); + + // If path is StringRef::from_lit("*", only match with host + "/"). + CU_ASSERT(0 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("*"), groups, 255, balloc)); + + CU_ASSERT(5 == match_downstream_addr_group( + routerconf, StringRef::from_lit("[::1]"), + StringRef::from_lit("/"), groups, 255, balloc)); + CU_ASSERT(5 == match_downstream_addr_group( + routerconf, StringRef::from_lit("[::1]:8080"), + StringRef::from_lit("/"), groups, 255, balloc)); + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("[::1"), + StringRef::from_lit("/"), groups, 255, balloc)); + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("[::1]8000"), + StringRef::from_lit("/"), groups, 255, balloc)); + + // Check the case where adding route extends tree + CU_ASSERT(6 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/bravo/delta"), groups, 255, + balloc)); + + CU_ASSERT(1 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/alpha/bravo/delta/"), groups, 255, + balloc)); + + // Check the case where query is done in a single node + CU_ASSERT(7 == match_downstream_addr_group( + routerconf, StringRef::from_lit("example.com"), + StringRef::from_lit("/alpha/bravo"), groups, 255, balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("example.com"), + StringRef::from_lit("/alpha/bravo/"), groups, 255, + balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("example.com"), + StringRef::from_lit("/alpha"), groups, 255, balloc)); + + // Check the case where quey is done in a single node + CU_ASSERT(8 == match_downstream_addr_group( + routerconf, StringRef::from_lit("192.168.0.1"), + StringRef::from_lit("/alpha"), groups, 255, balloc)); + + CU_ASSERT(8 == match_downstream_addr_group( + routerconf, StringRef::from_lit("192.168.0.1"), + StringRef::from_lit("/alpha/"), groups, 255, balloc)); + + CU_ASSERT(8 == match_downstream_addr_group( + routerconf, StringRef::from_lit("192.168.0.1"), + StringRef::from_lit("/alpha/bravo"), groups, 255, balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("192.168.0.1"), + StringRef::from_lit("/alph"), groups, 255, balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit("192.168.0.1"), + StringRef::from_lit("/"), groups, 255, balloc)); + + // Test for wildcard hosts + auto g1 = std::make_shared<DownstreamAddrGroup>(); + g1->pattern = ImmutableString::from_lit("git.nghttp2.org"); + groups.push_back(std::move(g1)); + + auto g2 = std::make_shared<DownstreamAddrGroup>(); + g2->pattern = ImmutableString::from_lit(".nghttp2.org"); + groups.push_back(std::move(g2)); + + auto g3 = std::make_shared<DownstreamAddrGroup>(); + g3->pattern = ImmutableString::from_lit(".local"); + groups.push_back(std::move(g3)); + + wp.emplace_back(StringRef::from_lit("git.nghttp2.org")); + wcrouter.add_route(StringRef::from_lit("gro.2ptthgn.tig"), 0); + wp.back().router.add_route(StringRef::from_lit("/echo/"), 10); + + wp.emplace_back(StringRef::from_lit(".nghttp2.org")); + wcrouter.add_route(StringRef::from_lit("gro.2ptthgn."), 1); + wp.back().router.add_route(StringRef::from_lit("/echo/"), 11); + wp.back().router.add_route(StringRef::from_lit("/echo/foxtrot"), 12); + + wp.emplace_back(StringRef::from_lit(".local")); + wcrouter.add_route(StringRef::from_lit("lacol."), 2); + wp.back().router.add_route(StringRef::from_lit("/"), 13); + + CU_ASSERT(11 == match_downstream_addr_group( + routerconf, StringRef::from_lit("git.nghttp2.org"), + StringRef::from_lit("/echo"), groups, 255, balloc)); + + CU_ASSERT(10 == match_downstream_addr_group( + routerconf, StringRef::from_lit("0git.nghttp2.org"), + StringRef::from_lit("/echo"), groups, 255, balloc)); + + CU_ASSERT(11 == match_downstream_addr_group( + routerconf, StringRef::from_lit("it.nghttp2.org"), + StringRef::from_lit("/echo"), groups, 255, balloc)); + + CU_ASSERT(255 == match_downstream_addr_group( + routerconf, StringRef::from_lit(".nghttp2.org"), + StringRef::from_lit("/echo/foxtrot"), groups, 255, + balloc)); + + CU_ASSERT(9 == match_downstream_addr_group( + routerconf, StringRef::from_lit("alpha.nghttp2.org"), + StringRef::from_lit("/golf"), groups, 255, balloc)); + + CU_ASSERT(0 == match_downstream_addr_group( + routerconf, StringRef::from_lit("nghttp2.org"), + StringRef::from_lit("/echo"), groups, 255, balloc)); + + CU_ASSERT(13 == match_downstream_addr_group( + routerconf, StringRef::from_lit("test.local"), + StringRef{}, groups, 255, balloc)); +} + +} // namespace shrpx diff --git a/src/shrpx_worker_test.h b/src/shrpx_worker_test.h new file mode 100644 index 0000000..8ffa2f1 --- /dev/null +++ b/src/shrpx_worker_test.h @@ -0,0 +1,38 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef SHRPX_WORKER_TEST_H +#define SHRPX_WORKER_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_shrpx_worker_match_downstream_addr_group(void); + +} // namespace shrpx + +#endif // SHRPX_WORKER_TEST_H diff --git a/src/ssl_compat.h b/src/ssl_compat.h new file mode 100644 index 0000000..f691104 --- /dev/null +++ b/src/ssl_compat.h @@ -0,0 +1,48 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef OPENSSL_COMPAT_H + +# include <openssl/opensslv.h> + +# ifdef LIBRESSL_VERSION_NUMBER +# define NGHTTP2_OPENSSL_IS_LIBRESSL +# endif // !LIBRESSL_VERSION_NUMBER + +# if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) +# define NGHTTP2_OPENSSL_IS_BORINGSSL +# endif // OPENSSL_IS_BORINGSSL || OPENSSL_IS_AWSLC + +# if !defined(NGHTTP2_OPENSSL_IS_BORINGSSL) && \ + !defined(NGHTTP2_OPENSSL_IS_LIBRESSL) +# define NGHTTP2_GENUINE_OPENSSL +# endif // !NGHTTP2_OPENSSL_IS_BORINGSSL && !NGHTTP2_OPENSSL_IS_LIBRESSL + +# ifdef NGHTTP2_GENUINE_OPENSSL +# define OPENSSL_3_0_0_API (OPENSSL_VERSION_NUMBER >= 0x30000000L) +# else // !NGHTTP2_GENUINE_OPENSSL +# define OPENSSL_3_0_0_API 0 +# endif // !NGHTTP2_GENUINE_OPENSSL + +#endif // OPENSSL_COMPAT_H diff --git a/src/template.h b/src/template.h new file mode 100644 index 0000000..530a1d1 --- /dev/null +++ b/src/template.h @@ -0,0 +1,548 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TEMPLATE_H +#define TEMPLATE_H + +#include "nghttp2_config.h" + +#include <cstring> +#include <cstdio> +#include <cstdlib> +#include <memory> +#include <array> +#include <functional> +#include <typeinfo> +#include <algorithm> +#include <ostream> +#include <utility> + +namespace nghttp2 { + +// std::forward is constexpr since C++14 +template <typename... T> +constexpr std::array< + typename std::decay<typename std::common_type<T...>::type>::type, + sizeof...(T)> +make_array(T &&...t) { + return std::array< + typename std::decay<typename std::common_type<T...>::type>::type, + sizeof...(T)>{{std::forward<T>(t)...}}; +} + +template <typename T, size_t N> constexpr size_t array_size(T (&)[N]) { + return N; +} + +template <typename T, size_t N> constexpr size_t str_size(T (&)[N]) { + return N - 1; +} + +// inspired by <http://blog.korfuri.fr/post/go-defer-in-cpp/>, but our +// template can take functions returning other than void. +template <typename F, typename... T> struct Defer { + Defer(F &&f, T &&...t) + : f(std::bind(std::forward<F>(f), std::forward<T>(t)...)) {} + Defer(Defer &&o) noexcept : f(std::move(o.f)) {} + ~Defer() { f(); } + + using ResultType = typename std::result_of<typename std::decay<F>::type( + typename std::decay<T>::type...)>::type; + std::function<ResultType()> f; +}; + +template <typename F, typename... T> Defer<F, T...> defer(F &&f, T &&...t) { + return Defer<F, T...>(std::forward<F>(f), std::forward<T>(t)...); +} + +template <typename T, typename F> bool test_flags(T t, F flags) { + return (t & flags) == flags; +} + +// doubly linked list of element T*. T must have field T *dlprev and +// T *dlnext, which point to previous element and next element in the +// list respectively. +template <typename T> struct DList { + DList() : head(nullptr), tail(nullptr), len(0) {} + + DList(const DList &) = delete; + DList &operator=(const DList &) = delete; + + DList(DList &&other) noexcept + : head{std::exchange(other.head, nullptr)}, + tail{std::exchange(other.tail, nullptr)}, + len{std::exchange(other.len, 0)} {} + + DList &operator=(DList &&other) noexcept { + if (this == &other) { + return *this; + } + head = std::exchange(other.head, nullptr); + tail = std::exchange(other.tail, nullptr); + len = std::exchange(other.len, 0); + + return *this; + } + + void append(T *t) { + ++len; + if (tail) { + tail->dlnext = t; + t->dlprev = tail; + tail = t; + return; + } + head = tail = t; + } + + void remove(T *t) { + --len; + auto p = t->dlprev; + auto n = t->dlnext; + if (p) { + p->dlnext = n; + } + if (head == t) { + head = n; + } + if (n) { + n->dlprev = p; + } + if (tail == t) { + tail = p; + } + t->dlprev = t->dlnext = nullptr; + } + + bool empty() const { return head == nullptr; } + + size_t size() const { return len; } + + T *head, *tail; + size_t len; +}; + +template <typename T> void dlist_delete_all(DList<T> &dl) { + for (auto e = dl.head; e;) { + auto next = e->dlnext; + delete e; + e = next; + } +} + +// User-defined literals for K, M, and G (powers of 1024) + +constexpr unsigned long long operator"" _k(unsigned long long k) { + return k * 1024; +} + +constexpr unsigned long long operator"" _m(unsigned long long m) { + return m * 1024 * 1024; +} + +constexpr unsigned long long operator"" _g(unsigned long long g) { + return g * 1024 * 1024 * 1024; +} + +// User-defined literals for time, converted into double in seconds + +// hours +constexpr double operator"" _h(unsigned long long h) { return h * 60 * 60; } + +// minutes +constexpr double operator"" _min(unsigned long long min) { return min * 60; } + +// seconds +constexpr double operator"" _s(unsigned long long s) { return s; } + +// milliseconds +constexpr double operator"" _ms(unsigned long long ms) { return ms / 1000.; } + +// Returns a copy of NULL-terminated string [first, last). +template <typename InputIt> +std::unique_ptr<char[]> strcopy(InputIt first, InputIt last) { + auto res = std::make_unique<char[]>(last - first + 1); + *std::copy(first, last, res.get()) = '\0'; + return res; +} + +// Returns a copy of NULL-terminated string |val|. +inline std::unique_ptr<char[]> strcopy(const char *val) { + return strcopy(val, val + strlen(val)); +} + +inline std::unique_ptr<char[]> strcopy(const char *val, size_t n) { + return strcopy(val, val + n); +} + +// Returns a copy of val.c_str(). +inline std::unique_ptr<char[]> strcopy(const std::string &val) { + return strcopy(std::begin(val), std::end(val)); +} + +inline std::unique_ptr<char[]> strcopy(const std::unique_ptr<char[]> &val) { + if (!val) { + return nullptr; + } + return strcopy(val.get()); +} + +inline std::unique_ptr<char[]> strcopy(const std::unique_ptr<char[]> &val, + size_t n) { + if (!val) { + return nullptr; + } + return strcopy(val.get(), val.get() + n); +} + +// ImmutableString represents string that is immutable unlike +// std::string. It has c_str() and size() functions to mimic +// std::string. It manages buffer by itself. Just like std::string, +// c_str() returns NULL-terminated string, but NULL character may +// appear before the final terminal NULL. +class ImmutableString { +public: + using traits_type = std::char_traits<char>; + using value_type = traits_type::char_type; + using allocator_type = std::allocator<char>; + using size_type = std::allocator_traits<allocator_type>::size_type; + using difference_type = + std::allocator_traits<allocator_type>::difference_type; + using const_reference = const value_type &; + using const_pointer = const value_type *; + using const_iterator = const_pointer; + using const_reverse_iterator = std::reverse_iterator<const_iterator>; + + ImmutableString() : len(0), base("") {} + ImmutableString(const char *s, size_t slen) + : len(slen), base(copystr(s, s + len)) {} + explicit ImmutableString(const char *s) + : len(strlen(s)), base(copystr(s, s + len)) {} + explicit ImmutableString(const std::string &s) + : len(s.size()), base(copystr(std::begin(s), std::end(s))) {} + template <typename InputIt> + ImmutableString(InputIt first, InputIt last) + : len(std::distance(first, last)), base(copystr(first, last)) {} + ImmutableString(const ImmutableString &other) + : len(other.len), base(copystr(std::begin(other), std::end(other))) {} + ImmutableString(ImmutableString &&other) noexcept + : len{std::exchange(other.len, 0)}, base{std::exchange(other.base, "")} {} + ~ImmutableString() { + if (len) { + delete[] base; + } + } + + ImmutableString &operator=(const ImmutableString &other) { + if (this == &other) { + return *this; + } + if (len) { + delete[] base; + } + len = other.len; + base = copystr(std::begin(other), std::end(other)); + return *this; + } + ImmutableString &operator=(ImmutableString &&other) noexcept { + if (this == &other) { + return *this; + } + if (len) { + delete[] base; + } + len = std::exchange(other.len, 0); + base = std::exchange(other.base, ""); + return *this; + } + + template <size_t N> static ImmutableString from_lit(const char (&s)[N]) { + return ImmutableString(s, N - 1); + } + + const_iterator begin() const { return base; }; + const_iterator cbegin() const { return base; }; + + const_iterator end() const { return base + len; }; + const_iterator cend() const { return base + len; }; + + const_reverse_iterator rbegin() const { + return const_reverse_iterator{base + len}; + } + const_reverse_iterator crbegin() const { + return const_reverse_iterator{base + len}; + } + + const_reverse_iterator rend() const { return const_reverse_iterator{base}; } + const_reverse_iterator crend() const { return const_reverse_iterator{base}; } + + const char *c_str() const { return base; } + size_type size() const { return len; } + bool empty() const { return len == 0; } + const_reference operator[](size_type pos) const { return *(base + pos); } + +private: + template <typename InputIt> const char *copystr(InputIt first, InputIt last) { + if (first == last) { + return ""; + } + auto res = new char[std::distance(first, last) + 1]; + *std::copy(first, last, res) = '\0'; + return res; + } + + size_type len; + const char *base; +}; + +inline bool operator==(const ImmutableString &lhs, const ImmutableString &rhs) { + return lhs.size() == rhs.size() && + std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs)); +} + +inline bool operator==(const ImmutableString &lhs, const std::string &rhs) { + return lhs.size() == rhs.size() && + std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs)); +} + +inline bool operator==(const std::string &lhs, const ImmutableString &rhs) { + return rhs == lhs; +} + +inline bool operator==(const ImmutableString &lhs, const char *rhs) { + return lhs.size() == strlen(rhs) && + std::equal(std::begin(lhs), std::end(lhs), rhs); +} + +inline bool operator==(const char *lhs, const ImmutableString &rhs) { + return rhs == lhs; +} + +inline bool operator!=(const ImmutableString &lhs, const ImmutableString &rhs) { + return !(lhs == rhs); +} + +inline bool operator!=(const ImmutableString &lhs, const std::string &rhs) { + return !(lhs == rhs); +} + +inline bool operator!=(const std::string &lhs, const ImmutableString &rhs) { + return !(rhs == lhs); +} + +inline bool operator!=(const ImmutableString &lhs, const char *rhs) { + return !(lhs == rhs); +} + +inline bool operator!=(const char *lhs, const ImmutableString &rhs) { + return !(rhs == lhs); +} + +inline std::ostream &operator<<(std::ostream &o, const ImmutableString &s) { + return o.write(s.c_str(), s.size()); +} + +inline std::string &operator+=(std::string &lhs, const ImmutableString &rhs) { + lhs.append(rhs.c_str(), rhs.size()); + return lhs; +} + +// StringRef is a reference to a string owned by something else. So +// it behaves like simple string, but it does not own pointer. When +// it is default constructed, it has empty string. You can freely +// copy or move around this struct, but never free its pointer. str() +// function can be used to export the content as std::string. +class StringRef { +public: + using traits_type = std::char_traits<char>; + using value_type = traits_type::char_type; + using allocator_type = std::allocator<char>; + using size_type = std::allocator_traits<allocator_type>::size_type; + using difference_type = + std::allocator_traits<allocator_type>::difference_type; + using const_reference = const value_type &; + using const_pointer = const value_type *; + using const_iterator = const_pointer; + using const_reverse_iterator = std::reverse_iterator<const_iterator>; + + constexpr StringRef() : base(""), len(0) {} + explicit StringRef(const std::string &s) : base(s.c_str()), len(s.size()) {} + explicit StringRef(const ImmutableString &s) + : base(s.c_str()), len(s.size()) {} + explicit StringRef(const char *s) : base(s), len(strlen(s)) {} + constexpr StringRef(const char *s, size_t n) : base(s), len(n) {} + template <typename CharT> + constexpr StringRef(const CharT *s, size_t n) + : base(reinterpret_cast<const char *>(s)), len(n) {} + template <typename InputIt> + StringRef(InputIt first, InputIt last) + : base(reinterpret_cast<const char *>(&*first)), + len(std::distance(first, last)) {} + template <typename InputIt> + StringRef(InputIt *first, InputIt *last) + : base(reinterpret_cast<const char *>(first)), + len(std::distance(first, last)) {} + template <typename CharT, size_t N> + constexpr static StringRef from_lit(const CharT (&s)[N]) { + return StringRef{s, N - 1}; + } + static StringRef from_maybe_nullptr(const char *s) { + if (s == nullptr) { + return StringRef(); + } + + return StringRef(s); + } + + constexpr const_iterator begin() const { return base; }; + constexpr const_iterator cbegin() const { return base; }; + + constexpr const_iterator end() const { return base + len; }; + constexpr const_iterator cend() const { return base + len; }; + + const_reverse_iterator rbegin() const { + return const_reverse_iterator{base + len}; + } + const_reverse_iterator crbegin() const { + return const_reverse_iterator{base + len}; + } + + const_reverse_iterator rend() const { return const_reverse_iterator{base}; } + const_reverse_iterator crend() const { return const_reverse_iterator{base}; } + + constexpr const char *c_str() const { return base; } + constexpr size_type size() const { return len; } + constexpr bool empty() const { return len == 0; } + constexpr const_reference operator[](size_type pos) const { + return *(base + pos); + } + + std::string str() const { return std::string(base, len); } + const uint8_t *byte() const { + return reinterpret_cast<const uint8_t *>(base); + } + +private: + const char *base; + size_type len; +}; + +inline bool operator==(const StringRef &lhs, const StringRef &rhs) { + return lhs.size() == rhs.size() && + std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs)); +} + +inline bool operator==(const StringRef &lhs, const std::string &rhs) { + return lhs.size() == rhs.size() && + std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs)); +} + +inline bool operator==(const std::string &lhs, const StringRef &rhs) { + return rhs == lhs; +} + +inline bool operator==(const StringRef &lhs, const char *rhs) { + return lhs.size() == strlen(rhs) && + std::equal(std::begin(lhs), std::end(lhs), rhs); +} + +inline bool operator==(const StringRef &lhs, const ImmutableString &rhs) { + return lhs.size() == rhs.size() && + std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs)); +} + +inline bool operator==(const ImmutableString &lhs, const StringRef &rhs) { + return rhs == lhs; +} + +inline bool operator==(const char *lhs, const StringRef &rhs) { + return rhs == lhs; +} + +inline bool operator!=(const StringRef &lhs, const StringRef &rhs) { + return !(lhs == rhs); +} + +inline bool operator!=(const StringRef &lhs, const std::string &rhs) { + return !(lhs == rhs); +} + +inline bool operator!=(const std::string &lhs, const StringRef &rhs) { + return !(rhs == lhs); +} + +inline bool operator!=(const StringRef &lhs, const char *rhs) { + return !(lhs == rhs); +} + +inline bool operator!=(const char *lhs, const StringRef &rhs) { + return !(rhs == lhs); +} + +inline bool operator<(const StringRef &lhs, const StringRef &rhs) { + return std::lexicographical_compare(std::begin(lhs), std::end(lhs), + std::begin(rhs), std::end(rhs)); +} + +inline std::ostream &operator<<(std::ostream &o, const StringRef &s) { + return o.write(s.c_str(), s.size()); +} + +inline std::string &operator+=(std::string &lhs, const StringRef &rhs) { + lhs.append(rhs.c_str(), rhs.size()); + return lhs; +} + +inline int run_app(std::function<int(int, char **)> app, int argc, + char **argv) { + try { + return app(argc, argv); + } catch (const std::bad_alloc &) { + fputs("Out of memory\n", stderr); + } catch (const std::exception &x) { + fprintf(stderr, "Caught %s:\n%s\n", typeid(x).name(), x.what()); + } catch (...) { + fputs("Unknown exception caught\n", stderr); + } + return EXIT_FAILURE; +} + +} // namespace nghttp2 + +namespace std { +template <> struct hash<nghttp2::StringRef> { + std::size_t operator()(const nghttp2::StringRef &s) const noexcept { + // 32 bit FNV-1a: + // https://tools.ietf.org/html/draft-eastlake-fnv-16#section-6.1.1 + uint32_t h = 2166136261u; + for (auto c : s) { + h ^= static_cast<uint8_t>(c); + h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); + } + return h; + } +}; +} // namespace std + +#endif // TEMPLATE_H diff --git a/src/template_test.cc b/src/template_test.cc new file mode 100644 index 0000000..4a77315 --- /dev/null +++ b/src/template_test.cc @@ -0,0 +1,204 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "template_test.h" + +#include <cstring> +#include <iostream> +#include <sstream> + +#include <CUnit/CUnit.h> + +#include "template.h" + +namespace nghttp2 { + +void test_template_immutable_string(void) { + ImmutableString null; + + CU_ASSERT("" == null); + CU_ASSERT(0 == null.size()); + CU_ASSERT(null.empty()); + + ImmutableString from_cstr("alpha"); + + CU_ASSERT(0 == strcmp("alpha", from_cstr.c_str())); + CU_ASSERT(5 == from_cstr.size()); + CU_ASSERT(!from_cstr.empty()); + CU_ASSERT("alpha" == from_cstr); + CU_ASSERT(from_cstr == "alpha"); + CU_ASSERT(std::string("alpha") == from_cstr); + CU_ASSERT(from_cstr == std::string("alpha")); + + // copy constructor + ImmutableString src("charlie"); + ImmutableString copy = src; + + CU_ASSERT("charlie" == copy); + CU_ASSERT(7 == copy.size()); + + // copy assignment + ImmutableString copy2; + copy2 = src; + + CU_ASSERT("charlie" == copy2); + CU_ASSERT(7 == copy2.size()); + + // move constructor + ImmutableString move = std::move(copy); + + CU_ASSERT("charlie" == move); + CU_ASSERT(7 == move.size()); + CU_ASSERT("" == copy); + CU_ASSERT(0 == copy.size()); + + // move assignment + move = std::move(from_cstr); + + CU_ASSERT("alpha" == move); + CU_ASSERT(5 == move.size()); + CU_ASSERT("" == from_cstr); + CU_ASSERT(0 == from_cstr.size()); + + // from string literal + auto from_lit = StringRef::from_lit("bravo"); + + CU_ASSERT("bravo" == from_lit); + CU_ASSERT(5 == from_lit.size()); + + // equality + ImmutableString eq("delta"); + + CU_ASSERT("delta1" != eq); + CU_ASSERT("delt" != eq); + CU_ASSERT(eq != "delta1"); + CU_ASSERT(eq != "delt"); + + // operator[] + ImmutableString br_op("foxtrot"); + + CU_ASSERT('f' == br_op[0]); + CU_ASSERT('o' == br_op[1]); + CU_ASSERT('t' == br_op[6]); + CU_ASSERT('\0' == br_op[7]); + + // operator==(const ImmutableString &, const ImmutableString &) + { + ImmutableString a("foo"); + ImmutableString b("foo"); + ImmutableString c("fo"); + + CU_ASSERT(a == b); + CU_ASSERT(a != c); + CU_ASSERT(c != b); + } + + // operator<< + { + ImmutableString a("foo"); + std::stringstream ss; + ss << a; + + CU_ASSERT("foo" == ss.str()); + } + + // operator +=(std::string &, const ImmutableString &) + { + std::string a = "alpha"; + a += ImmutableString("bravo"); + + CU_ASSERT("alphabravo" == a); + } +} + +void test_template_string_ref(void) { + StringRef empty; + + CU_ASSERT("" == empty); + CU_ASSERT(0 == empty.size()); + + // from std::string + std::string alpha = "alpha"; + + StringRef ref(alpha); + + CU_ASSERT("alpha" == ref); + CU_ASSERT(ref == "alpha"); + CU_ASSERT(alpha == ref); + CU_ASSERT(ref == alpha); + CU_ASSERT(5 == ref.size()); + + // from string literal + auto from_lit = StringRef::from_lit("alpha"); + + CU_ASSERT("alpha" == from_lit); + CU_ASSERT(5 == from_lit.size()); + + // from ImmutableString + auto im = ImmutableString::from_lit("bravo"); + + StringRef imref(im); + + CU_ASSERT("bravo" == imref); + CU_ASSERT(5 == imref.size()); + + // from C-string + StringRef cstrref("charlie"); + + CU_ASSERT("charlie" == cstrref); + CU_ASSERT(7 == cstrref.size()); + + // from C-string and its length + StringRef cstrnref("delta", 5); + + CU_ASSERT("delta" == cstrnref); + CU_ASSERT(5 == cstrnref.size()); + + // operator[] + StringRef br_op("foxtrot"); + + CU_ASSERT('f' == br_op[0]); + CU_ASSERT('o' == br_op[1]); + CU_ASSERT('t' == br_op[6]); + CU_ASSERT('\0' == br_op[7]); + + // operator<< + { + StringRef a("foo"); + std::stringstream ss; + ss << a; + + CU_ASSERT("foo" == ss.str()); + } + + // operator +=(std::string &, const StringRef &) + { + std::string a = "alpha"; + a += StringRef("bravo"); + + CU_ASSERT("alphabravo" == a); + } +} + +} // namespace nghttp2 diff --git a/src/template_test.h b/src/template_test.h new file mode 100644 index 0000000..2c1448f --- /dev/null +++ b/src/template_test.h @@ -0,0 +1,39 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TEMPLATE_TEST_H +#define TEMPLATE_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace nghttp2 { + +void test_template_immutable_string(void); +void test_template_string_ref(void); + +} // namespace nghttp2 + +#endif // TEMPLATE_TEST_H diff --git a/src/test.example.com-key.pem b/src/test.example.com-key.pem new file mode 100644 index 0000000..6d5515c --- /dev/null +++ b/src/test.example.com-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEArf2UBsEh/xwd/4WZfVFf5sMyWcns/1idF2FroLDwqVUYRlxp +U/KbrIG8X8v3w4cVP/xOXd1y9Q+W9OLK2YScAeHeE97mHZXbcpowUxvTDv/GNIHH +XK/yvYM8R2EEnZR71qXdoXsCakv/aG2ewkkvA108eEbk0u7RxNmIsYsO0Y4iBtwB +M/MSkAYPfWdrdHhY9z0l4M8GAyUOuZc0t6j0zw3fzkjqmVgGEJvcVzvSgZIzMLqS +zvC89viXtj1pyzQjMLgmuDbs0l47uRHpZXgMGVyF3UuQipPzvhO7G/ZbX/4kUU5b +PabdtvPbjju7dE5PfGrKOThM6HS93Y7QvYTUtwIDAQABAoIBAQCjUL69iFOs7muK +CZGFe/iU1uxQM6XuGPN7mso3z15W07UxdlS3o6ZUSoLTONWcBxP/N4knulHJjZSY +0LivbDYz3htic3t0kdGmxOxPVnLKRXN6ncbQTaeAE8tlBMAcWd/UH2Tlylz+Ac// +6cV3gNJMShwUmhb3l4v3Rml0nZ6PO1pFc/Chk5L9REAV8G6rNtc9bzgmgoFucRO/ +8ce/uJrENt1Pu3vBvmz42DTGfG48v5RZ0OY4qEPawZJ7p+QYiTf6h3Eilss/AllW +PPfQ0thdyB+yrZ3p6qb+ZUYphpGxgg6YlQxLfDKAikuo+EXwjPBPfeHhTO4kAj+h +opDCroZhAoGBANyVFbagCWqwguE6nVPmnCaiNQUIh8b7L2CnkkLfdbPQr/KvyIjg +Ua125bTJhe9Uk+ZBWsobQkjA0Baidiylx51pWYaxPVn5araVmkh2dqMluk2QE82X +AWemBgKhAqCLLLMVXbrRYlxpKUm1Fc/lJ8Ig2R/MJSntTMpQhJtIejUbAoGBAMnt +XMvlFABCoFbI9GMcteI/KkvNGQUy3OKEln/QCssnE4/XIu7LCxy6P+1lycbFy/mQ +0bnp525sPEIIkMpi6LeAbSzYN2O3BRjNrjPcbx6Khz9DweNhRIo5qTFRszZ+pHbV +N+9Oc9JVenwPw6EuW7uZRFKFhCHtsBFdUrWLJoSVAoGAQ3ytdwGBwA2fDW/UgL32 +mm9YT2DrwbpKJYU/X4xkw44ett6HOTGAa9ULtINPogi7c2AdeeZbIk0znSk5hLF3 +4DZCOM5zWdrQhmpBGNh9ta6uUFq7ZFRGDsMh5Z4DYsER/PyVf7neIS3ffviTYtbW +kjNgmrTnzesXanK2D5heI28CgYEAhl+qjRTYhoPP53C7EOmeL/0QzHij2c3LKAJL +lKqBREewwNvNp1L/BhL7T6OY7unZny48IpgBJn5oaxkAIW5IpzSTcnBAC99TSPo2 +ntRmLdDJx9PzRrkHv2Q3r1ZLCEymbV3eZyWx9ZpkdAKZkL0k1mZcDP5Eu79Ml4Ge +9Kiw7TECgYEAh+nTKwrCUFGbe4RIVCj/QG7FVPbq5PdxJ3gILZ3/1XkhPcNRFKJS +u5qPfA02tYEALz9KXATK1uRB/GlBM7Eap/g2GFiHpVxrw6wPpybLywJmNyNTwqiq +eJxQ0FRzW9Kwwn1ThPY38LdFe/wvXZFOcNvGD8hHCLQRdlBR4zuTsBk= +-----END RSA PRIVATE KEY----- diff --git a/src/test.example.com.csr b/src/test.example.com.csr new file mode 100644 index 0000000..8163935 --- /dev/null +++ b/src/test.example.com.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICpTCCAY0CAQAwYDELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx +ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEZMBcGA1UEAxMQdGVz +dC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK39 +lAbBIf8cHf+FmX1RX+bDMlnJ7P9YnRdha6Cw8KlVGEZcaVPym6yBvF/L98OHFT/8 +Tl3dcvUPlvTiytmEnAHh3hPe5h2V23KaMFMb0w7/xjSBx1yv8r2DPEdhBJ2Ue9al +3aF7AmpL/2htnsJJLwNdPHhG5NLu0cTZiLGLDtGOIgbcATPzEpAGD31na3R4WPc9 +JeDPBgMlDrmXNLeo9M8N385I6plYBhCb3Fc70oGSMzC6ks7wvPb4l7Y9acs0IzC4 +Jrg27NJeO7kR6WV4DBlchd1LkIqT874Tuxv2W1/+JFFOWz2m3bbz2447u3ROT3xq +yjk4TOh0vd2O0L2E1LcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBMAUqwty7R +/YWRrC8NuvrbSsW0r7Z7FXWxny5w5ImONCgVffc2wVydtBVQ0rfd3pDZLyu0P4sx +4bJ/KBz67t2MsOKbCMDS7SJuFwHu9AUzaYNh455HeBOVwb6LemJDNnCtMG9DgcRv +2BpwKqekUVTGDuUQmLibjE8qwDHw/p9k4gjQBxlfJe2sIZGs6oA/JGFJUU6ZIn8Y +M6aazrbjWexbWCnjhiXkNa8kfKiSHzU+2ct+GY5QxI221+63bXRiAi2/LK0gaY+p ++3vYu75F7+8oPZOfsGmYEyPz7c1jPqcwPgVDk+sdvl1MO1TGFRaFNIlRP1DhpHkj +fuJ/id6oUHhj +-----END CERTIFICATE REQUEST----- diff --git a/src/test.example.com.csr.json b/src/test.example.com.csr.json new file mode 100644 index 0000000..5cd3e1e --- /dev/null +++ b/src/test.example.com.csr.json @@ -0,0 +1,14 @@ +{ + "CN": "test.example.com", + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "AU", + "ST": "Some-State", + "O": "Internet Widgits Pty Ltd" + } + ] +} diff --git a/src/test.example.com.pem b/src/test.example.com.pem new file mode 100644 index 0000000..1c7e71e --- /dev/null +++ b/src/test.example.com.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwTCCAqmgAwIBAgIUDhKNhGRUq1TSHD6aG2k4TRR8iA0wDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoT +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEXMBUGA1UEAxMOY2EubmdodHRwMi5v +cmcwHhcNMTYwNjI1MDkzNzAwWhcNMjYwNjIzMDkzNzAwWjBgMQswCQYDVQQGEwJB +VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRkwFwYDVQQDExB0ZXN0LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEArf2UBsEh/xwd/4WZfVFf5sMyWcns/1idF2Fr +oLDwqVUYRlxpU/KbrIG8X8v3w4cVP/xOXd1y9Q+W9OLK2YScAeHeE97mHZXbcpow +UxvTDv/GNIHHXK/yvYM8R2EEnZR71qXdoXsCakv/aG2ewkkvA108eEbk0u7RxNmI +sYsO0Y4iBtwBM/MSkAYPfWdrdHhY9z0l4M8GAyUOuZc0t6j0zw3fzkjqmVgGEJvc +VzvSgZIzMLqSzvC89viXtj1pyzQjMLgmuDbs0l47uRHpZXgMGVyF3UuQipPzvhO7 +G/ZbX/4kUU5bPabdtvPbjju7dE5PfGrKOThM6HS93Y7QvYTUtwIDAQABo3UwczAO +BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIw +ADAdBgNVHQ4EFgQUm8jn1FICope9qUce6ORQ0CtbmhYwHwYDVR0jBBgwFoAU0DnF +VHVlxrFEkv1ULqnO4ua924YwDQYJKoZIhvcNAQELBQADggEBAD7RPz/5rAnS1MNP +JfAj1TXZSBwlYgtmJL65yaFB6a1SNSTo15deAm/1Vl10LbmYdV4sVnGKeZjhKNk+ +bvVzetUSUS7Rh1fHtxlivJFkG1VrvPu9b416l2aKftBiaNyAWXbyjqXwLYli6Ehk +uu6jZd0040Ggh7bY+KMSnDFDrp7Rar7OvGu9Iovs+sPdkc/iEbvwEiXdMjf3gwkT +Wqx6br1VDLzhD83HAsFA9tt5fv6KTf91UgJnCmOi81Uo6fSEJG84g32T25gwwmCK +q4U049aGF/f4u3QuWDsfYqNePycurAg3m5PC0wCoqvpY2u/q+PGbjWMi2PfZsF8U +imgl/L0= +-----END CERTIFICATE----- diff --git a/src/test.nghttp2.org-key.pem b/src/test.nghttp2.org-key.pem new file mode 100644 index 0000000..2532895 --- /dev/null +++ b/src/test.nghttp2.org-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA7p6KKa3ctS+Sr/nf2uTKNtTshuDVzTsBTbaGydj8q0YDmT3n +CnOPWXvvG1N+jJv5pcAXN2ZnV9UpGh3N5g/CaRcFTgQQ8o+NlCXYBdPIXAJ+Kkbx +limDw3n9xIXfeL6+V2QuPNrqh6n23xwDg5boKaNkpf7X5OrjT1Ph57SEfX1op3GX +bwkAP2+3WlxxYYs0htRq2gH97q9J4MlhHPDapi+uKGs+2b1y6Uxgf4nD5jEWdPmy +VqeKs+fT4ja2n+3gujpdOo2lg504p50gL4zP8zhAlcqlQCmeJGL1xFzCtm2wHQo7 +6XHSWca4pJ7rxf2oIdtE7ikvgFlTVXnG1T3TEQIDAQABAoIBAQDlX/UD96MPcDmb +e6EZ85AGgUsUpJAhBjVMlMagxTqtEVJoPj8XptoHdMD2DZ66XzztfedTU9bHcZpf +BoNkQYXqKzzoL7Ry1leML4ymnVweRi8tSKD2bdXBVEUCYoXctc6WhzCDQxTrcBBl +i7I9DhUB4ZTglEbIQJpdKQ8hAj/Rt55KWSxc+8X7ItSdtMrq+uz+pqg4PkysVAFS +3aDybOqiI/2hzOvwQU4HaB48uUQwpOU6EGidt0C5nAdWOQMbS8kkCJz6UODiUfdM +mLIyA4ygkQ45QthzrddKauUMhUd/y1SAFJOambR4ZiyA+bItomlbNq018sFx3FDr +Uvg7nz2ZAoGBAPC6aO4W0U7vsWyL/lgC7ybFbtPJ4emj4wK/W87qx3LW1/dRgl7Q +h6oblZTFK/oV6xA7J2/Foocz/s1ntnyIdxrtrZUYAIiBXlrWhDg9+MnnmErfXx7H +CkRyWH6i9JbTRUeiWRNBGQ9yMkwQPc2Ckytxrh7w9M+RVCpyzUh8lX+XAoGBAP3B +4V8cF3bVEUOk0tHshR5m2kcJ22qmUv8WUG+IdRUDb4tRpzVFC8HcKedEjk3jxkXR +UInRSD+hLhx0HIhjZKWqJffSZI/G3U8AqoKu9+eh/xHZCah/8KW1YWNsn4rROcyZ +5XFRiMn7psWTjLEZ17zQS4rk9g65SKc9u1wtTxeXAoGAIY3qOF2n2Tfh5D5zOnNW +QHI+q3i1a6qzZtujgWkKWgCGY+vRn0Oz1Us5A16kbZyGgmGscpD6wZvGxXzSW/Nt +nqxIiMKquFxH+aNzFJ/WwNXuTWlrSc/2p2nE2gn+y9MxEfYYMm3df2CskBuncbDk +sKaM3bU6eoBIWg5cfOEYuYsCgYACB2bR59uYK6PzsoGtBAMcdx4Pq1iBxcqsF3WV +LrYg8OIXbxOzLVYmuqfrHXU10jhnnoDSWUYGnDdOKu9/d6v6Vx3umVQMgj6Kvyqd +2OBKjdUIQ3/8ROmbqZOZw+iSp5GavTBEc65wTv7KXZ+mWtqKu++esK32+CxIignR +dttHCQKBgQDZUt94wj9s5p7H5LH6hxyqNr6P9JYEuYPao5l/3mOJ8N330wKuN2G+ +GUg7p/AhtQHwdoyErlsQavKZa791oCvfJiOURYa8gYU03sYsyI1tV45UexCwl40f +oS+VQYgU16UdYo9B2petecEPNpM+mgpne3qzVUwJ5NUNURgmWpyiQw== +-----END RSA PRIVATE KEY----- diff --git a/src/test.nghttp2.org.csr b/src/test.nghttp2.org.csr new file mode 100644 index 0000000..dc4bb10 --- /dev/null +++ b/src/test.nghttp2.org.csr @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDATCCAekCAQAwZDELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx +ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEdMBsGA1UEAxMUbm90 +LXVzZWQubmdodHRwMi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDunooprdy1L5Kv+d/a5Mo21OyG4NXNOwFNtobJ2PyrRgOZPecKc49Ze+8bU36M +m/mlwBc3ZmdX1SkaHc3mD8JpFwVOBBDyj42UJdgF08hcAn4qRvGWKYPDef3Ehd94 +vr5XZC482uqHqfbfHAODlugpo2Sl/tfk6uNPU+HntIR9fWincZdvCQA/b7daXHFh +izSG1GraAf3ur0ngyWEc8NqmL64oaz7ZvXLpTGB/icPmMRZ0+bJWp4qz59PiNraf +7eC6Ol06jaWDnTinnSAvjM/zOECVyqVAKZ4kYvXEXMK2bbAdCjvpcdJZxriknuvF +/agh20TuKS+AWVNVecbVPdMRAgMBAAGgWDBWBgkqhkiG9w0BCQ4xSTBHMEUGA1Ud +EQQ+MDyCEFRFU1QuTkdIVFRQMi5PUkeCEioudGVzdC5uZ2h0dHAyLm9yZ4IUdyp3 +LnRlc3QubmdodHRwMi5vcmcwDQYJKoZIhvcNAQELBQADggEBAIAEwnoM5moRwO5U +eaeVCuzpxw1qQsB769GyQu+ey1aa+2BYflirv/FW+8x/uzQpCWGEgHqd5w+MXyXA +PsyucHgKh5Ia6MUW6xxlHkkOtVtmZiH7lXWv90RNtdfHHGWnBzw8iGsk5WfEaNho +NlPiuYLiFqA7W6jR/c4kOg3zziDlwTXaH6SWLCuDzLTb7E7nGcrWkN6moYj+QlSx +viA4GsqDBoFgXT7cSfUzS8ZwIjrqbx7C1xkzPEt5jAiCD/UBX9ot0G+lEgCv3UQj +Q1KkY+TO3bzMkt/kQSX2Q6plKj8D77tlDfFCjd77VC2lL3Qmzaz+M6T7uF+wyl9W +AQJvoUg= +-----END CERTIFICATE REQUEST----- diff --git a/src/test.nghttp2.org.csr.json b/src/test.nghttp2.org.csr.json new file mode 100644 index 0000000..5ee3069 --- /dev/null +++ b/src/test.nghttp2.org.csr.json @@ -0,0 +1,19 @@ +{ + "CN": "not-used.nghttp2.org", + "hosts": [ + "TEST.NGHTTP2.ORG", + "*.test.nghttp2.org", + "w*w.test.nghttp2.org" + ], + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "AU", + "ST": "Some-State", + "O": "Internet Widgits Pty Ltd" + } + ] +} diff --git a/src/test.nghttp2.org.pem b/src/test.nghttp2.org.pem new file mode 100644 index 0000000..0c386fc --- /dev/null +++ b/src/test.nghttp2.org.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEDjCCAvagAwIBAgIUQBCY8Nre85JT1c7P+HbXUF9yzg8wDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoT +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEXMBUGA1UEAxMOY2EubmdodHRwMi5v +cmcwHhcNMTYwNjI1MDkzMzAwWhcNMjYwNjIzMDkzMzAwWjBkMQswCQYDVQQGEwJB +VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMR0wGwYDVQQDExRub3QtdXNlZC5uZ2h0dHAyLm9yZzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAO6eiimt3LUvkq/539rkyjbU7Ibg1c07 +AU22hsnY/KtGA5k95wpzj1l77xtTfoyb+aXAFzdmZ1fVKRodzeYPwmkXBU4EEPKP +jZQl2AXTyFwCfipG8ZYpg8N5/cSF33i+vldkLjza6oep9t8cA4OW6CmjZKX+1+Tq +409T4ee0hH19aKdxl28JAD9vt1pccWGLNIbUatoB/e6vSeDJYRzw2qYvrihrPtm9 +culMYH+Jw+YxFnT5slanirPn0+I2tp/t4Lo6XTqNpYOdOKedIC+Mz/M4QJXKpUAp +niRi9cRcwrZtsB0KO+lx0lnGuKSe68X9qCHbRO4pL4BZU1V5xtU90xECAwEAAaOB +vTCBujAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0T +AQH/BAIwADAdBgNVHQ4EFgQUGlxgxowH6jrQiyyFpCbwPkCXXIYwHwYDVR0jBBgw +FoAU0DnFVHVlxrFEkv1ULqnO4ua924YwRQYDVR0RBD4wPIIQVEVTVC5OR0hUVFAy +Lk9SR4ISKi50ZXN0Lm5naHR0cDIub3JnghR3KncudGVzdC5uZ2h0dHAyLm9yZzAN +BgkqhkiG9w0BAQsFAAOCAQEANCqM6ocfqOpgDEHYOOQTGFHJIptQhS3kRYAdTIo2 +G8XvGCoy+CDYe1GAUWbxE090+a1I1rsYMHcWKJnjKaCBZid7KMhyayIvrmgEsOCh +L8iLf3bxkCoyIAmCpxJwa3LMxm2QQLtRx8AoMXWf+N8are4HY6MLNn6aP4zaTrTZ +H+WkjKIh7WjSHtW/ro666PCXJDCCdRXljOf8v/fff3bYiLg8o70RBp7OFM0HaPtK +wCfcLLxBeoVIncWswB6GtVUFhLeGjepDzWpuDHOdw6DtpghwSXvWFu9bRtl+x02m +LAGfJ0kJrpYGfr9UB51NFX3aM/D3p2zxrjKwR2b59vJEcA== +-----END CERTIFICATE----- diff --git a/src/testdata/Makefile.am b/src/testdata/Makefile.am new file mode 100644 index 0000000..b1cb575 --- /dev/null +++ b/src/testdata/Makefile.am @@ -0,0 +1,27 @@ +# nghttp2 - HTTP/2 C Library + +# Copyright (c) 2023 Tatsuhiro Tsujikawa + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +EXTRA_DIST = \ + ipaddr.crt \ + nosan.crt \ + nosan_ip.crt \ + verify_hostname.crt diff --git a/src/testdata/ipaddr.crt b/src/testdata/ipaddr.crt new file mode 100644 index 0000000..cdacdf0 --- /dev/null +++ b/src/testdata/ipaddr.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBfDCCASKgAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwsxOTIuMTY4 +LjAuMTAeFw0yMzAzMTUxMjQ5MDBaFw0zMzAxMjExMjQ5MDBaMBYxFDASBgNVBAMT +CzE5Mi4xNjguMC4xMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERDh3hNne6xGM +fOrf7ln5EFnlLpk98qadBx3MKjG5gAfMYHzf/S7v19G608sH1LtabubV+Tvjllon +K56G2Gk0+6NhMF8wDgYDVR0PAQH/BAQDAgeAME0GA1UdEQRGMESCE25naHR0cDIu +ZXhhbXBsZS5jb22CFSoubmdodHRwMi5leGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAA +AAAAAAAAAAAAATAKBggqhkjOPQQDAgNIADBFAiEA3jZzO49MYccR5mYS08qVUCdh +HsEAC8GhRXFwL6zvf2ACIFAJrca2zTU4QRjV6V+LGRHc2ZocE2e7wFTLobblmDfB +-----END CERTIFICATE----- diff --git a/src/testdata/nosan.crt b/src/testdata/nosan.crt new file mode 100644 index 0000000..ebbb5c0 --- /dev/null +++ b/src/testdata/nosan.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBKDCBz6ADAgECAgEBMAoGCCqGSM49BAMCMBQxEjAQBgNVBAMTCWxvY2FsaG9z +dDAeFw0yMzAzMTUxMjQzMzhaFw0zMzAxMjExMjQzMzhaMBQxEjAQBgNVBAMTCWxv +Y2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIEpWYgtXtcx0uJ2oFPK +RiII93iw5ITMrhMfBXQ0SzCfkUdvCJ0gNW+3isTBu4Jt0URpgP37eGwiJf2wPApq +KpajEjAQMA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAgNIADBFAiEA4IYil4G4 +cMxaVkcAnMGgiSdn7/qIgdhFB0Vx5AOd+EUCIGubRPhsXAJXvG//cK25mmxi3Wax +r7AgRKuDtWxn2bCO +-----END CERTIFICATE----- diff --git a/src/testdata/nosan_ip.crt b/src/testdata/nosan_ip.crt new file mode 100644 index 0000000..717a1bd --- /dev/null +++ b/src/testdata/nosan_ip.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBJzCBz6ADAgECAgEBMAoGCCqGSM49BAMCMBQxEjAQBgNVBAMTCTEyNy4wLjAu +MTAeFw0yMzAzMTUxMjQ1MTVaFw0zMzAxMjExMjQ1MTVaMBQxEjAQBgNVBAMTCTEy +Ny4wLjAuMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOXGPfSzXoeD7jszmAQO +qAhak5HQMTmj32Q/xqO9WmCnXRQ+T06701o6q1hjotrC/HdMk9kabsKHc9V7Bk4O +zkGjEjAQMA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAgNHADBEAiAI3fKrkNTN +IEo9qI8bd/pZ6on4d9vLcnHtqYhcuWZGTwIgW2zYMwASLUw4H1k/prBtTEEJOahJ +bvFs3oMbJEprQ+g= +-----END CERTIFICATE----- diff --git a/src/testdata/verify_hostname.crt b/src/testdata/verify_hostname.crt new file mode 100644 index 0000000..a327616 --- /dev/null +++ b/src/testdata/verify_hostname.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBeTCCAR6gAwIBAgIBATAKBggqhkjOPQQDAjAUMRIwEAYDVQQDEwlsb2NhbGhv +c3QwHhcNMjMwMzE1MTIzNzU1WhcNMzMwMTIxMTIzNzU1WjAUMRIwEAYDVQQDEwls +b2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATMHcWmb55fi0KHNDwM +cYzVTAOfzJf44AqrqC+Pq2zW/ig8tPZbXf3eA/Vvp07Di+yWmuo3fGatUcY4nsx+ +Jd62o2EwXzAOBgNVHQ8BAf8EBAMCB4AwTQYDVR0RBEYwRIITbmdodHRwMi5leGFt +cGxlLmNvbYIVKi5uZ2h0dHAyLmV4YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAA +AAAAAAABMAoGCCqGSM49BAMCA0kAMEYCIQDQJFRJ3Ah4cGy7bwpkzVYeTgG+NhDa +55F4dPtJp9dS8wIhALQ9qf379lke1jVHg2t84iZLo3bL23RgICMezEYvqO3K +-----END CERTIFICATE----- diff --git a/src/timegm.c b/src/timegm.c new file mode 100644 index 0000000..3a671b1 --- /dev/null +++ b/src/timegm.c @@ -0,0 +1,88 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "timegm.h" + +#include <inttypes.h> + +/* Counter the number of leap year in the range [0, y). The |y| is the + year, including century (e.g., 2012) */ +static int count_leap_year(int y) { + y -= 1; + return y / 4 - y / 100 + y / 400; +} + +/* Based on the algorithm of Python 2.7 calendar.timegm. */ +time_t nghttp2_timegm(struct tm *tm) { + int days; + int num_leap_year; + int64_t t; + if (tm->tm_mon > 11) { + return -1; + } + num_leap_year = count_leap_year(tm->tm_year + 1900) - count_leap_year(1970); + days = (tm->tm_year - 70) * 365 + num_leap_year + tm->tm_yday; + t = ((int64_t)days * 24 + tm->tm_hour) * 3600 + tm->tm_min * 60 + tm->tm_sec; + + if (sizeof(time_t) == 4) { + if (t < INT32_MIN || t > INT32_MAX) { + return -1; + } + } + + return (time_t)t; +} + +/* Returns nonzero if the |y| is the leap year. The |y| is the year, + including century (e.g., 2012) */ +static int is_leap_year(int y) { + return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); +} + +/* The number of days before ith month begins */ +static int daysum[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + +time_t nghttp2_timegm_without_yday(struct tm *tm) { + int days; + int num_leap_year; + int64_t t; + if (tm->tm_mon > 11) { + return -1; + } + num_leap_year = count_leap_year(tm->tm_year + 1900) - count_leap_year(1970); + days = (tm->tm_year - 70) * 365 + num_leap_year + daysum[tm->tm_mon] + + tm->tm_mday - 1; + if (tm->tm_mon >= 2 && is_leap_year(tm->tm_year + 1900)) { + ++days; + } + t = ((int64_t)days * 24 + tm->tm_hour) * 3600 + tm->tm_min * 60 + tm->tm_sec; + + if (sizeof(time_t) == 4) { + if (t < INT32_MIN || t > INT32_MAX) { + return -1; + } + } + + return (time_t)t; +} diff --git a/src/timegm.h b/src/timegm.h new file mode 100644 index 0000000..152917c --- /dev/null +++ b/src/timegm.h @@ -0,0 +1,49 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TIMEGM_H +#define TIMEGM_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <time.h> + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +time_t nghttp2_timegm(struct tm *tm); + +/* Just like nghttp2_timegm, but without using tm->tm_yday. This is + useful if we use tm from strptime, since some platforms do not + calculate tm_yday with that call. */ +time_t nghttp2_timegm_without_yday(struct tm *tm); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* TIMEGM_H */ diff --git a/src/tls.cc b/src/tls.cc new file mode 100644 index 0000000..e88dc56 --- /dev/null +++ b/src/tls.cc @@ -0,0 +1,125 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "tls.h" + +#include <cassert> +#include <vector> +#include <mutex> +#include <iostream> + +#include <openssl/crypto.h> +#include <openssl/conf.h> + +#include "ssl_compat.h" + +namespace nghttp2 { + +namespace tls { + +const char *get_tls_protocol(SSL *ssl) { + switch (SSL_version(ssl)) { + case SSL2_VERSION: + return "SSLv2"; + case SSL3_VERSION: + return "SSLv3"; +#ifdef TLS1_3_VERSION + case TLS1_3_VERSION: + return "TLSv1.3"; +#endif // TLS1_3_VERSION + case TLS1_2_VERSION: + return "TLSv1.2"; + case TLS1_1_VERSION: + return "TLSv1.1"; + case TLS1_VERSION: + return "TLSv1"; + default: + return "unknown"; + } +} + +TLSSessionInfo *get_tls_session_info(TLSSessionInfo *tls_info, SSL *ssl) { + if (!ssl) { + return nullptr; + } + + auto session = SSL_get_session(ssl); + if (!session) { + return nullptr; + } + + tls_info->cipher = SSL_get_cipher_name(ssl); + tls_info->protocol = get_tls_protocol(ssl); + tls_info->session_reused = SSL_session_reused(ssl); + + unsigned int session_id_length; + tls_info->session_id = SSL_SESSION_get_id(session, &session_id_length); + tls_info->session_id_length = session_id_length; + + return tls_info; +} + +/* Conditional logic w/ lookup tables to check if id is one of the + the block listed cipher suites for HTTP/2 described in RFC 7540. + https://github.com/jay/http2_blacklisted_ciphers +*/ +#define IS_CIPHER_BANNED_METHOD2(id) \ + ((0x0000 <= id && id <= 0x00FF && \ + "\xFF\xFF\xFF\xCF\xFF\xFF\xFF\xFF\x7F\x00\x00\x00\x80\x3F\x00\x00" \ + "\xF0\xFF\xFF\x3F\xF3\xF3\xFF\xFF\x3F\x00\x00\x00\x00\x00\x00\x80" \ + [(id & 0xFF) / 8] & \ + (1 << (id % 8))) || \ + (0xC000 <= id && id <= 0xC0FF && \ + "\xFE\xFF\xFF\xFF\xFF\x67\xFE\xFF\xFF\xFF\x33\xCF\xFC\xCF\xFF\xCF" \ + "\x3C\xF3\xFC\x3F\x33\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + [(id & 0xFF) / 8] & \ + (1 << (id % 8)))) + +bool check_http2_cipher_block_list(SSL *ssl) { + int id = SSL_CIPHER_get_id(SSL_get_current_cipher(ssl)) & 0xFFFFFF; + + return IS_CIPHER_BANNED_METHOD2(id); +} + +bool check_http2_tls_version(SSL *ssl) { + auto tls_ver = SSL_version(ssl); + + return tls_ver >= TLS1_2_VERSION; +} + +bool check_http2_requirement(SSL *ssl) { + return check_http2_tls_version(ssl) && !check_http2_cipher_block_list(ssl); +} + +int ssl_ctx_set_proto_versions(SSL_CTX *ssl_ctx, int min, int max) { + if (SSL_CTX_set_min_proto_version(ssl_ctx, min) != 1 || + SSL_CTX_set_max_proto_version(ssl_ctx, max) != 1) { + return -1; + } + return 0; +} + +} // namespace tls + +} // namespace nghttp2 diff --git a/src/tls.h b/src/tls.h new file mode 100644 index 0000000..59e2ccd --- /dev/null +++ b/src/tls.h @@ -0,0 +1,104 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef TLS_H +#define TLS_H + +#include "nghttp2_config.h" + +#include <cinttypes> + +#include <openssl/ssl.h> + +#include "ssl_compat.h" + +namespace nghttp2 { + +namespace tls { + +// Recommended general purpose "Intermediate compatibility" cipher +// suites for TLSv1.2 by mozilla. +// +// https://wiki.mozilla.org/Security/Server_Side_TLS +constexpr char DEFAULT_CIPHER_LIST[] = + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-" + "AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-" + "POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-" + "AES256-GCM-SHA384"; + +// Recommended general purpose "Modern compatibility" cipher suites +// for TLSv1.3 by mozilla. +// +// https://wiki.mozilla.org/Security/Server_Side_TLS +constexpr char DEFAULT_TLS13_CIPHER_LIST[] = +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_LIBRESSL) + "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256" +#else // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_LIBRESSL + "" +#endif // !NGHTTP2_GENUINE_OPENSSL && !NGHTTP2_OPENSSL_IS_LIBRESSL + ; + +constexpr auto NGHTTP2_TLS_MIN_VERSION = TLS1_VERSION; +#ifdef TLS1_3_VERSION +constexpr auto NGHTTP2_TLS_MAX_VERSION = TLS1_3_VERSION; +#else // !TLS1_3_VERSION +constexpr auto NGHTTP2_TLS_MAX_VERSION = TLS1_2_VERSION; +#endif // !TLS1_3_VERSION + +const char *get_tls_protocol(SSL *ssl); + +struct TLSSessionInfo { + const char *cipher; + const char *protocol; + const uint8_t *session_id; + bool session_reused; + size_t session_id_length; +}; + +TLSSessionInfo *get_tls_session_info(TLSSessionInfo *tls_info, SSL *ssl); + +// Returns true iff the negotiated protocol is TLSv1.2. +bool check_http2_tls_version(SSL *ssl); + +// Returns true iff the negotiated cipher suite is in HTTP/2 cipher +// block list. +bool check_http2_cipher_block_list(SSL *ssl); + +// Returns true if SSL/TLS requirement for HTTP/2 is fulfilled. +// To fulfill the requirement, the following 2 terms must be hold: +// +// 1. The negotiated protocol must be TLSv1.2. +// 2. The negotiated cipher cuite is not listed in the block list +// described in RFC 7540. +bool check_http2_requirement(SSL *ssl); + +// Sets TLS min and max versions to |ssl_ctx|. This function returns +// 0 if it succeeds, or -1. +int ssl_ctx_set_proto_versions(SSL_CTX *ssl_ctx, int min, int max); + +} // namespace tls + +} // namespace nghttp2 + +#endif // TLS_H diff --git a/src/util.cc b/src/util.cc new file mode 100644 index 0000000..0996c0a --- /dev/null +++ b/src/util.cc @@ -0,0 +1,1796 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util.h" + +#include <sys/types.h> +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif // HAVE_SYS_SOCKET_H +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H +#include <sys/stat.h> +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#ifdef HAVE_NETINET_IP_H +# include <netinet/ip.h> +#endif // HAVE_NETINET_IP_H +#include <netinet/udp.h> +#ifdef _WIN32 +# include <ws2tcpip.h> +#else // !_WIN32 +# include <netinet/tcp.h> +#endif // !_WIN32 +#ifdef HAVE_ARPA_INET_H +# include <arpa/inet.h> +#endif // HAVE_ARPA_INET_H + +#include <cmath> +#include <cerrno> +#include <cassert> +#include <cstdio> +#include <cstring> +#include <ctime> +#include <iostream> +#include <fstream> +#include <iomanip> + +#include <openssl/evp.h> + +#include <nghttp2/nghttp2.h> + +#include "ssl_compat.h" +#include "timegm.h" + +namespace nghttp2 { + +namespace util { + +#ifndef _WIN32 +namespace { +int nghttp2_inet_pton(int af, const char *src, void *dst) { + return inet_pton(af, src, dst); +} +} // namespace +#else // _WIN32 +namespace { +// inet_pton-wrapper for Windows +int nghttp2_inet_pton(int af, const char *src, void *dst) { +# if _WIN32_WINNT >= 0x0600 + return InetPtonA(af, src, dst); +# else + // the function takes a 'char*', so we need to make a copy + char addr[INET6_ADDRSTRLEN + 1]; + strncpy(addr, src, sizeof(addr)); + addr[sizeof(addr) - 1] = 0; + + int size = sizeof(struct in6_addr); + + if (WSAStringToAddress(addr, af, nullptr, (LPSOCKADDR)dst, &size) == 0) + return 1; + return 0; +# endif +} +} // namespace +#endif // _WIN32 + +const char UPPER_XDIGITS[] = "0123456789ABCDEF"; + +bool in_rfc3986_unreserved_chars(const char c) { + switch (c) { + case '-': + case '.': + case '_': + case '~': + return true; + } + + return is_alpha(c) || is_digit(c); +} + +bool in_rfc3986_sub_delims(const char c) { + switch (c) { + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + return true; + } + + return false; +} + +std::string percent_encode(const unsigned char *target, size_t len) { + std::string dest; + for (size_t i = 0; i < len; ++i) { + unsigned char c = target[i]; + + if (in_rfc3986_unreserved_chars(c)) { + dest += c; + } else { + dest += '%'; + dest += UPPER_XDIGITS[c >> 4]; + dest += UPPER_XDIGITS[(c & 0x0f)]; + } + } + return dest; +} + +std::string percent_encode(const std::string &target) { + return percent_encode(reinterpret_cast<const unsigned char *>(target.c_str()), + target.size()); +} + +bool in_token(char c) { + switch (c) { + case '!': + case '#': + case '$': + case '%': + case '&': + case '\'': + case '*': + case '+': + case '-': + case '.': + case '^': + case '_': + case '`': + case '|': + case '~': + return true; + } + + return is_alpha(c) || is_digit(c); +} + +bool in_attr_char(char c) { + switch (c) { + case '*': + case '\'': + case '%': + return false; + } + + return util::in_token(c); +} + +StringRef percent_encode_token(BlockAllocator &balloc, + const StringRef &target) { + auto iov = make_byte_ref(balloc, target.size() * 3 + 1); + auto p = percent_encode_token(iov.base, target); + + *p = '\0'; + + return StringRef{iov.base, p}; +} + +size_t percent_encode_tokenlen(const StringRef &target) { + size_t n = 0; + + for (auto first = std::begin(target); first != std::end(target); ++first) { + uint8_t c = *first; + + if (c != '%' && in_token(c)) { + ++n; + continue; + } + + // percent-encoded character '%ff' + n += 3; + } + + return n; +} + +uint32_t hex_to_uint(char c) { + if (c <= '9') { + return c - '0'; + } + if (c <= 'Z') { + return c - 'A' + 10; + } + if (c <= 'z') { + return c - 'a' + 10; + } + return 256; +} + +StringRef quote_string(BlockAllocator &balloc, const StringRef &target) { + auto cnt = std::count(std::begin(target), std::end(target), '"'); + + if (cnt == 0) { + return make_string_ref(balloc, target); + } + + auto iov = make_byte_ref(balloc, target.size() + cnt + 1); + auto p = quote_string(iov.base, target); + + *p = '\0'; + + return StringRef{iov.base, p}; +} + +size_t quote_stringlen(const StringRef &target) { + size_t n = 0; + + for (auto c : target) { + if (c == '"') { + n += 2; + } else { + ++n; + } + } + + return n; +} + +namespace { +template <typename Iterator> +Iterator cpydig(Iterator d, uint32_t n, size_t len) { + auto p = d + len - 1; + + do { + *p-- = (n % 10) + '0'; + n /= 10; + } while (p >= d); + + return d + len; +} +} // namespace + +namespace { +constexpr const char *MONTH[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +constexpr const char *DAY_OF_WEEK[] = {"Sun", "Mon", "Tue", "Wed", + "Thu", "Fri", "Sat"}; +} // namespace + +std::string http_date(time_t t) { + /* Sat, 27 Sep 2014 06:31:15 GMT */ + std::string res(29, 0); + http_date(&res[0], t); + return res; +} + +char *http_date(char *res, time_t t) { + struct tm tms; + + if (gmtime_r(&t, &tms) == nullptr) { + return res; + } + + auto p = res; + + auto s = DAY_OF_WEEK[tms.tm_wday]; + p = std::copy_n(s, 3, p); + *p++ = ','; + *p++ = ' '; + p = cpydig(p, tms.tm_mday, 2); + *p++ = ' '; + s = MONTH[tms.tm_mon]; + p = std::copy_n(s, 3, p); + *p++ = ' '; + p = cpydig(p, tms.tm_year + 1900, 4); + *p++ = ' '; + p = cpydig(p, tms.tm_hour, 2); + *p++ = ':'; + p = cpydig(p, tms.tm_min, 2); + *p++ = ':'; + p = cpydig(p, tms.tm_sec, 2); + s = " GMT"; + p = std::copy_n(s, 4, p); + + return p; +} + +std::string common_log_date(time_t t) { + // 03/Jul/2014:00:19:38 +0900 + std::string res(26, 0); + common_log_date(&res[0], t); + return res; +} + +char *common_log_date(char *res, time_t t) { + struct tm tms; + + if (localtime_r(&t, &tms) == nullptr) { + return res; + } + + auto p = res; + + p = cpydig(p, tms.tm_mday, 2); + *p++ = '/'; + auto s = MONTH[tms.tm_mon]; + p = std::copy_n(s, 3, p); + *p++ = '/'; + p = cpydig(p, tms.tm_year + 1900, 4); + *p++ = ':'; + p = cpydig(p, tms.tm_hour, 2); + *p++ = ':'; + p = cpydig(p, tms.tm_min, 2); + *p++ = ':'; + p = cpydig(p, tms.tm_sec, 2); + *p++ = ' '; + +#ifdef HAVE_STRUCT_TM_TM_GMTOFF + auto gmtoff = tms.tm_gmtoff; +#else // !HAVE_STRUCT_TM_TM_GMTOFF + auto gmtoff = nghttp2_timegm(&tms) - t; +#endif // !HAVE_STRUCT_TM_TM_GMTOFF + if (gmtoff >= 0) { + *p++ = '+'; + } else { + *p++ = '-'; + gmtoff = -gmtoff; + } + + p = cpydig(p, gmtoff / 3600, 2); + p = cpydig(p, (gmtoff % 3600) / 60, 2); + + return p; +} + +std::string iso8601_date(int64_t ms) { + // 2014-11-15T12:58:24.741Z + // 2014-11-15T12:58:24.741+09:00 + std::string res(29, 0); + auto p = iso8601_date(&res[0], ms); + res.resize(p - &res[0]); + return res; +} + +char *iso8601_date(char *res, int64_t ms) { + time_t sec = ms / 1000; + + tm tms; + if (localtime_r(&sec, &tms) == nullptr) { + return res; + } + + auto p = res; + + p = cpydig(p, tms.tm_year + 1900, 4); + *p++ = '-'; + p = cpydig(p, tms.tm_mon + 1, 2); + *p++ = '-'; + p = cpydig(p, tms.tm_mday, 2); + *p++ = 'T'; + p = cpydig(p, tms.tm_hour, 2); + *p++ = ':'; + p = cpydig(p, tms.tm_min, 2); + *p++ = ':'; + p = cpydig(p, tms.tm_sec, 2); + *p++ = '.'; + p = cpydig(p, ms % 1000, 3); + +#ifdef HAVE_STRUCT_TM_TM_GMTOFF + auto gmtoff = tms.tm_gmtoff; +#else // !HAVE_STRUCT_TM_TM_GMTOFF + auto gmtoff = nghttp2_timegm(&tms) - sec; +#endif // !HAVE_STRUCT_TM_TM_GMTOFF + if (gmtoff == 0) { + *p++ = 'Z'; + } else { + if (gmtoff > 0) { + *p++ = '+'; + } else { + *p++ = '-'; + gmtoff = -gmtoff; + } + p = cpydig(p, gmtoff / 3600, 2); + *p++ = ':'; + p = cpydig(p, (gmtoff % 3600) / 60, 2); + } + + return p; +} + +char *iso8601_basic_date(char *res, int64_t ms) { + time_t sec = ms / 1000; + + tm tms; + if (localtime_r(&sec, &tms) == nullptr) { + return res; + } + + auto p = res; + + p = cpydig(p, tms.tm_year + 1900, 4); + p = cpydig(p, tms.tm_mon + 1, 2); + p = cpydig(p, tms.tm_mday, 2); + *p++ = 'T'; + p = cpydig(p, tms.tm_hour, 2); + p = cpydig(p, tms.tm_min, 2); + p = cpydig(p, tms.tm_sec, 2); + *p++ = '.'; + p = cpydig(p, ms % 1000, 3); + +#ifdef HAVE_STRUCT_TM_TM_GMTOFF + auto gmtoff = tms.tm_gmtoff; +#else // !HAVE_STRUCT_TM_TM_GMTOFF + auto gmtoff = nghttp2_timegm(&tms) - sec; +#endif // !HAVE_STRUCT_TM_TM_GMTOFF + if (gmtoff == 0) { + *p++ = 'Z'; + } else { + if (gmtoff > 0) { + *p++ = '+'; + } else { + *p++ = '-'; + gmtoff = -gmtoff; + } + p = cpydig(p, gmtoff / 3600, 2); + p = cpydig(p, (gmtoff % 3600) / 60, 2); + } + + return p; +} + +time_t parse_http_date(const StringRef &s) { + tm tm{}; +#ifdef _WIN32 + // there is no strptime - use std::get_time + std::stringstream sstr(s.str()); + sstr >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S GMT"); + if (sstr.fail()) { + return 0; + } +#else // !_WIN32 + char *r = strptime(s.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &tm); + if (r == 0) { + return 0; + } +#endif // !_WIN32 + return nghttp2_timegm_without_yday(&tm); +} + +time_t parse_openssl_asn1_time_print(const StringRef &s) { + tm tm{}; + auto r = strptime(s.c_str(), "%b %d %H:%M:%S %Y GMT", &tm); + if (r == nullptr) { + return 0; + } + return nghttp2_timegm_without_yday(&tm); +} + +char upcase(char c) { + if ('a' <= c && c <= 'z') { + return c - 'a' + 'A'; + } else { + return c; + } +} + +std::string format_hex(const unsigned char *s, size_t len) { + std::string res; + res.resize(len * 2); + + for (size_t i = 0; i < len; ++i) { + unsigned char c = s[i]; + + res[i * 2] = LOWER_XDIGITS[c >> 4]; + res[i * 2 + 1] = LOWER_XDIGITS[c & 0x0f]; + } + return res; +} + +StringRef format_hex(BlockAllocator &balloc, const StringRef &s) { + auto iov = make_byte_ref(balloc, s.size() * 2 + 1); + auto p = iov.base; + + for (auto cc : s) { + uint8_t c = cc; + *p++ = LOWER_XDIGITS[c >> 4]; + *p++ = LOWER_XDIGITS[c & 0xf]; + } + + *p = '\0'; + + return StringRef{iov.base, p}; +} + +void to_token68(std::string &base64str) { + std::transform(std::begin(base64str), std::end(base64str), + std::begin(base64str), [](char c) { + switch (c) { + case '+': + return '-'; + case '/': + return '_'; + default: + return c; + } + }); + base64str.erase(std::find(std::begin(base64str), std::end(base64str), '='), + std::end(base64str)); +} + +StringRef to_base64(BlockAllocator &balloc, const StringRef &token68str) { + // At most 3 padding '=' + auto len = token68str.size() + 3; + auto iov = make_byte_ref(balloc, len + 1); + auto p = iov.base; + + p = std::transform(std::begin(token68str), std::end(token68str), p, + [](char c) { + switch (c) { + case '-': + return '+'; + case '_': + return '/'; + default: + return c; + } + }); + + auto rem = token68str.size() & 0x3; + if (rem) { + p = std::fill_n(p, 4 - rem, '='); + } + + *p = '\0'; + + return StringRef{iov.base, p}; +} + +namespace { +// Calculates Damerau–Levenshtein distance between c-string a and b +// with given costs. swapcost, subcost, addcost and delcost are cost +// to swap 2 adjacent characters, substitute characters, add character +// and delete character respectively. +int levenshtein(const char *a, int alen, const char *b, int blen, int swapcost, + int subcost, int addcost, int delcost) { + auto dp = std::vector<std::vector<int>>(3, std::vector<int>(blen + 1)); + for (int i = 0; i <= blen; ++i) { + dp[1][i] = i; + } + for (int i = 1; i <= alen; ++i) { + dp[0][0] = i; + for (int j = 1; j <= blen; ++j) { + dp[0][j] = dp[1][j - 1] + (a[i - 1] == b[j - 1] ? 0 : subcost); + if (i >= 2 && j >= 2 && a[i - 1] != b[j - 1] && a[i - 2] == b[j - 1] && + a[i - 1] == b[j - 2]) { + dp[0][j] = std::min(dp[0][j], dp[2][j - 2] + swapcost); + } + dp[0][j] = std::min(dp[0][j], + std::min(dp[1][j] + delcost, dp[0][j - 1] + addcost)); + } + std::rotate(std::begin(dp), std::begin(dp) + 2, std::end(dp)); + } + return dp[1][blen]; +} +} // namespace + +void show_candidates(const char *unkopt, const option *options) { + for (; *unkopt == '-'; ++unkopt) + ; + if (*unkopt == '\0') { + return; + } + auto unkoptend = unkopt; + for (; *unkoptend && *unkoptend != '='; ++unkoptend) + ; + auto unkoptlen = unkoptend - unkopt; + if (unkoptlen == 0) { + return; + } + int prefix_match = 0; + auto cands = std::vector<std::pair<int, const char *>>(); + for (size_t i = 0; options[i].name != nullptr; ++i) { + auto optnamelen = strlen(options[i].name); + // Use cost 0 for prefix match + if (istarts_with(options[i].name, options[i].name + optnamelen, unkopt, + unkopt + unkoptlen)) { + if (optnamelen == static_cast<size_t>(unkoptlen)) { + // Exact match, then we don't show any candidates. + return; + } + ++prefix_match; + cands.emplace_back(0, options[i].name); + continue; + } + // Use cost 0 for suffix match, but match at least 3 characters + if (unkoptlen >= 3 && + iends_with(options[i].name, options[i].name + optnamelen, unkopt, + unkopt + unkoptlen)) { + cands.emplace_back(0, options[i].name); + continue; + } + // cost values are borrowed from git, help.c. + int sim = + levenshtein(unkopt, unkoptlen, options[i].name, optnamelen, 0, 2, 1, 3); + cands.emplace_back(sim, options[i].name); + } + if (prefix_match == 1 || cands.empty()) { + return; + } + std::sort(std::begin(cands), std::end(cands)); + int threshold = cands[0].first; + // threshold value is a magic value. + if (threshold > 6) { + return; + } + std::cerr << "\nDid you mean:\n"; + for (auto &item : cands) { + if (item.first > threshold) { + break; + } + std::cerr << "\t--" << item.second << "\n"; + } +} + +bool has_uri_field(const http_parser_url &u, http_parser_url_fields field) { + return u.field_set & (1 << field); +} + +bool fieldeq(const char *uri1, const http_parser_url &u1, const char *uri2, + const http_parser_url &u2, http_parser_url_fields field) { + if (!has_uri_field(u1, field)) { + if (!has_uri_field(u2, field)) { + return true; + } else { + return false; + } + } else if (!has_uri_field(u2, field)) { + return false; + } + if (u1.field_data[field].len != u2.field_data[field].len) { + return false; + } + return memcmp(uri1 + u1.field_data[field].off, + uri2 + u2.field_data[field].off, u1.field_data[field].len) == 0; +} + +bool fieldeq(const char *uri, const http_parser_url &u, + http_parser_url_fields field, const char *t) { + return fieldeq(uri, u, field, StringRef{t}); +} + +bool fieldeq(const char *uri, const http_parser_url &u, + http_parser_url_fields field, const StringRef &t) { + if (!has_uri_field(u, field)) { + return t.empty(); + } + auto &f = u.field_data[field]; + return StringRef{uri + f.off, f.len} == t; +} + +StringRef get_uri_field(const char *uri, const http_parser_url &u, + http_parser_url_fields field) { + if (!util::has_uri_field(u, field)) { + return StringRef{}; + } + + return StringRef{uri + u.field_data[field].off, u.field_data[field].len}; +} + +uint16_t get_default_port(const char *uri, const http_parser_url &u) { + if (util::fieldeq(uri, u, UF_SCHEMA, "https")) { + return 443; + } else if (util::fieldeq(uri, u, UF_SCHEMA, "http")) { + return 80; + } else { + return 443; + } +} + +bool porteq(const char *uri1, const http_parser_url &u1, const char *uri2, + const http_parser_url &u2) { + uint16_t port1, port2; + port1 = + util::has_uri_field(u1, UF_PORT) ? u1.port : get_default_port(uri1, u1); + port2 = + util::has_uri_field(u2, UF_PORT) ? u2.port : get_default_port(uri2, u2); + return port1 == port2; +} + +void write_uri_field(std::ostream &o, const char *uri, const http_parser_url &u, + http_parser_url_fields field) { + if (util::has_uri_field(u, field)) { + o.write(uri + u.field_data[field].off, u.field_data[field].len); + } +} + +bool numeric_host(const char *hostname) { + return numeric_host(hostname, AF_INET) || numeric_host(hostname, AF_INET6); +} + +bool numeric_host(const char *hostname, int family) { + int rv; + std::array<uint8_t, sizeof(struct in6_addr)> dst; + + rv = nghttp2_inet_pton(family, hostname, dst.data()); + + return rv == 1; +} + +std::string numeric_name(const struct sockaddr *sa, socklen_t salen) { + std::array<char, NI_MAXHOST> host; + auto rv = getnameinfo(sa, salen, host.data(), host.size(), nullptr, 0, + NI_NUMERICHOST); + if (rv != 0) { + return "unknown"; + } + return host.data(); +} + +std::string to_numeric_addr(const Address *addr) { + return to_numeric_addr(&addr->su.sa, addr->len); +} + +std::string to_numeric_addr(const struct sockaddr *sa, socklen_t salen) { + auto family = sa->sa_family; +#ifndef _WIN32 + if (family == AF_UNIX) { + return reinterpret_cast<const sockaddr_un *>(sa)->sun_path; + } +#endif // !_WIN32 + + std::array<char, NI_MAXHOST> host; + std::array<char, NI_MAXSERV> serv; + auto rv = getnameinfo(sa, salen, host.data(), host.size(), serv.data(), + serv.size(), NI_NUMERICHOST | NI_NUMERICSERV); + if (rv != 0) { + return "unknown"; + } + + auto hostlen = strlen(host.data()); + auto servlen = strlen(serv.data()); + + std::string s; + char *p; + if (family == AF_INET6) { + s.resize(hostlen + servlen + 2 + 1); + p = &s[0]; + *p++ = '['; + p = std::copy_n(host.data(), hostlen, p); + *p++ = ']'; + } else { + s.resize(hostlen + servlen + 1); + p = &s[0]; + p = std::copy_n(host.data(), hostlen, p); + } + *p++ = ':'; + std::copy_n(serv.data(), servlen, p); + + return s; +} + +void set_port(Address &addr, uint16_t port) { + switch (addr.su.storage.ss_family) { + case AF_INET: + addr.su.in.sin_port = htons(port); + break; + case AF_INET6: + addr.su.in6.sin6_port = htons(port); + break; + } +} + +std::string ascii_dump(const uint8_t *data, size_t len) { + std::string res; + + for (size_t i = 0; i < len; ++i) { + auto c = data[i]; + + if (c >= 0x20 && c < 0x7f) { + res += c; + } else { + res += '.'; + } + } + + return res; +} + +char *get_exec_path(int argc, char **const argv, const char *cwd) { + if (argc == 0 || cwd == nullptr) { + return nullptr; + } + + auto argv0 = argv[0]; + auto len = strlen(argv0); + + char *path; + + if (argv0[0] == '/') { + path = static_cast<char *>(malloc(len + 1)); + if (path == nullptr) { + return nullptr; + } + memcpy(path, argv0, len + 1); + } else { + auto cwdlen = strlen(cwd); + path = static_cast<char *>(malloc(len + 1 + cwdlen + 1)); + if (path == nullptr) { + return nullptr; + } + memcpy(path, cwd, cwdlen); + path[cwdlen] = '/'; + memcpy(path + cwdlen + 1, argv0, len + 1); + } + + return path; +} + +bool check_path(const std::string &path) { + // We don't like '\' in path. + return !path.empty() && path[0] == '/' && + path.find('\\') == std::string::npos && + path.find("/../") == std::string::npos && + path.find("/./") == std::string::npos && + !util::ends_with_l(path, "/..") && !util::ends_with_l(path, "/."); +} + +int64_t to_time64(const timeval &tv) { + return tv.tv_sec * 1000000 + tv.tv_usec; +} + +bool check_h2_is_selected(const StringRef &proto) { + return streq(NGHTTP2_H2, proto) || streq(NGHTTP2_H2_16, proto) || + streq(NGHTTP2_H2_14, proto); +} + +namespace { +bool select_proto(const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, + const StringRef &key) { + for (auto p = in, end = in + inlen; p + key.size() <= end; p += *p + 1) { + if (std::equal(std::begin(key), std::end(key), p)) { + *out = p + 1; + *outlen = *p; + return true; + } + } + return false; +} +} // namespace + +bool select_h2(const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen) { + return select_proto(out, outlen, in, inlen, NGHTTP2_H2_ALPN) || + select_proto(out, outlen, in, inlen, NGHTTP2_H2_16_ALPN) || + select_proto(out, outlen, in, inlen, NGHTTP2_H2_14_ALPN); +} + +bool select_protocol(const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, + std::vector<std::string> proto_list) { + for (const auto &proto : proto_list) { + if (select_proto(out, outlen, in, inlen, StringRef{proto})) { + return true; + } + } + + return false; +} + +std::vector<unsigned char> get_default_alpn() { + auto res = std::vector<unsigned char>(NGHTTP2_H2_ALPN.size() + + NGHTTP2_H2_16_ALPN.size() + + NGHTTP2_H2_14_ALPN.size()); + auto p = std::begin(res); + + p = std::copy_n(std::begin(NGHTTP2_H2_ALPN), NGHTTP2_H2_ALPN.size(), p); + p = std::copy_n(std::begin(NGHTTP2_H2_16_ALPN), NGHTTP2_H2_16_ALPN.size(), p); + p = std::copy_n(std::begin(NGHTTP2_H2_14_ALPN), NGHTTP2_H2_14_ALPN.size(), p); + + return res; +} + +std::vector<StringRef> split_str(const StringRef &s, char delim) { + size_t len = 1; + auto last = std::end(s); + StringRef::const_iterator d; + for (auto first = std::begin(s); (d = std::find(first, last, delim)) != last; + ++len, first = d + 1) + ; + + auto list = std::vector<StringRef>(len); + + len = 0; + for (auto first = std::begin(s);; ++len) { + auto stop = std::find(first, last, delim); + list[len] = StringRef{first, stop}; + if (stop == last) { + break; + } + first = stop + 1; + } + return list; +} + +std::vector<StringRef> split_str(const StringRef &s, char delim, size_t n) { + if (n == 0) { + return split_str(s, delim); + } + + if (n == 1) { + return {s}; + } + + size_t len = 1; + auto last = std::end(s); + StringRef::const_iterator d; + for (auto first = std::begin(s); + len < n && (d = std::find(first, last, delim)) != last; + ++len, first = d + 1) + ; + + auto list = std::vector<StringRef>(len); + + len = 0; + for (auto first = std::begin(s);; ++len) { + if (len == n - 1) { + list[len] = StringRef{first, last}; + break; + } + + auto stop = std::find(first, last, delim); + list[len] = StringRef{first, stop}; + if (stop == last) { + break; + } + first = stop + 1; + } + return list; +} + +std::vector<std::string> parse_config_str_list(const StringRef &s, char delim) { + auto sublist = split_str(s, delim); + auto res = std::vector<std::string>(); + res.reserve(sublist.size()); + for (const auto &s : sublist) { + res.emplace_back(std::begin(s), std::end(s)); + } + return res; +} + +int make_socket_closeonexec(int fd) { +#ifdef _WIN32 + (void)fd; + return 0; +#else // !_WIN32 + int flags; + int rv; + while ((flags = fcntl(fd, F_GETFD)) == -1 && errno == EINTR) + ; + while ((rv = fcntl(fd, F_SETFD, flags | FD_CLOEXEC)) == -1 && errno == EINTR) + ; + return rv; +#endif // !_WIN32 +} + +int make_socket_nonblocking(int fd) { + int rv; + +#ifdef _WIN32 + u_long mode = 1; + + rv = ioctlsocket(fd, FIONBIO, &mode); +#else // !_WIN32 + int flags; + while ((flags = fcntl(fd, F_GETFL, 0)) == -1 && errno == EINTR) + ; + while ((rv = fcntl(fd, F_SETFL, flags | O_NONBLOCK)) == -1 && errno == EINTR) + ; +#endif // !_WIN32 + + return rv; +} + +int make_socket_nodelay(int fd) { + int val = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char *>(&val), + sizeof(val)) == -1) { + return -1; + } + return 0; +} + +int create_nonblock_socket(int family) { +#ifdef SOCK_NONBLOCK + auto fd = socket(family, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + + if (fd == -1) { + return -1; + } +#else // !SOCK_NONBLOCK + auto fd = socket(family, SOCK_STREAM, 0); + + if (fd == -1) { + return -1; + } + + make_socket_nonblocking(fd); + make_socket_closeonexec(fd); +#endif // !SOCK_NONBLOCK + + if (family == AF_INET || family == AF_INET6) { + make_socket_nodelay(fd); + } + + return fd; +} + +int create_nonblock_udp_socket(int family) { +#ifdef SOCK_NONBLOCK + auto fd = socket(family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + + if (fd == -1) { + return -1; + } +#else // !SOCK_NONBLOCK + auto fd = socket(family, SOCK_DGRAM, 0); + + if (fd == -1) { + return -1; + } + + make_socket_nonblocking(fd); + make_socket_closeonexec(fd); +#endif // !SOCK_NONBLOCK + + return fd; +} + +int bind_any_addr_udp(int fd, int family) { + addrinfo hints{}; + addrinfo *res, *rp; + int rv; + + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; + + rv = getaddrinfo(nullptr, "0", &hints, &res); + if (rv != 0) { + return -1; + } + + for (rp = res; rp; rp = rp->ai_next) { + if (bind(fd, rp->ai_addr, rp->ai_addrlen) != -1) { + break; + } + } + + freeaddrinfo(res); + + if (!rp) { + return -1; + } + + return 0; +} + +bool check_socket_connected(int fd) { + int error; + socklen_t len = sizeof(error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&error, &len) != 0) { + return false; + } + + return error == 0; +} + +int get_socket_error(int fd) { + int error; + socklen_t len = sizeof(error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&error, &len) != 0) { + return -1; + } + + return error; +} + +bool ipv6_numeric_addr(const char *host) { + uint8_t dst[16]; + return nghttp2_inet_pton(AF_INET6, host, dst) == 1; +} + +namespace { +std::pair<int64_t, size_t> parse_uint_digits(const void *ss, size_t len) { + const uint8_t *s = static_cast<const uint8_t *>(ss); + int64_t n = 0; + size_t i; + if (len == 0) { + return {-1, 0}; + } + constexpr int64_t max = std::numeric_limits<int64_t>::max(); + for (i = 0; i < len; ++i) { + if ('0' <= s[i] && s[i] <= '9') { + if (n > max / 10) { + return {-1, 0}; + } + n *= 10; + if (n > max - (s[i] - '0')) { + return {-1, 0}; + } + n += s[i] - '0'; + continue; + } + break; + } + if (i == 0) { + return {-1, 0}; + } + return {n, i}; +} +} // namespace + +int64_t parse_uint_with_unit(const char *s) { + return parse_uint_with_unit(reinterpret_cast<const uint8_t *>(s), strlen(s)); +} + +int64_t parse_uint_with_unit(const StringRef &s) { + return parse_uint_with_unit(s.byte(), s.size()); +} + +int64_t parse_uint_with_unit(const uint8_t *s, size_t len) { + int64_t n; + size_t i; + std::tie(n, i) = parse_uint_digits(s, len); + if (n == -1) { + return -1; + } + if (i == len) { + return n; + } + if (i + 1 != len) { + return -1; + } + int mul = 1; + switch (s[i]) { + case 'K': + case 'k': + mul = 1 << 10; + break; + case 'M': + case 'm': + mul = 1 << 20; + break; + case 'G': + case 'g': + mul = 1 << 30; + break; + default: + return -1; + } + constexpr int64_t max = std::numeric_limits<int64_t>::max(); + if (n > max / mul) { + return -1; + } + return n * mul; +} + +int64_t parse_uint(const char *s) { + return parse_uint(reinterpret_cast<const uint8_t *>(s), strlen(s)); +} + +int64_t parse_uint(const std::string &s) { + return parse_uint(reinterpret_cast<const uint8_t *>(s.c_str()), s.size()); +} + +int64_t parse_uint(const StringRef &s) { + return parse_uint(s.byte(), s.size()); +} + +int64_t parse_uint(const uint8_t *s, size_t len) { + int64_t n; + size_t i; + std::tie(n, i) = parse_uint_digits(s, len); + if (n == -1 || i != len) { + return -1; + } + return n; +} + +double parse_duration_with_unit(const char *s) { + return parse_duration_with_unit(reinterpret_cast<const uint8_t *>(s), + strlen(s)); +} + +double parse_duration_with_unit(const StringRef &s) { + return parse_duration_with_unit(s.byte(), s.size()); +} + +double parse_duration_with_unit(const uint8_t *s, size_t len) { + constexpr auto max = std::numeric_limits<int64_t>::max(); + int64_t n; + size_t i; + + std::tie(n, i) = parse_uint_digits(s, len); + if (n == -1) { + goto fail; + } + if (i == len) { + return static_cast<double>(n); + } + switch (s[i]) { + case 'S': + case 's': + // seconds + if (i + 1 != len) { + goto fail; + } + return static_cast<double>(n); + case 'M': + case 'm': + if (i + 1 == len) { + // minutes + if (n > max / 60) { + goto fail; + } + return static_cast<double>(n) * 60; + } + + if (i + 2 != len || (s[i + 1] != 's' && s[i + 1] != 'S')) { + goto fail; + } + // milliseconds + return static_cast<double>(n) / 1000.; + case 'H': + case 'h': + // hours + if (i + 1 != len) { + goto fail; + } + if (n > max / 3600) { + goto fail; + } + return static_cast<double>(n) * 3600; + } +fail: + return std::numeric_limits<double>::infinity(); +} + +std::string duration_str(double t) { + if (t == 0.) { + return "0"; + } + auto frac = static_cast<int64_t>(t * 1000) % 1000; + if (frac > 0) { + return utos(static_cast<int64_t>(t * 1000)) + "ms"; + } + auto v = static_cast<int64_t>(t); + if (v % 60) { + return utos(v) + "s"; + } + v /= 60; + if (v % 60) { + return utos(v) + "m"; + } + v /= 60; + return utos(v) + "h"; +} + +std::string format_duration(const std::chrono::microseconds &u) { + const char *unit = "us"; + int d = 0; + auto t = u.count(); + if (t >= 1000000) { + d = 1000000; + unit = "s"; + } else if (t >= 1000) { + d = 1000; + unit = "ms"; + } else { + return utos(t) + unit; + } + return dtos(static_cast<double>(t) / d) + unit; +} + +std::string format_duration(double t) { + const char *unit = "us"; + if (t >= 1.) { + unit = "s"; + } else if (t >= 0.001) { + t *= 1000.; + unit = "ms"; + } else { + t *= 1000000.; + return utos(static_cast<int64_t>(t)) + unit; + } + return dtos(t) + unit; +} + +std::string dtos(double n) { + auto m = llround(100. * n); + auto f = utos(m % 100); + return utos(m / 100) + "." + (f.size() == 1 ? "0" : "") + f; +} + +StringRef make_http_hostport(BlockAllocator &balloc, const StringRef &host, + uint16_t port) { + auto iov = make_byte_ref(balloc, host.size() + 2 + 1 + 5 + 1); + return make_http_hostport(iov.base, host, port); +} + +StringRef make_hostport(BlockAllocator &balloc, const StringRef &host, + uint16_t port) { + auto iov = make_byte_ref(balloc, host.size() + 2 + 1 + 5 + 1); + return make_hostport(iov.base, host, port); +} + +namespace { +void hexdump8(FILE *out, const uint8_t *first, const uint8_t *last) { + auto stop = std::min(first + 8, last); + for (auto k = first; k != stop; ++k) { + fprintf(out, "%02x ", *k); + } + // each byte needs 3 spaces (2 hex value and space) + for (; stop != first + 8; ++stop) { + fputs(" ", out); + } + // we have extra space after 8 bytes + fputc(' ', out); +} +} // namespace + +void hexdump(FILE *out, const uint8_t *src, size_t len) { + if (len == 0) { + return; + } + size_t buflen = 0; + auto repeated = false; + std::array<uint8_t, 16> buf{}; + auto end = src + len; + auto i = src; + for (;;) { + auto nextlen = + std::min(static_cast<size_t>(16), static_cast<size_t>(end - i)); + if (nextlen == buflen && + std::equal(std::begin(buf), std::begin(buf) + buflen, i)) { + // as long as adjacent 16 bytes block are the same, we just + // print single '*'. + if (!repeated) { + repeated = true; + fputs("*\n", out); + } + i += nextlen; + continue; + } + repeated = false; + fprintf(out, "%08lx", static_cast<unsigned long>(i - src)); + if (i == end) { + fputc('\n', out); + break; + } + fputs(" ", out); + hexdump8(out, i, end); + hexdump8(out, i + 8, std::max(i + 8, end)); + fputc('|', out); + auto stop = std::min(i + 16, end); + buflen = stop - i; + auto p = buf.data(); + for (; i != stop; ++i) { + *p++ = *i; + if (0x20 <= *i && *i <= 0x7e) { + fputc(*i, out); + } else { + fputc('.', out); + } + } + fputs("|\n", out); + } +} + +void put_uint16be(uint8_t *buf, uint16_t n) { + uint16_t x = htons(n); + memcpy(buf, &x, sizeof(uint16_t)); +} + +void put_uint32be(uint8_t *buf, uint32_t n) { + uint32_t x = htonl(n); + memcpy(buf, &x, sizeof(uint32_t)); +} + +uint16_t get_uint16(const uint8_t *data) { + uint16_t n; + memcpy(&n, data, sizeof(uint16_t)); + return ntohs(n); +} + +uint32_t get_uint32(const uint8_t *data) { + uint32_t n; + memcpy(&n, data, sizeof(uint32_t)); + return ntohl(n); +} + +uint64_t get_uint64(const uint8_t *data) { + uint64_t n = 0; + n += static_cast<uint64_t>(data[0]) << 56; + n += static_cast<uint64_t>(data[1]) << 48; + n += static_cast<uint64_t>(data[2]) << 40; + n += static_cast<uint64_t>(data[3]) << 32; + n += static_cast<uint64_t>(data[4]) << 24; + n += data[5] << 16; + n += data[6] << 8; + n += data[7]; + return n; +} + +int read_mime_types(std::map<std::string, std::string> &res, + const char *filename) { + std::ifstream infile(filename); + if (!infile) { + return -1; + } + + auto delim_pred = [](char c) { return c == ' ' || c == '\t'; }; + + std::string line; + while (std::getline(infile, line)) { + if (line.empty() || line[0] == '#') { + continue; + } + + auto type_end = std::find_if(std::begin(line), std::end(line), delim_pred); + if (type_end == std::begin(line)) { + continue; + } + + auto ext_end = type_end; + for (;;) { + auto ext_start = std::find_if_not(ext_end, std::end(line), delim_pred); + if (ext_start == std::end(line)) { + break; + } + ext_end = std::find_if(ext_start, std::end(line), delim_pred); +#ifdef HAVE_STD_MAP_EMPLACE + res.emplace(std::string(ext_start, ext_end), + std::string(std::begin(line), type_end)); +#else // !HAVE_STD_MAP_EMPLACE + res.insert(std::make_pair(std::string(ext_start, ext_end), + std::string(std::begin(line), type_end))); +#endif // !HAVE_STD_MAP_EMPLACE + } + } + + return 0; +} + +StringRef percent_decode(BlockAllocator &balloc, const StringRef &src) { + auto iov = make_byte_ref(balloc, src.size() * 3 + 1); + auto p = iov.base; + for (auto first = std::begin(src); first != std::end(src); ++first) { + if (*first != '%') { + *p++ = *first; + continue; + } + + if (first + 1 != std::end(src) && first + 2 != std::end(src) && + is_hex_digit(*(first + 1)) && is_hex_digit(*(first + 2))) { + *p++ = (hex_to_uint(*(first + 1)) << 4) + hex_to_uint(*(first + 2)); + first += 2; + continue; + } + + *p++ = *first; + } + *p = '\0'; + return StringRef{iov.base, p}; +} + +// Returns x**y +double int_pow(double x, size_t y) { + auto res = 1.; + for (; y; --y) { + res *= x; + } + return res; +} + +uint32_t hash32(const StringRef &s) { + /* 32 bit FNV-1a: http://isthe.com/chongo/tech/comp/fnv/ */ + uint32_t h = 2166136261u; + size_t i; + + for (i = 0; i < s.size(); ++i) { + h ^= s[i]; + h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); + } + + return h; +} + +namespace { +int message_digest(uint8_t *res, const EVP_MD *meth, const StringRef &s) { + int rv; + + auto ctx = EVP_MD_CTX_new(); + if (ctx == nullptr) { + return -1; + } + + auto ctx_deleter = defer(EVP_MD_CTX_free, ctx); + + rv = EVP_DigestInit_ex(ctx, meth, nullptr); + if (rv != 1) { + return -1; + } + + rv = EVP_DigestUpdate(ctx, s.c_str(), s.size()); + if (rv != 1) { + return -1; + } + + unsigned int mdlen = EVP_MD_size(meth); + + rv = EVP_DigestFinal_ex(ctx, res, &mdlen); + if (rv != 1) { + return -1; + } + + return 0; +} +} // namespace + +int sha256(uint8_t *res, const StringRef &s) { + return message_digest(res, EVP_sha256(), s); +} + +int sha1(uint8_t *res, const StringRef &s) { + return message_digest(res, EVP_sha1(), s); +} + +bool is_hex_string(const StringRef &s) { + if (s.size() % 2) { + return false; + } + + for (auto c : s) { + if (!is_hex_digit(c)) { + return false; + } + } + + return true; +} + +StringRef decode_hex(BlockAllocator &balloc, const StringRef &s) { + auto iov = make_byte_ref(balloc, s.size() + 1); + auto p = decode_hex(iov.base, s); + *p = '\0'; + return StringRef{iov.base, p}; +} + +StringRef extract_host(const StringRef &hostport) { + if (hostport[0] == '[') { + // assume this is IPv6 numeric address + auto p = std::find(std::begin(hostport), std::end(hostport), ']'); + if (p == std::end(hostport)) { + return StringRef{}; + } + if (p + 1 < std::end(hostport) && *(p + 1) != ':') { + return StringRef{}; + } + return StringRef{std::begin(hostport), p + 1}; + } + + auto p = std::find(std::begin(hostport), std::end(hostport), ':'); + if (p == std::begin(hostport)) { + return StringRef{}; + } + return StringRef{std::begin(hostport), p}; +} + +std::pair<StringRef, StringRef> split_hostport(const StringRef &hostport) { + if (hostport.empty()) { + return {}; + } + if (hostport[0] == '[') { + // assume this is IPv6 numeric address + auto p = std::find(std::begin(hostport), std::end(hostport), ']'); + if (p == std::end(hostport)) { + return {}; + } + if (p + 1 == std::end(hostport)) { + return {StringRef{std::begin(hostport) + 1, p}, {}}; + } + if (*(p + 1) != ':' || p + 2 == std::end(hostport)) { + return {}; + } + return {StringRef{std::begin(hostport) + 1, p}, + StringRef{p + 2, std::end(hostport)}}; + } + + auto p = std::find(std::begin(hostport), std::end(hostport), ':'); + if (p == std::begin(hostport)) { + return {}; + } + if (p == std::end(hostport)) { + return {StringRef{std::begin(hostport), p}, {}}; + } + if (p + 1 == std::end(hostport)) { + return {}; + } + + return {StringRef{std::begin(hostport), p}, + StringRef{p + 1, std::end(hostport)}}; +} + +std::mt19937 make_mt19937() { + std::random_device rd; + return std::mt19937(rd()); +} + +int daemonize(int nochdir, int noclose) { +#ifdef __APPLE__ + pid_t pid; + pid = fork(); + if (pid == -1) { + return -1; + } else if (pid > 0) { + _exit(EXIT_SUCCESS); + } + if (setsid() == -1) { + return -1; + } + pid = fork(); + if (pid == -1) { + return -1; + } else if (pid > 0) { + _exit(EXIT_SUCCESS); + } + if (nochdir == 0) { + if (chdir("/") == -1) { + return -1; + } + } + if (noclose == 0) { + if (freopen("/dev/null", "r", stdin) == nullptr) { + return -1; + } + if (freopen("/dev/null", "w", stdout) == nullptr) { + return -1; + } + if (freopen("/dev/null", "w", stderr) == nullptr) { + return -1; + } + } + return 0; +#else // !__APPLE__ + return daemon(nochdir, noclose); +#endif // !__APPLE__ +} + +StringRef rstrip(BlockAllocator &balloc, const StringRef &s) { + auto it = std::rbegin(s); + for (; it != std::rend(s) && (*it == ' ' || *it == '\t'); ++it) + ; + + auto len = it - std::rbegin(s); + if (len == 0) { + return s; + } + + return make_string_ref(balloc, StringRef{s.c_str(), s.size() - len}); +} + +#ifdef ENABLE_HTTP3 +int msghdr_get_local_addr(Address &dest, msghdr *msg, int family) { + switch (family) { + case AF_INET: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) { + in_pktinfo pktinfo; + memcpy(&pktinfo, CMSG_DATA(cmsg), sizeof(pktinfo)); + dest.len = sizeof(dest.su.in); + auto &sa = dest.su.in; + sa.sin_family = AF_INET; + sa.sin_addr = pktinfo.ipi_addr; + + return 0; + } + } + + return -1; + case AF_INET6: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_PKTINFO) { + in6_pktinfo pktinfo; + memcpy(&pktinfo, CMSG_DATA(cmsg), sizeof(pktinfo)); + dest.len = sizeof(dest.su.in6); + auto &sa = dest.su.in6; + sa.sin6_family = AF_INET6; + sa.sin6_addr = pktinfo.ipi6_addr; + return 0; + } + } + + return -1; + } + + return -1; +} + +uint8_t msghdr_get_ecn(msghdr *msg, int family) { + switch (family) { + case AF_INET: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IP && +# ifdef __APPLE__ + cmsg->cmsg_type == IP_RECVTOS +# else // !__APPLE__ + cmsg->cmsg_type == IP_TOS +# endif // !__APPLE__ + && cmsg->cmsg_len) { + return *reinterpret_cast<uint8_t *>(CMSG_DATA(cmsg)) & IPTOS_ECN_MASK; + } + } + + return 0; + case AF_INET6: + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_TCLASS && + cmsg->cmsg_len) { + unsigned int tos; + + memcpy(&tos, CMSG_DATA(cmsg), sizeof(tos)); + + return tos & IPTOS_ECN_MASK; + } + } + + return 0; + } + + return 0; +} + +size_t msghdr_get_udp_gro(msghdr *msg) { + uint16_t gso_size = 0; + +# ifdef UDP_GRO + for (auto cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level == SOL_UDP && cmsg->cmsg_type == UDP_GRO) { + memcpy(&gso_size, CMSG_DATA(cmsg), sizeof(gso_size)); + + break; + } + } +# endif // UDP_GRO + + return gso_size; +} +#endif // ENABLE_HTTP3 + +} // namespace util + +} // namespace nghttp2 diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..d818bf2 --- /dev/null +++ b/src/util.h @@ -0,0 +1,971 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2012 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef UTIL_H +#define UTIL_H + +#include "nghttp2_config.h" + +#ifdef HAVE_UNISTD_H +# include <unistd.h> +#endif // HAVE_UNISTD_H +#include <getopt.h> +#ifdef HAVE_NETDB_H +# include <netdb.h> +#endif // HAVE_NETDB_H + +#include <cmath> +#include <cstring> +#include <cassert> +#include <vector> +#include <string> +#include <algorithm> +#include <sstream> +#include <memory> +#include <chrono> +#include <map> +#include <random> + +#ifdef HAVE_LIBEV +# include <ev.h> +#endif // HAVE_LIBEV + +#include "url-parser/url_parser.h" + +#include "template.h" +#include "network.h" +#include "allocator.h" + +namespace nghttp2 { + +constexpr auto NGHTTP2_H2_ALPN = StringRef::from_lit("\x2h2"); +constexpr auto NGHTTP2_H2 = StringRef::from_lit("h2"); + +// The additional HTTP/2 protocol ALPN protocol identifier we also +// supports for our applications to make smooth migration into final +// h2 ALPN ID. +constexpr auto NGHTTP2_H2_16_ALPN = StringRef::from_lit("\x5h2-16"); +constexpr auto NGHTTP2_H2_16 = StringRef::from_lit("h2-16"); + +constexpr auto NGHTTP2_H2_14_ALPN = StringRef::from_lit("\x5h2-14"); +constexpr auto NGHTTP2_H2_14 = StringRef::from_lit("h2-14"); + +constexpr auto NGHTTP2_H1_1_ALPN = StringRef::from_lit("\x8http/1.1"); +constexpr auto NGHTTP2_H1_1 = StringRef::from_lit("http/1.1"); + +constexpr size_t NGHTTP2_MAX_UINT64_DIGITS = str_size("18446744073709551615"); + +namespace util { + +extern const char UPPER_XDIGITS[]; + +inline bool is_alpha(const char c) { + return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'); +} + +inline bool is_digit(const char c) { return '0' <= c && c <= '9'; } + +inline bool is_hex_digit(const char c) { + return is_digit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); +} + +// Returns true if |s| is hex string. +bool is_hex_string(const StringRef &s); + +bool in_rfc3986_unreserved_chars(const char c); + +bool in_rfc3986_sub_delims(const char c); + +// Returns true if |c| is in token (HTTP-p1, Section 3.2.6) +bool in_token(char c); + +bool in_attr_char(char c); + +// Returns integer corresponding to hex notation |c|. If +// is_hex_digit(c) is false, it returns 256. +uint32_t hex_to_uint(char c); + +std::string percent_encode(const unsigned char *target, size_t len); + +std::string percent_encode(const std::string &target); + +template <typename InputIt> +std::string percent_decode(InputIt first, InputIt last) { + std::string result; + result.resize(last - first); + auto p = std::begin(result); + for (; first != last; ++first) { + if (*first != '%') { + *p++ = *first; + continue; + } + + if (first + 1 != last && first + 2 != last && is_hex_digit(*(first + 1)) && + is_hex_digit(*(first + 2))) { + *p++ = (hex_to_uint(*(first + 1)) << 4) + hex_to_uint(*(first + 2)); + first += 2; + continue; + } + + *p++ = *first; + } + result.resize(p - std::begin(result)); + return result; +} + +StringRef percent_decode(BlockAllocator &balloc, const StringRef &src); + +// Percent encode |target| if character is not in token or '%'. +StringRef percent_encode_token(BlockAllocator &balloc, const StringRef &target); + +template <typename OutputIt> +OutputIt percent_encode_token(OutputIt it, const StringRef &target) { + for (auto first = std::begin(target); first != std::end(target); ++first) { + uint8_t c = *first; + + if (c != '%' && in_token(c)) { + *it++ = c; + continue; + } + + *it++ = '%'; + *it++ = UPPER_XDIGITS[c >> 4]; + *it++ = UPPER_XDIGITS[(c & 0x0f)]; + } + + return it; +} + +// Returns the number of bytes written by percent_encode_token with +// the same |target| parameter. The return value does not include a +// terminal NUL byte. +size_t percent_encode_tokenlen(const StringRef &target); + +// Returns quotedString version of |target|. Currently, this function +// just replace '"' with '\"'. +StringRef quote_string(BlockAllocator &balloc, const StringRef &target); + +template <typename OutputIt> +OutputIt quote_string(OutputIt it, const StringRef &target) { + for (auto c : target) { + if (c == '"') { + *it++ = '\\'; + *it++ = '"'; + } else { + *it++ = c; + } + } + + return it; +} + +// Returns the number of bytes written by quote_string with the same +// |target| parameter. The return value does not include a terminal +// NUL byte. +size_t quote_stringlen(const StringRef &target); + +std::string format_hex(const unsigned char *s, size_t len); + +template <size_t N> std::string format_hex(const unsigned char (&s)[N]) { + return format_hex(s, N); +} + +template <size_t N> std::string format_hex(const std::array<uint8_t, N> &s) { + return format_hex(s.data(), s.size()); +} + +StringRef format_hex(BlockAllocator &balloc, const StringRef &s); + +static constexpr char LOWER_XDIGITS[] = "0123456789abcdef"; + +template <typename OutputIt> +OutputIt format_hex(OutputIt it, const StringRef &s) { + for (auto cc : s) { + uint8_t c = cc; + *it++ = LOWER_XDIGITS[c >> 4]; + *it++ = LOWER_XDIGITS[c & 0xf]; + } + + return it; +} + +// decode_hex decodes hex string |s|, returns the decoded byte string. +// This function assumes |s| is hex string, that is is_hex_string(s) +// == true. +StringRef decode_hex(BlockAllocator &balloc, const StringRef &s); + +template <typename OutputIt> +OutputIt decode_hex(OutputIt d_first, const StringRef &s) { + for (auto it = std::begin(s); it != std::end(s); it += 2) { + *d_first++ = (hex_to_uint(*it) << 4) | hex_to_uint(*(it + 1)); + } + + return d_first; +} + +// Returns given time |t| from epoch in HTTP Date format (e.g., Mon, +// 10 Oct 2016 10:25:58 GMT). +std::string http_date(time_t t); +// Writes given time |t| from epoch in HTTP Date format into the +// buffer pointed by |res|. The buffer must be at least 29 bytes +// long. This function returns the one beyond the last position. +char *http_date(char *res, time_t t); + +// Returns given time |t| from epoch in Common Log format (e.g., +// 03/Jul/2014:00:19:38 +0900) +std::string common_log_date(time_t t); +// Writes given time |t| from epoch in Common Log format into the +// buffer pointed by |res|. The buffer must be at least 26 bytes +// long. This function returns the one beyond the last position. +char *common_log_date(char *res, time_t t); + +// Returns given millisecond |ms| from epoch in ISO 8601 format (e.g., +// 2014-11-15T12:58:24.741Z or 2014-11-15T12:58:24.741+09:00) +std::string iso8601_date(int64_t ms); +// Writes given time |t| from epoch in ISO 8601 format into the buffer +// pointed by |res|. The buffer must be at least 29 bytes long. This +// function returns the one beyond the last position. +char *iso8601_date(char *res, int64_t ms); + +// Writes given time |t| from epoch in ISO 8601 basic format into the +// buffer pointed by |res|. The buffer must be at least 24 bytes +// long. This function returns the one beyond the last position. +char *iso8601_basic_date(char *res, int64_t ms); + +time_t parse_http_date(const StringRef &s); + +// Parses time formatted as "MMM DD HH:MM:SS YYYY [GMT]" (e.g., Feb 3 +// 00:55:52 2015 GMT), which is specifically used by OpenSSL +// ASN1_TIME_print(). +time_t parse_openssl_asn1_time_print(const StringRef &s); + +char upcase(char c); + +inline char lowcase(char c) { + constexpr static unsigned char tbl[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z', 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, + 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, + 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, + 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, + 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, + 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, + 255, + }; + return tbl[static_cast<unsigned char>(c)]; +} + +template <typename InputIterator1, typename InputIterator2> +bool starts_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { + if (last1 - first1 < last2 - first2) { + return false; + } + return std::equal(first2, last2, first1); +} + +template <typename S, typename T> bool starts_with(const S &a, const T &b) { + return starts_with(a.begin(), a.end(), b.begin(), b.end()); +} + +struct CaseCmp { + bool operator()(char lhs, char rhs) const { + return lowcase(lhs) == lowcase(rhs); + } +}; + +template <typename InputIterator1, typename InputIterator2> +bool istarts_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { + if (last1 - first1 < last2 - first2) { + return false; + } + return std::equal(first2, last2, first1, CaseCmp()); +} + +template <typename S, typename T> bool istarts_with(const S &a, const T &b) { + return istarts_with(a.begin(), a.end(), b.begin(), b.end()); +} + +template <typename T, typename CharT, size_t N> +bool istarts_with_l(const T &a, const CharT (&b)[N]) { + return istarts_with(a.begin(), a.end(), b, b + N - 1); +} + +template <typename InputIterator1, typename InputIterator2> +bool ends_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { + if (last1 - first1 < last2 - first2) { + return false; + } + return std::equal(first2, last2, last1 - (last2 - first2)); +} + +template <typename T, typename S> bool ends_with(const T &a, const S &b) { + return ends_with(a.begin(), a.end(), b.begin(), b.end()); +} + +template <typename T, typename CharT, size_t N> +bool ends_with_l(const T &a, const CharT (&b)[N]) { + return ends_with(a.begin(), a.end(), b, b + N - 1); +} + +template <typename InputIterator1, typename InputIterator2> +bool iends_with(InputIterator1 first1, InputIterator1 last1, + InputIterator2 first2, InputIterator2 last2) { + if (last1 - first1 < last2 - first2) { + return false; + } + return std::equal(first2, last2, last1 - (last2 - first2), CaseCmp()); +} + +template <typename T, typename S> bool iends_with(const T &a, const S &b) { + return iends_with(a.begin(), a.end(), b.begin(), b.end()); +} + +template <typename T, typename CharT, size_t N> +bool iends_with_l(const T &a, const CharT (&b)[N]) { + return iends_with(a.begin(), a.end(), b, b + N - 1); +} + +template <typename InputIt1, typename InputIt2> +bool strieq(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2) { + if (std::distance(first1, last1) != std::distance(first2, last2)) { + return false; + } + + return std::equal(first1, last1, first2, CaseCmp()); +} + +template <typename T, typename S> bool strieq(const T &a, const S &b) { + return strieq(a.begin(), a.end(), b.begin(), b.end()); +} + +template <typename CharT, typename InputIt, size_t N> +bool strieq_l(const CharT (&a)[N], InputIt b, size_t blen) { + return strieq(a, a + (N - 1), b, b + blen); +} + +template <typename CharT, size_t N, typename T> +bool strieq_l(const CharT (&a)[N], const T &b) { + return strieq(a, a + (N - 1), b.begin(), b.end()); +} + +template <typename InputIt1, typename InputIt2> +bool streq(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2) { + if (std::distance(first1, last1) != std::distance(first2, last2)) { + return false; + } + return std::equal(first1, last1, first2); +} + +template <typename T, typename S> bool streq(const T &a, const S &b) { + return streq(a.begin(), a.end(), b.begin(), b.end()); +} + +template <typename CharT, typename InputIt, size_t N> +bool streq_l(const CharT (&a)[N], InputIt b, size_t blen) { + return streq(a, a + (N - 1), b, b + blen); +} + +template <typename CharT, size_t N, typename T> +bool streq_l(const CharT (&a)[N], const T &b) { + return streq(a, a + (N - 1), b.begin(), b.end()); +} + +// Returns true if |a| contains |b|. If both |a| and |b| are empty, +// this function returns false. +template <typename S, typename T> bool strifind(const S &a, const T &b) { + return std::search(a.begin(), a.end(), b.begin(), b.end(), CaseCmp()) != + a.end(); +} + +template <typename InputIt> void inp_strlower(InputIt first, InputIt last) { + std::transform(first, last, first, lowcase); +} + +// Lowercase |s| in place. +inline void inp_strlower(std::string &s) { + inp_strlower(std::begin(s), std::end(s)); +} + +// Returns string representation of |n| with 2 fractional digits. +std::string dtos(double n); + +template <typename T> std::string utos(T n) { + std::string res; + if (n == 0) { + res = "0"; + return res; + } + size_t nlen = 0; + for (auto t = n; t; t /= 10, ++nlen) + ; + res.resize(nlen); + for (; n; n /= 10) { + res[--nlen] = (n % 10) + '0'; + } + return res; +} + +template <typename T, typename OutputIt> OutputIt utos(OutputIt dst, T n) { + if (n == 0) { + *dst++ = '0'; + return dst; + } + size_t nlen = 0; + for (auto t = n; t; t /= 10, ++nlen) + ; + auto p = dst + nlen; + auto res = p; + for (; n; n /= 10) { + *--p = (n % 10) + '0'; + } + return res; +} + +template <typename T> +StringRef make_string_ref_uint(BlockAllocator &balloc, T n) { + auto iov = make_byte_ref(balloc, NGHTTP2_MAX_UINT64_DIGITS + 1); + auto p = iov.base; + p = util::utos(p, n); + *p = '\0'; + return StringRef{iov.base, p}; +} + +template <typename T> std::string utos_unit(T n) { + char u = 0; + if (n >= (1 << 30)) { + u = 'G'; + n /= (1 << 30); + } else if (n >= (1 << 20)) { + u = 'M'; + n /= (1 << 20); + } else if (n >= (1 << 10)) { + u = 'K'; + n /= (1 << 10); + } + if (u == 0) { + return utos(n); + } + return utos(n) + u; +} + +// Like utos_unit(), but 2 digits fraction part is followed. +template <typename T> std::string utos_funit(T n) { + char u = 0; + int b = 0; + if (n >= (1 << 30)) { + u = 'G'; + b = 30; + } else if (n >= (1 << 20)) { + u = 'M'; + b = 20; + } else if (n >= (1 << 10)) { + u = 'K'; + b = 10; + } + if (b == 0) { + return utos(n); + } + return dtos(static_cast<double>(n) / (1 << b)) + u; +} + +template <typename T> std::string utox(T n) { + std::string res; + if (n == 0) { + res = "0"; + return res; + } + int i = 0; + T t = n; + for (; t; t /= 16, ++i) + ; + res.resize(i); + --i; + for (; n; --i, n /= 16) { + res[i] = UPPER_XDIGITS[(n & 0x0f)]; + } + return res; +} + +void to_token68(std::string &base64str); + +StringRef to_base64(BlockAllocator &balloc, const StringRef &token68str); + +void show_candidates(const char *unkopt, const option *options); + +bool has_uri_field(const http_parser_url &u, http_parser_url_fields field); + +bool fieldeq(const char *uri1, const http_parser_url &u1, const char *uri2, + const http_parser_url &u2, http_parser_url_fields field); + +bool fieldeq(const char *uri, const http_parser_url &u, + http_parser_url_fields field, const char *t); + +bool fieldeq(const char *uri, const http_parser_url &u, + http_parser_url_fields field, const StringRef &t); + +StringRef get_uri_field(const char *uri, const http_parser_url &u, + http_parser_url_fields field); + +uint16_t get_default_port(const char *uri, const http_parser_url &u); + +bool porteq(const char *uri1, const http_parser_url &u1, const char *uri2, + const http_parser_url &u2); + +void write_uri_field(std::ostream &o, const char *uri, const http_parser_url &u, + http_parser_url_fields field); + +bool numeric_host(const char *hostname); + +bool numeric_host(const char *hostname, int family); + +// Returns numeric address string of |addr|. If getnameinfo() is +// failed, "unknown" is returned. +std::string numeric_name(const struct sockaddr *sa, socklen_t salen); + +// Returns string representation of numeric address and port of +// |addr|. If address family is AF_UNIX, this return path to UNIX +// domain socket. Otherwise, the format is like <HOST>:<PORT>. For +// IPv6 address, address is enclosed by square brackets ([]). +std::string to_numeric_addr(const Address *addr); + +std::string to_numeric_addr(const struct sockaddr *sa, socklen_t salen); + +// Sets |port| to |addr|. +void set_port(Address &addr, uint16_t port); + +// Returns ASCII dump of |data| of length |len|. Only ASCII printable +// characters are preserved. Other characters are replaced with ".". +std::string ascii_dump(const uint8_t *data, size_t len); + +// Returns absolute path of executable path. If argc == 0 or |cwd| is +// nullptr, this function returns nullptr. If argv[0] starts with +// '/', this function returns argv[0]. Otherwise return cwd + "/" + +// argv[0]. If non-null is returned, it is NULL-terminated string and +// dynamically allocated by malloc. The caller is responsible to free +// it. +char *get_exec_path(int argc, char **const argv, const char *cwd); + +// Validates path so that it does not contain directory traversal +// vector. Returns true if path is safe. The |path| must start with +// "/" otherwise returns false. This function should be called after +// percent-decode was performed. +bool check_path(const std::string &path); + +// Returns the |tv| value as 64 bit integer using a microsecond as an +// unit. +int64_t to_time64(const timeval &tv); + +// Returns true if ALPN ID |proto| is supported HTTP/2 protocol +// identifier. +bool check_h2_is_selected(const StringRef &proto); + +// Selects h2 protocol ALPN ID if one of supported h2 versions are +// present in |in| of length inlen. Returns true if h2 version is +// selected. +bool select_h2(const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen); + +// Selects protocol ALPN ID if one of identifiers contained in |protolist| is +// present in |in| of length inlen. Returns true if identifier is +// selected. +bool select_protocol(const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, + std::vector<std::string> proto_list); + +// Returns default ALPN protocol list, which only contains supported +// HTTP/2 protocol identifier. +std::vector<unsigned char> get_default_alpn(); + +// Parses delimited strings in |s| and returns the array of substring, +// delimited by |delim|. The any white spaces around substring are +// treated as a part of substring. +std::vector<std::string> parse_config_str_list(const StringRef &s, + char delim = ','); + +// Parses delimited strings in |s| and returns Substrings in |s| +// delimited by |delim|. The any white spaces around substring are +// treated as a part of substring. +std::vector<StringRef> split_str(const StringRef &s, char delim); + +// Behaves like split_str, but this variant splits at most |n| - 1 +// times and returns at most |n| sub-strings. If |n| is zero, it +// falls back to split_str. +std::vector<StringRef> split_str(const StringRef &s, char delim, size_t n); + +// Writes given time |tp| in Common Log format (e.g., +// 03/Jul/2014:00:19:38 +0900) in buffer pointed by |out|. The buffer +// must be at least 27 bytes, including terminal NULL byte. Expected +// type of |tp| is std::chrono::time_point. This function returns +// StringRef wrapping the buffer pointed by |out|, and this string is +// terminated by NULL. +template <typename T> StringRef format_common_log(char *out, const T &tp) { + auto t = + std::chrono::duration_cast<std::chrono::seconds>(tp.time_since_epoch()); + auto p = common_log_date(out, t.count()); + *p = '\0'; + return StringRef{out, p}; +} + +// Returns given time |tp| in ISO 8601 format (e.g., +// 2014-11-15T12:58:24.741Z or 2014-11-15T12:58:24.741+09:00). +// Expected type of |tp| is std::chrono::time_point +template <typename T> std::string format_iso8601(const T &tp) { + auto t = std::chrono::duration_cast<std::chrono::milliseconds>( + tp.time_since_epoch()); + return iso8601_date(t.count()); +} + +// Writes given time |tp| in ISO 8601 format (e.g., +// 2014-11-15T12:58:24.741Z or 2014-11-15T12:58:24.741+09:00) in +// buffer pointed by |out|. The buffer must be at least 30 bytes, +// including terminal NULL byte. Expected type of |tp| is +// std::chrono::time_point. This function returns StringRef wrapping +// the buffer pointed by |out|, and this string is terminated by NULL. +template <typename T> StringRef format_iso8601(char *out, const T &tp) { + auto t = std::chrono::duration_cast<std::chrono::milliseconds>( + tp.time_since_epoch()); + auto p = iso8601_date(out, t.count()); + *p = '\0'; + return StringRef{out, p}; +} + +// Writes given time |tp| in ISO 8601 basic format (e.g., +// 20141115T125824.741Z or 20141115T125824.741+0900) in buffer pointed +// by |out|. The buffer must be at least 25 bytes, including terminal +// NULL byte. Expected type of |tp| is std::chrono::time_point. This +// function returns StringRef wrapping the buffer pointed by |out|, +// and this string is terminated by NULL. +template <typename T> StringRef format_iso8601_basic(char *out, const T &tp) { + auto t = std::chrono::duration_cast<std::chrono::milliseconds>( + tp.time_since_epoch()); + auto p = iso8601_basic_date(out, t.count()); + *p = '\0'; + return StringRef{out, p}; +} + +// Writes given time |tp| in HTTP Date format (e.g., Mon, 10 Oct 2016 +// 10:25:58 GMT) in buffer pointed by |out|. The buffer must be at +// least 30 bytes, including terminal NULL byte. Expected type of +// |tp| is std::chrono::time_point. This function returns StringRef +// wrapping the buffer pointed by |out|, and this string is terminated +// by NULL. +template <typename T> StringRef format_http_date(char *out, const T &tp) { + auto t = + std::chrono::duration_cast<std::chrono::seconds>(tp.time_since_epoch()); + auto p = http_date(out, t.count()); + *p = '\0'; + return StringRef{out, p}; +} + +// Return the system precision of the template parameter |Clock| as +// a nanosecond value of type |Rep| +template <typename Clock, typename Rep> Rep clock_precision() { + std::chrono::duration<Rep, std::nano> duration = typename Clock::duration(1); + + return duration.count(); +} + +#ifdef HAVE_LIBEV +template <typename Duration = std::chrono::steady_clock::duration> +Duration duration_from(ev_tstamp d) { + return std::chrono::duration_cast<Duration>(std::chrono::duration<double>(d)); +} + +template <typename Duration> ev_tstamp ev_tstamp_from(const Duration &d) { + return std::chrono::duration<double>(d).count(); +} +#endif // HAVE_LIBEV + +int make_socket_closeonexec(int fd); +int make_socket_nonblocking(int fd); +int make_socket_nodelay(int fd); + +int create_nonblock_socket(int family); +int create_nonblock_udp_socket(int family); + +int bind_any_addr_udp(int fd, int family); + +bool check_socket_connected(int fd); + +// Returns the error code (errno) by inspecting SO_ERROR of given +// |fd|. This function returns the error code if it succeeds, or -1. +// Returning 0 means no error. +int get_socket_error(int fd); + +// Returns true if |host| is IPv6 numeric address (e.g., ::1) +bool ipv6_numeric_addr(const char *host); + +// Parses NULL terminated string |s| as unsigned integer and returns +// the parsed integer. Additionally, if |s| ends with 'k', 'm', 'g' +// and its upper case characters, multiply the integer by 1024, 1024 * +// 1024 and 1024 * 1024 respectively. If there is an error, returns +// -1. +int64_t parse_uint_with_unit(const char *s); +// The following overload does not require |s| is NULL terminated. +int64_t parse_uint_with_unit(const uint8_t *s, size_t len); +int64_t parse_uint_with_unit(const StringRef &s); + +// Parses NULL terminated string |s| as unsigned integer and returns +// the parsed integer. If there is an error, returns -1. +int64_t parse_uint(const char *s); +// The following overload does not require |s| is NULL terminated. +int64_t parse_uint(const uint8_t *s, size_t len); +int64_t parse_uint(const std::string &s); +int64_t parse_uint(const StringRef &s); + +// Parses NULL terminated string |s| as unsigned integer and returns +// the parsed integer casted to double. If |s| ends with "s", the +// parsed value's unit is a second. If |s| ends with "ms", the unit +// is millisecond. Similarly, it also supports 'm' and 'h' for +// minutes and hours respectively. If none of them are given, the +// unit is second. This function returns +// std::numeric_limits<double>::infinity() if error occurs. +double parse_duration_with_unit(const char *s); +// The following overload does not require |s| is NULL terminated. +double parse_duration_with_unit(const uint8_t *s, size_t len); +double parse_duration_with_unit(const StringRef &s); + +// Returns string representation of time duration |t|. If t has +// fractional part (at least more than or equal to 1e-3), |t| is +// multiplied by 1000 and the unit "ms" is appended. Otherwise, |t| +// is left as is and "s" is appended. +std::string duration_str(double t); + +// Returns string representation of time duration |t|. It appends +// unit after the formatting. The available units are s, ms and us. +// The unit which is equal to or less than |t| is used and 2 +// fractional digits follow. +std::string format_duration(const std::chrono::microseconds &u); + +// Just like above, but this takes |t| as seconds. +std::string format_duration(double t); + +// The maximum buffer size including terminal NULL to store the result +// of make_hostport. +constexpr size_t max_hostport = NI_MAXHOST + /* [] for IPv6 */ 2 + /* : */ 1 + + /* port */ 5 + /* terminal NULL */ 1; + +// Just like make_http_hostport(), but doesn't treat 80 and 443 +// specially. +StringRef make_hostport(BlockAllocator &balloc, const StringRef &host, + uint16_t port); + +template <typename OutputIt> +StringRef make_hostport(OutputIt first, const StringRef &host, uint16_t port) { + auto ipv6 = ipv6_numeric_addr(host.c_str()); + auto serv = utos(port); + auto p = first; + + if (ipv6) { + *p++ = '['; + } + + p = std::copy(std::begin(host), std::end(host), p); + + if (ipv6) { + *p++ = ']'; + } + + *p++ = ':'; + + p = std::copy(std::begin(serv), std::end(serv), p); + + *p = '\0'; + + return StringRef{first, p}; +} + +// Creates "host:port" string using given |host| and |port|. If +// |host| is numeric IPv6 address (e.g., ::1), it is enclosed by "[" +// and "]". If |port| is 80 or 443, port part is omitted. +StringRef make_http_hostport(BlockAllocator &balloc, const StringRef &host, + uint16_t port); + +template <typename OutputIt> +StringRef make_http_hostport(OutputIt first, const StringRef &host, + uint16_t port) { + if (port != 80 && port != 443) { + return make_hostport(first, host, port); + } + + auto ipv6 = ipv6_numeric_addr(host.c_str()); + auto p = first; + + if (ipv6) { + *p++ = '['; + } + + p = std::copy(std::begin(host), std::end(host), p); + + if (ipv6) { + *p++ = ']'; + } + + *p = '\0'; + + return StringRef{first, p}; +} + +// Dumps |src| of length |len| in the format similar to `hexdump -C`. +void hexdump(FILE *out, const uint8_t *src, size_t len); + +// Copies 2 byte unsigned integer |n| in host byte order to |buf| in +// network byte order. +void put_uint16be(uint8_t *buf, uint16_t n); + +// Copies 4 byte unsigned integer |n| in host byte order to |buf| in +// network byte order. +void put_uint32be(uint8_t *buf, uint32_t n); + +// Retrieves 2 byte unsigned integer stored in |data| in network byte +// order and returns it in host byte order. +uint16_t get_uint16(const uint8_t *data); + +// Retrieves 4 byte unsigned integer stored in |data| in network byte +// order and returns it in host byte order. +uint32_t get_uint32(const uint8_t *data); + +// Retrieves 8 byte unsigned integer stored in |data| in network byte +// order and returns it in host byte order. +uint64_t get_uint64(const uint8_t *data); + +// Reads mime types file (see /etc/mime.types), and stores extension +// -> MIME type map in |res|. This function returns 0 if it succeeds, +// or -1. +int read_mime_types(std::map<std::string, std::string> &res, + const char *filename); + +// Fills random alpha and digit byte to the range [|first|, |last|). +// Returns the one beyond the |last|. +template <typename OutputIt, typename Generator> +OutputIt random_alpha_digit(OutputIt first, OutputIt last, Generator &gen) { + // If we use uint8_t instead char, gcc 6.2.0 complains by shouting + // char-array initialized from wide string. + static constexpr char s[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + std::uniform_int_distribution<> dis(0, 26 * 2 + 10 - 1); + for (; first != last; ++first) { + *first = s[dis(gen)]; + } + return first; +} + +// Fills random bytes to the range [|first|, |last|). +template <typename OutputIt, typename Generator> +void random_bytes(OutputIt first, OutputIt last, Generator &gen) { + std::uniform_int_distribution<uint8_t> dis; + std::generate(first, last, [&dis, &gen]() { return dis(gen); }); +} + +// Shuffles the range [|first|, |last|] by calling swap function |fun| +// for each pair. |fun| takes 2 RandomIt iterators. If |fun| is +// noop, no modification is made. +template <typename RandomIt, typename Generator, typename SwapFun> +void shuffle(RandomIt first, RandomIt last, Generator &&gen, SwapFun fun) { + auto len = std::distance(first, last); + if (len < 2) { + return; + } + + for (unsigned int i = 0; i < static_cast<unsigned int>(len - 1); ++i) { + auto dis = std::uniform_int_distribution<unsigned int>(i, len - 1); + auto j = dis(gen); + if (i == j) { + continue; + } + fun(first + i, first + j); + } +} + +template <typename OutputIterator, typename CharT, size_t N> +OutputIterator copy_lit(OutputIterator it, CharT (&s)[N]) { + return std::copy_n(s, N - 1, it); +} + +// Returns x**y +double int_pow(double x, size_t y); + +uint32_t hash32(const StringRef &s); + +// Computes SHA-256 of |s|, and stores it in |buf|. This function +// returns 0 if it succeeds, or -1. +int sha256(uint8_t *buf, const StringRef &s); + +// Computes SHA-1 of |s|, and stores it in |buf|. This function +// returns 0 if it succeeds, or -1. +int sha1(uint8_t *buf, const StringRef &s); + +// Returns host from |hostport|. If host cannot be found in +// |hostport|, returns empty string. The returned string might not be +// NULL-terminated. +StringRef extract_host(const StringRef &hostport); + +// split_hostport splits host and port in |hostport|. Unlike +// extract_host, square brackets enclosing host name is stripped. If +// port is not available, it returns empty string in the second +// string. The returned string might not be NULL-terminated. On any +// error, it returns a pair which has empty strings. +std::pair<StringRef, StringRef> split_hostport(const StringRef &hostport); + +// Returns new std::mt19937 object. +std::mt19937 make_mt19937(); + +// daemonize calls daemon(3). If __APPLE__ is defined, it implements +// daemon() using fork(). +int daemonize(int nochdir, int noclose); + +// Returns |s| from which trailing white spaces (SPC or HTAB) are +// removed. If any white spaces are removed, new string is allocated +// by |balloc| and returned. Otherwise, the copy of |s| is returned +// without allocation. +StringRef rstrip(BlockAllocator &balloc, const StringRef &s); + +#ifdef ENABLE_HTTP3 +int msghdr_get_local_addr(Address &dest, msghdr *msg, int family); + +uint8_t msghdr_get_ecn(msghdr *msg, int family); + +// msghdr_get_udp_gro returns UDP_GRO value from |msg|. If UDP_GRO is +// not found, or UDP_GRO is not supported, this function returns 0. +size_t msghdr_get_udp_gro(msghdr *msg); +#endif // ENABLE_HTTP3 + +} // namespace util + +} // namespace nghttp2 + +#endif // UTIL_H diff --git a/src/util_test.cc b/src/util_test.cc new file mode 100644 index 0000000..0ac0f1d --- /dev/null +++ b/src/util_test.cc @@ -0,0 +1,707 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "util_test.h" + +#include <cstring> +#include <iostream> +#include <random> + +#include <CUnit/CUnit.h> + +#include <nghttp2/nghttp2.h> + +#include "util.h" +#include "template.h" + +using namespace nghttp2; + +namespace shrpx { + +void test_util_streq(void) { + CU_ASSERT( + util::streq(StringRef::from_lit("alpha"), StringRef::from_lit("alpha"))); + CU_ASSERT(!util::streq(StringRef::from_lit("alpha"), + StringRef::from_lit("alphabravo"))); + CU_ASSERT(!util::streq(StringRef::from_lit("alphabravo"), + StringRef::from_lit("alpha"))); + CU_ASSERT( + !util::streq(StringRef::from_lit("alpha"), StringRef::from_lit("alphA"))); + CU_ASSERT(!util::streq(StringRef{}, StringRef::from_lit("a"))); + CU_ASSERT(util::streq(StringRef{}, StringRef{})); + CU_ASSERT(!util::streq(StringRef::from_lit("alpha"), StringRef{})); + + CU_ASSERT( + !util::streq(StringRef::from_lit("alph"), StringRef::from_lit("alpha"))); + CU_ASSERT( + !util::streq(StringRef::from_lit("alpha"), StringRef::from_lit("alph"))); + CU_ASSERT( + !util::streq(StringRef::from_lit("alpha"), StringRef::from_lit("alphA"))); + + CU_ASSERT(util::streq_l("alpha", "alpha", 5)); + CU_ASSERT(util::streq_l("alpha", "alphabravo", 5)); + CU_ASSERT(!util::streq_l("alpha", "alphabravo", 6)); + CU_ASSERT(!util::streq_l("alphabravo", "alpha", 5)); + CU_ASSERT(!util::streq_l("alpha", "alphA", 5)); + CU_ASSERT(!util::streq_l("", "a", 1)); + CU_ASSERT(util::streq_l("", "", 0)); + CU_ASSERT(!util::streq_l("alpha", "", 0)); +} + +void test_util_strieq(void) { + CU_ASSERT(util::strieq(std::string("alpha"), std::string("alpha"))); + CU_ASSERT(util::strieq(std::string("alpha"), std::string("AlPhA"))); + CU_ASSERT(util::strieq(std::string(), std::string())); + CU_ASSERT(!util::strieq(std::string("alpha"), std::string("AlPhA "))); + CU_ASSERT(!util::strieq(std::string(), std::string("AlPhA "))); + + CU_ASSERT( + util::strieq(StringRef::from_lit("alpha"), StringRef::from_lit("alpha"))); + CU_ASSERT( + util::strieq(StringRef::from_lit("alpha"), StringRef::from_lit("AlPhA"))); + CU_ASSERT(util::strieq(StringRef{}, StringRef{})); + CU_ASSERT(!util::strieq(StringRef::from_lit("alpha"), + StringRef::from_lit("AlPhA "))); + CU_ASSERT( + !util::strieq(StringRef::from_lit(""), StringRef::from_lit("AlPhA "))); + + CU_ASSERT(util::strieq_l("alpha", "alpha", 5)); + CU_ASSERT(util::strieq_l("alpha", "AlPhA", 5)); + CU_ASSERT(util::strieq_l("", static_cast<const char *>(nullptr), 0)); + CU_ASSERT(!util::strieq_l("alpha", "AlPhA ", 6)); + CU_ASSERT(!util::strieq_l("", "AlPhA ", 6)); + + CU_ASSERT(util::strieq_l("alpha", StringRef::from_lit("alpha"))); + CU_ASSERT(util::strieq_l("alpha", StringRef::from_lit("AlPhA"))); + CU_ASSERT(util::strieq_l("", StringRef{})); + CU_ASSERT(!util::strieq_l("alpha", StringRef::from_lit("AlPhA "))); + CU_ASSERT(!util::strieq_l("", StringRef::from_lit("AlPhA "))); +} + +void test_util_inp_strlower(void) { + std::string a("alPha"); + util::inp_strlower(a); + CU_ASSERT("alpha" == a); + + a = "ALPHA123BRAVO"; + util::inp_strlower(a); + CU_ASSERT("alpha123bravo" == a); + + a = ""; + util::inp_strlower(a); + CU_ASSERT("" == a); +} + +void test_util_to_base64(void) { + BlockAllocator balloc(4096, 4096); + + CU_ASSERT("AAA++B/=" == + util::to_base64(balloc, StringRef::from_lit("AAA--B_"))); + CU_ASSERT("AAA++B/B" == + util::to_base64(balloc, StringRef::from_lit("AAA--B_B"))); +} + +void test_util_to_token68(void) { + std::string x = "AAA++B/="; + util::to_token68(x); + CU_ASSERT("AAA--B_" == x); + + x = "AAA++B/B"; + util::to_token68(x); + CU_ASSERT("AAA--B_B" == x); +} + +void test_util_percent_encode_token(void) { + BlockAllocator balloc(4096, 4096); + CU_ASSERT("h2" == + util::percent_encode_token(balloc, StringRef::from_lit("h2"))); + CU_ASSERT("h3~" == + util::percent_encode_token(balloc, StringRef::from_lit("h3~"))); + CU_ASSERT("100%25" == + util::percent_encode_token(balloc, StringRef::from_lit("100%"))); + CU_ASSERT("http%202" == + util::percent_encode_token(balloc, StringRef::from_lit("http 2"))); +} + +void test_util_percent_decode(void) { + { + std::string s = "%66%6F%6f%62%61%72"; + CU_ASSERT("foobar" == util::percent_decode(std::begin(s), std::end(s))); + } + { + std::string s = "%66%6"; + CU_ASSERT("f%6" == util::percent_decode(std::begin(s), std::end(s))); + } + { + std::string s = "%66%"; + CU_ASSERT("f%" == util::percent_decode(std::begin(s), std::end(s))); + } + BlockAllocator balloc(1024, 1024); + + CU_ASSERT("foobar" == util::percent_decode( + balloc, StringRef::from_lit("%66%6F%6f%62%61%72"))); + + CU_ASSERT("f%6" == + util::percent_decode(balloc, StringRef::from_lit("%66%6"))); + + CU_ASSERT("f%" == util::percent_decode(balloc, StringRef::from_lit("%66%"))); +} + +void test_util_quote_string(void) { + BlockAllocator balloc(4096, 4096); + CU_ASSERT("alpha" == + util::quote_string(balloc, StringRef::from_lit("alpha"))); + CU_ASSERT("" == util::quote_string(balloc, StringRef::from_lit(""))); + CU_ASSERT("\\\"alpha\\\"" == + util::quote_string(balloc, StringRef::from_lit("\"alpha\""))); +} + +void test_util_utox(void) { + CU_ASSERT("0" == util::utox(0)); + CU_ASSERT("1" == util::utox(1)); + CU_ASSERT("F" == util::utox(15)); + CU_ASSERT("10" == util::utox(16)); + CU_ASSERT("3B9ACA07" == util::utox(1000000007)); + CU_ASSERT("100000000" == util::utox(1LL << 32)); +} + +void test_util_http_date(void) { + CU_ASSERT("Thu, 01 Jan 1970 00:00:00 GMT" == util::http_date(0)); + CU_ASSERT("Wed, 29 Feb 2012 09:15:16 GMT" == util::http_date(1330506916)); + + std::array<char, 30> http_buf; + + CU_ASSERT("Thu, 01 Jan 1970 00:00:00 GMT" == + util::format_http_date(http_buf.data(), + std::chrono::system_clock::time_point())); + CU_ASSERT("Wed, 29 Feb 2012 09:15:16 GMT" == + util::format_http_date(http_buf.data(), + std::chrono::system_clock::time_point( + std::chrono::seconds(1330506916)))); +} + +void test_util_select_h2(void) { + const unsigned char *out = nullptr; + unsigned char outlen = 0; + + // Check single entry and select it. + const unsigned char t1[] = "\x2h2"; + CU_ASSERT(util::select_h2(&out, &outlen, t1, sizeof(t1) - 1)); + CU_ASSERT( + memcmp(NGHTTP2_PROTO_VERSION_ID, out, NGHTTP2_PROTO_VERSION_ID_LEN) == 0); + CU_ASSERT(NGHTTP2_PROTO_VERSION_ID_LEN == outlen); + + out = nullptr; + outlen = 0; + + // Check the case where id is correct but length is invalid and too + // long. + const unsigned char t2[] = "\x6h2-14"; + CU_ASSERT(!util::select_h2(&out, &outlen, t2, sizeof(t2) - 1)); + + // Check the case where h2 is located after bogus ID. + const unsigned char t3[] = "\x2h3\x2h2"; + CU_ASSERT(util::select_h2(&out, &outlen, t3, sizeof(t3) - 1)); + + CU_ASSERT( + memcmp(NGHTTP2_PROTO_VERSION_ID, out, NGHTTP2_PROTO_VERSION_ID_LEN) == 0); + CU_ASSERT(NGHTTP2_PROTO_VERSION_ID_LEN == outlen); + + out = nullptr; + outlen = 0; + + // Check the case that last entry's length is invalid and too long. + const unsigned char t4[] = "\x2h3\x6h2-14"; + CU_ASSERT(!util::select_h2(&out, &outlen, t4, sizeof(t4) - 1)); + + // Check the case that all entries are not supported. + const unsigned char t5[] = "\x2h3\x2h4"; + CU_ASSERT(!util::select_h2(&out, &outlen, t5, sizeof(t5) - 1)); + + // Check the case where 2 values are eligible, but last one is + // picked up because it has precedence over the other. + const unsigned char t6[] = "\x5h2-14\x5h2-16"; + CU_ASSERT(util::select_h2(&out, &outlen, t6, sizeof(t6) - 1)); + CU_ASSERT(util::streq(NGHTTP2_H2_16, StringRef{out, outlen})); +} + +void test_util_ipv6_numeric_addr(void) { + CU_ASSERT(util::ipv6_numeric_addr("::1")); + CU_ASSERT(util::ipv6_numeric_addr("2001:0db8:85a3:0042:1000:8a2e:0370:7334")); + // IPv4 + CU_ASSERT(!util::ipv6_numeric_addr("127.0.0.1")); + // not numeric address + CU_ASSERT(!util::ipv6_numeric_addr("localhost")); +} + +void test_util_utos(void) { + uint8_t buf[32]; + + CU_ASSERT(("0" == StringRef{buf, util::utos(buf, 0)})); + CU_ASSERT(("123" == StringRef{buf, util::utos(buf, 123)})); + CU_ASSERT(("18446744073709551615" == + StringRef{buf, util::utos(buf, 18446744073709551615ULL)})); +} + +void test_util_make_string_ref_uint(void) { + BlockAllocator balloc(1024, 1024); + + CU_ASSERT("0" == util::make_string_ref_uint(balloc, 0)); + CU_ASSERT("123" == util::make_string_ref_uint(balloc, 123)); + CU_ASSERT("18446744073709551615" == + util::make_string_ref_uint(balloc, 18446744073709551615ULL)); +} + +void test_util_utos_unit(void) { + CU_ASSERT("0" == util::utos_unit(0)); + CU_ASSERT("1023" == util::utos_unit(1023)); + CU_ASSERT("1K" == util::utos_unit(1024)); + CU_ASSERT("1K" == util::utos_unit(1025)); + CU_ASSERT("1M" == util::utos_unit(1 << 20)); + CU_ASSERT("1G" == util::utos_unit(1 << 30)); + CU_ASSERT("1024G" == util::utos_unit(1LL << 40)); +} + +void test_util_utos_funit(void) { + CU_ASSERT("0" == util::utos_funit(0)); + CU_ASSERT("1023" == util::utos_funit(1023)); + CU_ASSERT("1.00K" == util::utos_funit(1024)); + CU_ASSERT("1.00K" == util::utos_funit(1025)); + CU_ASSERT("1.09K" == util::utos_funit(1119)); + CU_ASSERT("1.27K" == util::utos_funit(1300)); + CU_ASSERT("1.00M" == util::utos_funit(1 << 20)); + CU_ASSERT("1.18M" == util::utos_funit(1234567)); + CU_ASSERT("1.00G" == util::utos_funit(1 << 30)); + CU_ASSERT("4492450797.23G" == util::utos_funit(4823732313248234343LL)); + CU_ASSERT("1024.00G" == util::utos_funit(1LL << 40)); +} + +void test_util_parse_uint_with_unit(void) { + CU_ASSERT(0 == util::parse_uint_with_unit("0")); + CU_ASSERT(1023 == util::parse_uint_with_unit("1023")); + CU_ASSERT(1024 == util::parse_uint_with_unit("1k")); + CU_ASSERT(2048 == util::parse_uint_with_unit("2K")); + CU_ASSERT(1 << 20 == util::parse_uint_with_unit("1m")); + CU_ASSERT(1 << 21 == util::parse_uint_with_unit("2M")); + CU_ASSERT(1 << 30 == util::parse_uint_with_unit("1g")); + CU_ASSERT(1LL << 31 == util::parse_uint_with_unit("2G")); + CU_ASSERT(9223372036854775807LL == + util::parse_uint_with_unit("9223372036854775807")); + // check overflow case + CU_ASSERT(-1 == util::parse_uint_with_unit("9223372036854775808")); + CU_ASSERT(-1 == util::parse_uint_with_unit("10000000000000000000")); + CU_ASSERT(-1 == util::parse_uint_with_unit("9223372036854775807G")); + // bad characters + CU_ASSERT(-1 == util::parse_uint_with_unit("1.1")); + CU_ASSERT(-1 == util::parse_uint_with_unit("1a")); + CU_ASSERT(-1 == util::parse_uint_with_unit("a1")); + CU_ASSERT(-1 == util::parse_uint_with_unit("1T")); + CU_ASSERT(-1 == util::parse_uint_with_unit("")); +} + +void test_util_parse_uint(void) { + CU_ASSERT(0 == util::parse_uint("0")); + CU_ASSERT(1023 == util::parse_uint("1023")); + CU_ASSERT(-1 == util::parse_uint("1k")); + CU_ASSERT(9223372036854775807LL == util::parse_uint("9223372036854775807")); + // check overflow case + CU_ASSERT(-1 == util::parse_uint("9223372036854775808")); + CU_ASSERT(-1 == util::parse_uint("10000000000000000000")); + // bad characters + CU_ASSERT(-1 == util::parse_uint("1.1")); + CU_ASSERT(-1 == util::parse_uint("1a")); + CU_ASSERT(-1 == util::parse_uint("a1")); + CU_ASSERT(-1 == util::parse_uint("1T")); + CU_ASSERT(-1 == util::parse_uint("")); +} + +void test_util_parse_duration_with_unit(void) { + CU_ASSERT(0. == util::parse_duration_with_unit("0")); + CU_ASSERT(123. == util::parse_duration_with_unit("123")); + CU_ASSERT(123. == util::parse_duration_with_unit("123s")); + CU_ASSERT(0.500 == util::parse_duration_with_unit("500ms")); + CU_ASSERT(123. == util::parse_duration_with_unit("123S")); + CU_ASSERT(0.500 == util::parse_duration_with_unit("500MS")); + CU_ASSERT(180 == util::parse_duration_with_unit("3m")); + CU_ASSERT(3600 * 5 == util::parse_duration_with_unit("5h")); + + auto err = std::numeric_limits<double>::infinity(); + // check overflow case + CU_ASSERT(err == util::parse_duration_with_unit("9223372036854775808")); + // bad characters + CU_ASSERT(err == util::parse_duration_with_unit("0u")); + CU_ASSERT(err == util::parse_duration_with_unit("0xs")); + CU_ASSERT(err == util::parse_duration_with_unit("0mt")); + CU_ASSERT(err == util::parse_duration_with_unit("0mss")); + CU_ASSERT(err == util::parse_duration_with_unit("s")); + CU_ASSERT(err == util::parse_duration_with_unit("ms")); +} + +void test_util_duration_str(void) { + CU_ASSERT("0" == util::duration_str(0.)); + CU_ASSERT("1s" == util::duration_str(1.)); + CU_ASSERT("500ms" == util::duration_str(0.5)); + CU_ASSERT("1500ms" == util::duration_str(1.5)); + CU_ASSERT("2m" == util::duration_str(120.)); + CU_ASSERT("121s" == util::duration_str(121.)); + CU_ASSERT("1h" == util::duration_str(3600.)); +} + +void test_util_format_duration(void) { + CU_ASSERT("0us" == util::format_duration(std::chrono::microseconds(0))); + CU_ASSERT("999us" == util::format_duration(std::chrono::microseconds(999))); + CU_ASSERT("1.00ms" == util::format_duration(std::chrono::microseconds(1000))); + CU_ASSERT("1.09ms" == util::format_duration(std::chrono::microseconds(1090))); + CU_ASSERT("1.01ms" == util::format_duration(std::chrono::microseconds(1009))); + CU_ASSERT("999.99ms" == + util::format_duration(std::chrono::microseconds(999990))); + CU_ASSERT("1.00s" == + util::format_duration(std::chrono::microseconds(1000000))); + CU_ASSERT("1.05s" == + util::format_duration(std::chrono::microseconds(1050000))); + + CU_ASSERT("0us" == util::format_duration(0.)); + CU_ASSERT("999us" == util::format_duration(0.000999)); + CU_ASSERT("1.00ms" == util::format_duration(0.001)); + CU_ASSERT("1.09ms" == util::format_duration(0.00109)); + CU_ASSERT("1.01ms" == util::format_duration(0.001009)); + CU_ASSERT("999.99ms" == util::format_duration(0.99999)); + CU_ASSERT("1.00s" == util::format_duration(1.)); + CU_ASSERT("1.05s" == util::format_duration(1.05)); +} + +void test_util_starts_with(void) { + CU_ASSERT(util::starts_with(StringRef::from_lit("foo"), + StringRef::from_lit("foo"))); + CU_ASSERT(util::starts_with(StringRef::from_lit("fooo"), + StringRef::from_lit("foo"))); + CU_ASSERT(util::starts_with(StringRef::from_lit("ofoo"), StringRef{})); + CU_ASSERT(!util::starts_with(StringRef::from_lit("ofoo"), + StringRef::from_lit("foo"))); + + CU_ASSERT(util::istarts_with(StringRef::from_lit("FOO"), + StringRef::from_lit("fOO"))); + CU_ASSERT(util::istarts_with(StringRef::from_lit("ofoo"), StringRef{})); + CU_ASSERT(util::istarts_with(StringRef::from_lit("fOOo"), + StringRef::from_lit("Foo"))); + CU_ASSERT(!util::istarts_with(StringRef::from_lit("ofoo"), + StringRef::from_lit("foo"))); + + CU_ASSERT(util::istarts_with_l(StringRef::from_lit("fOOo"), "Foo")); + CU_ASSERT(!util::istarts_with_l(StringRef::from_lit("ofoo"), "foo")); +} + +void test_util_ends_with(void) { + CU_ASSERT( + util::ends_with(StringRef::from_lit("foo"), StringRef::from_lit("foo"))); + CU_ASSERT(util::ends_with(StringRef::from_lit("foo"), StringRef{})); + CU_ASSERT( + util::ends_with(StringRef::from_lit("ofoo"), StringRef::from_lit("foo"))); + CU_ASSERT( + !util::ends_with(StringRef::from_lit("ofoo"), StringRef::from_lit("fo"))); + + CU_ASSERT( + util::iends_with(StringRef::from_lit("fOo"), StringRef::from_lit("Foo"))); + CU_ASSERT(util::iends_with(StringRef::from_lit("foo"), StringRef{})); + CU_ASSERT(util::iends_with(StringRef::from_lit("oFoo"), + StringRef::from_lit("fOO"))); + CU_ASSERT(!util::iends_with(StringRef::from_lit("ofoo"), + StringRef::from_lit("fo"))); + + CU_ASSERT(util::iends_with_l(StringRef::from_lit("oFoo"), "fOO")); + CU_ASSERT(!util::iends_with_l(StringRef::from_lit("ofoo"), "fo")); +} + +void test_util_parse_http_date(void) { + CU_ASSERT(1001939696 == util::parse_http_date(StringRef::from_lit( + "Mon, 1 Oct 2001 12:34:56 GMT"))); +} + +void test_util_localtime_date(void) { + auto tz = getenv("TZ"); + if (tz) { + tz = strdup(tz); + } +#ifdef __linux__ + setenv("TZ", "NZST-12:00:00:00", 1); +#else // !__linux__ + setenv("TZ", ":Pacific/Auckland", 1); +#endif // !__linux__ + tzset(); + + CU_ASSERT_STRING_EQUAL("02/Oct/2001:00:34:56 +1200", + util::common_log_date(1001939696).c_str()); + CU_ASSERT_STRING_EQUAL("2001-10-02T00:34:56.123+12:00", + util::iso8601_date(1001939696000LL + 123).c_str()); + + std::array<char, 27> common_buf; + + CU_ASSERT("02/Oct/2001:00:34:56 +1200" == + util::format_common_log(common_buf.data(), + std::chrono::system_clock::time_point( + std::chrono::seconds(1001939696)))); + + std::array<char, 30> iso8601_buf; + + CU_ASSERT( + "2001-10-02T00:34:56.123+12:00" == + util::format_iso8601(iso8601_buf.data(), + std::chrono::system_clock::time_point( + std::chrono::milliseconds(1001939696123LL)))); + + if (tz) { + setenv("TZ", tz, 1); + free(tz); + } else { + unsetenv("TZ"); + } + tzset(); +} + +void test_util_get_uint64(void) { + { + auto v = std::array<unsigned char, 8>{ + {0x01, 0x12, 0x34, 0x56, 0xff, 0x9a, 0xab, 0xbc}}; + + auto n = util::get_uint64(v.data()); + + CU_ASSERT(0x01123456ff9aabbcULL == n); + } + { + auto v = std::array<unsigned char, 8>{ + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}; + + auto n = util::get_uint64(v.data()); + + CU_ASSERT(0xffffffffffffffffULL == n); + } +} + +void test_util_parse_config_str_list(void) { + auto res = util::parse_config_str_list(StringRef::from_lit("a")); + CU_ASSERT(1 == res.size()); + CU_ASSERT("a" == res[0]); + + res = util::parse_config_str_list(StringRef::from_lit("a,")); + CU_ASSERT(2 == res.size()); + CU_ASSERT("a" == res[0]); + CU_ASSERT("" == res[1]); + + res = util::parse_config_str_list(StringRef::from_lit(":a::"), ':'); + CU_ASSERT(4 == res.size()); + CU_ASSERT("" == res[0]); + CU_ASSERT("a" == res[1]); + CU_ASSERT("" == res[2]); + CU_ASSERT("" == res[3]); + + res = util::parse_config_str_list(StringRef{}); + CU_ASSERT(1 == res.size()); + CU_ASSERT("" == res[0]); + + res = util::parse_config_str_list(StringRef::from_lit("alpha,bravo,charlie")); + CU_ASSERT(3 == res.size()); + CU_ASSERT("alpha" == res[0]); + CU_ASSERT("bravo" == res[1]); + CU_ASSERT("charlie" == res[2]); +} + +void test_util_make_http_hostport(void) { + BlockAllocator balloc(4096, 4096); + + CU_ASSERT("localhost" == util::make_http_hostport( + balloc, StringRef::from_lit("localhost"), 80)); + CU_ASSERT("[::1]" == + util::make_http_hostport(balloc, StringRef::from_lit("::1"), 443)); + CU_ASSERT( + "localhost:3000" == + util::make_http_hostport(balloc, StringRef::from_lit("localhost"), 3000)); +} + +void test_util_make_hostport(void) { + std::array<char, util::max_hostport> hostport_buf; + CU_ASSERT("localhost:80" == + util::make_hostport(std::begin(hostport_buf), + StringRef::from_lit("localhost"), 80)); + CU_ASSERT("[::1]:443" == util::make_hostport(std::begin(hostport_buf), + StringRef::from_lit("::1"), + 443)); + + BlockAllocator balloc(4096, 4096); + CU_ASSERT("localhost:80" == + util::make_hostport(balloc, StringRef::from_lit("localhost"), 80)); + CU_ASSERT("[::1]:443" == + util::make_hostport(balloc, StringRef::from_lit("::1"), 443)); +} + +void test_util_strifind(void) { + CU_ASSERT(util::strifind(StringRef::from_lit("gzip, deflate, bzip2"), + StringRef::from_lit("gzip"))); + + CU_ASSERT(util::strifind(StringRef::from_lit("gzip, deflate, bzip2"), + StringRef::from_lit("dEflate"))); + + CU_ASSERT(util::strifind(StringRef::from_lit("gzip, deflate, bzip2"), + StringRef::from_lit("BZIP2"))); + + CU_ASSERT(util::strifind(StringRef::from_lit("nghttp2"), StringRef{})); + + // Be aware this fact + CU_ASSERT(!util::strifind(StringRef{}, StringRef{})); + + CU_ASSERT(!util::strifind(StringRef::from_lit("nghttp2"), + StringRef::from_lit("http1"))); +} + +void test_util_random_alpha_digit(void) { + std::random_device rd; + std::mt19937 gen(rd()); + std::array<uint8_t, 19> data; + + auto p = util::random_alpha_digit(std::begin(data), std::end(data), gen); + + CU_ASSERT(std::end(data) == p); + + for (auto b : data) { + CU_ASSERT(('A' <= b && b <= 'Z') || ('a' <= b && b <= 'z') || + ('0' <= b && b <= '9')); + } +} + +void test_util_format_hex(void) { + BlockAllocator balloc(4096, 4096); + + CU_ASSERT("0ff0" == + util::format_hex(balloc, StringRef::from_lit("\x0f\xf0"))); + CU_ASSERT("" == util::format_hex(balloc, StringRef::from_lit(""))); +} + +void test_util_is_hex_string(void) { + CU_ASSERT(util::is_hex_string(StringRef{})); + CU_ASSERT(util::is_hex_string(StringRef::from_lit("0123456789abcdef"))); + CU_ASSERT(util::is_hex_string(StringRef::from_lit("0123456789ABCDEF"))); + CU_ASSERT(!util::is_hex_string(StringRef::from_lit("000"))); + CU_ASSERT(!util::is_hex_string(StringRef::from_lit("XX"))); +} + +void test_util_decode_hex(void) { + BlockAllocator balloc(4096, 4096); + + CU_ASSERT("\x0f\xf0" == + util::decode_hex(balloc, StringRef::from_lit("0ff0"))); + CU_ASSERT("" == util::decode_hex(balloc, StringRef{})); +} + +void test_util_extract_host(void) { + CU_ASSERT(StringRef::from_lit("foo") == + util::extract_host(StringRef::from_lit("foo"))); + CU_ASSERT(StringRef::from_lit("foo") == + util::extract_host(StringRef::from_lit("foo:"))); + CU_ASSERT(StringRef::from_lit("foo") == + util::extract_host(StringRef::from_lit("foo:0"))); + CU_ASSERT(StringRef::from_lit("[::1]") == + util::extract_host(StringRef::from_lit("[::1]"))); + CU_ASSERT(StringRef::from_lit("[::1]") == + util::extract_host(StringRef::from_lit("[::1]:"))); + + CU_ASSERT(util::extract_host(StringRef::from_lit(":foo")).empty()); + CU_ASSERT(util::extract_host(StringRef::from_lit("[::1")).empty()); + CU_ASSERT(util::extract_host(StringRef::from_lit("[::1]0")).empty()); + CU_ASSERT(util::extract_host(StringRef{}).empty()); +} + +void test_util_split_hostport(void) { + CU_ASSERT(std::make_pair(StringRef::from_lit("foo"), StringRef{}) == + util::split_hostport(StringRef::from_lit("foo"))); + CU_ASSERT( + std::make_pair(StringRef::from_lit("foo"), StringRef::from_lit("80")) == + util::split_hostport(StringRef::from_lit("foo:80"))); + CU_ASSERT( + std::make_pair(StringRef::from_lit("::1"), StringRef::from_lit("80")) == + util::split_hostport(StringRef::from_lit("[::1]:80"))); + CU_ASSERT(std::make_pair(StringRef::from_lit("::1"), StringRef{}) == + util::split_hostport(StringRef::from_lit("[::1]"))); + + CU_ASSERT(std::make_pair(StringRef{}, StringRef{}) == + util::split_hostport(StringRef{})); + CU_ASSERT(std::make_pair(StringRef{}, StringRef{}) == + util::split_hostport(StringRef::from_lit("[::1]:"))); + CU_ASSERT(std::make_pair(StringRef{}, StringRef{}) == + util::split_hostport(StringRef::from_lit("foo:"))); + CU_ASSERT(std::make_pair(StringRef{}, StringRef{}) == + util::split_hostport(StringRef::from_lit("[::1:"))); + CU_ASSERT(std::make_pair(StringRef{}, StringRef{}) == + util::split_hostport(StringRef::from_lit("[::1]80"))); +} + +void test_util_split_str(void) { + CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("")} == + util::split_str(StringRef::from_lit(""), ',')); + CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("alpha")} == + util::split_str(StringRef::from_lit("alpha"), ',')); + CU_ASSERT((std::vector<StringRef>{StringRef::from_lit("alpha"), + StringRef::from_lit("")}) == + util::split_str(StringRef::from_lit("alpha,"), ',')); + CU_ASSERT((std::vector<StringRef>{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo")}) == + util::split_str(StringRef::from_lit("alpha,bravo"), ',')); + CU_ASSERT((std::vector<StringRef>{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo"), + StringRef::from_lit("charlie")}) == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',')); + CU_ASSERT( + (std::vector<StringRef>{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo"), + StringRef::from_lit("charlie")}) == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 0)); + CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("")} == + util::split_str(StringRef::from_lit(""), ',', 1)); + CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("")} == + util::split_str(StringRef::from_lit(""), ',', 2)); + CU_ASSERT( + (std::vector<StringRef>{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo,charlie")}) == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 2)); + CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("alpha")} == + util::split_str(StringRef::from_lit("alpha"), ',', 2)); + CU_ASSERT((std::vector<StringRef>{StringRef::from_lit("alpha"), + StringRef::from_lit("")}) == + util::split_str(StringRef::from_lit("alpha,"), ',', 2)); + CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("alpha")} == + util::split_str(StringRef::from_lit("alpha"), ',', 0)); + CU_ASSERT( + std::vector<StringRef>{StringRef::from_lit("alpha,bravo,charlie")} == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 1)); +} + +void test_util_rstrip(void) { + BlockAllocator balloc(4096, 4096); + + CU_ASSERT("alpha" == util::rstrip(balloc, StringRef::from_lit("alpha"))); + CU_ASSERT("alpha" == util::rstrip(balloc, StringRef::from_lit("alpha "))); + CU_ASSERT("alpha" == util::rstrip(balloc, StringRef::from_lit("alpha \t"))); + CU_ASSERT("" == util::rstrip(balloc, StringRef::from_lit(""))); + CU_ASSERT("" == util::rstrip(balloc, StringRef::from_lit("\t\t\t "))); +} + +} // namespace shrpx diff --git a/src/util_test.h b/src/util_test.h new file mode 100644 index 0000000..48925ab --- /dev/null +++ b/src/util_test.h @@ -0,0 +1,75 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2013 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef UTIL_TEST_H +#define UTIL_TEST_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif // HAVE_CONFIG_H + +namespace shrpx { + +void test_util_streq(void); +void test_util_strieq(void); +void test_util_inp_strlower(void); +void test_util_to_base64(void); +void test_util_to_token68(void); +void test_util_percent_encode_token(void); +void test_util_percent_decode(void); +void test_util_quote_string(void); +void test_util_utox(void); +void test_util_http_date(void); +void test_util_select_h2(void); +void test_util_ipv6_numeric_addr(void); +void test_util_utos(void); +void test_util_make_string_ref_uint(void); +void test_util_utos_unit(void); +void test_util_utos_funit(void); +void test_util_parse_uint_with_unit(void); +void test_util_parse_uint(void); +void test_util_parse_duration_with_unit(void); +void test_util_duration_str(void); +void test_util_format_duration(void); +void test_util_starts_with(void); +void test_util_ends_with(void); +void test_util_parse_http_date(void); +void test_util_localtime_date(void); +void test_util_get_uint64(void); +void test_util_parse_config_str_list(void); +void test_util_make_http_hostport(void); +void test_util_make_hostport(void); +void test_util_strifind(void); +void test_util_random_alpha_digit(void); +void test_util_format_hex(void); +void test_util_is_hex_string(void); +void test_util_decode_hex(void); +void test_util_extract_host(void); +void test_util_split_hostport(void); +void test_util_split_str(void); +void test_util_rstrip(void); + +} // namespace shrpx + +#endif // UTIL_TEST_H diff --git a/src/xsi_strerror.c b/src/xsi_strerror.c new file mode 100644 index 0000000..d008d4e --- /dev/null +++ b/src/xsi_strerror.c @@ -0,0 +1,50 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "xsi_strerror.h" + +/* Make sure that we get XSI-compliant version of strerror_r */ +#ifdef _POSIX_C_SOURCE +# undef _POSIX_C_SOURCE +#endif /* _POSIX_C_SOURCE */ + +#ifdef _GNU_SOURCE +# undef _GNU_SOURCE +#endif /* _GNU_SOURCE */ + +#include <string.h> + +char *xsi_strerror(int errnum, char *buf, size_t buflen) { + int rv; + + rv = strerror_r(errnum, buf, buflen); + + if (rv != 0) { + if (buflen > 0) { + buf[0] = '\0'; + } + } + + return buf; +} diff --git a/src/xsi_strerror.h b/src/xsi_strerror.h new file mode 100644 index 0000000..32cadc3 --- /dev/null +++ b/src/xsi_strerror.h @@ -0,0 +1,55 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2016 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef XSI_STRERROR_H +#define XSI_STRERROR_H + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/* Looks like error message is quite small, but we really don't know + how much longer they become. */ +#define STRERROR_BUFSIZE 256 + +/* + * Returns description of error denoted by |errnum|. The description + * is written in |buf| of length |buflen| including terminal NULL. If + * there is an error, including the case that buffer space is not + * sufficient to include error message, and |buflen| > 0, empty string + * is written to |buf|. This function returns |buf|. + */ +char *xsi_strerror(int errnum, char *buf, size_t buflen); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* XSI_STRERROR_H */ |