From bcc97b8699e889f47939367bcd44c21f2b8e1ef2 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Thu, 30 Jun 2016 23:26:12 +0900 Subject: [PATCH] Implement CACHE_DIGEST frame for nghttp and nghttpd We use https://tools.ietf.org/html/draft-kazuho-h2-cache-digest-01 as specification. We haven't implemented flags handling yet. --- lib/nghttp2_session.c | 1 - src/CMakeLists.txt | 4 +- src/HttpServer.cc | 147 +++++++++++++- src/HttpServer.h | 19 ++ src/Makefile.am | 10 +- src/app_helper.cc | 2 + src/app_helper.h | 5 + src/cache_digest.cc | 406 +++++++++++++++++++++++++++++++++++++++ src/cache_digest.h | 48 +++++ src/cache_digest_test.cc | 76 ++++++++ src/cache_digest_test.h | 38 ++++ src/nghttp.cc | 78 +++++++- src/nghttp.h | 4 + src/shrpx-unittest.cc | 5 +- 14 files changed, 830 insertions(+), 13 deletions(-) create mode 100644 src/cache_digest.cc create mode 100644 src/cache_digest.h create mode 100644 src/cache_digest_test.cc create mode 100644 src/cache_digest_test.h diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c index 8e381700..ee1d5802 100644 --- a/lib/nghttp2_session.c +++ b/lib/nghttp2_session.c @@ -5653,7 +5653,6 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, iframe->frame.hd.type)) { if (!session->callbacks.unpack_extension_callback) { /* Silently ignore unknown frame type. */ - busy = 1; iframe->state = NGHTTP2_IB_IGN_PAYLOAD; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee96cd83..a9d89d08 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,7 +39,7 @@ link_libraries( if(ENABLE_APP) set(HELPER_OBJECTS util.cc - http2.cc timegm.c app_helper.cc nghttp2_gzip.c + http2.cc timegm.c app_helper.cc nghttp2_gzip.c cache_digest.cc ) # nghttp client @@ -111,6 +111,7 @@ if(ENABLE_APP) shrpx_router.cc shrpx_api_downstream_connection.cc shrpx_health_monitor_downstream_connection.cc + cache_digest.cc ) if(HAVE_SPDYLAY) list(APPEND NGHTTPX_SRCS @@ -159,6 +160,7 @@ if(ENABLE_APP) memchunk_test.cc template_test.cc base64_test.cc + cache_digest_test.cc ) add_executable(nghttpx-unittest EXCLUDE_FROM_ALL ${NGHTTPX_UNITTEST_SOURCES} diff --git a/src/HttpServer.cc b/src/HttpServer.cc index 5865a204..84b3b247 100644 --- a/src/HttpServer.cc +++ b/src/HttpServer.cc @@ -62,6 +62,7 @@ #include "util.h" #include "ssl.h" #include "template.h" +#include "cache_digest.h" #ifndef O_BINARY #define O_BINARY (0) @@ -825,10 +826,22 @@ int Http2Handler::on_write() { return write_(*this); } int Http2Handler::connection_made() { int r; - r = nghttp2_session_server_new(&session_, sessions_->get_callbacks(), this); + nghttp2_option *opt; + r = nghttp2_option_new(&opt); if (r != 0) { - return r; + return -1; + } + + nghttp2_option_set_user_recv_extension_type(opt, NGHTTP2_DRAFT_CACHE_DIGEST); + + r = nghttp2_session_server_new2(&session_, sessions_->get_callbacks(), this, + opt); + + nghttp2_option_del(opt); + + if (r != 0) { + return -1; } auto config = sessions_->get_config(); @@ -852,7 +865,7 @@ int Http2Handler::connection_made() { r = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry.data(), niv); if (r != 0) { - return r; + return -1; } if (config->connection_window_bits != -1) { @@ -860,7 +873,7 @@ int Http2Handler::connection_made() { session_, NGHTTP2_FLAG_NONE, 0, (1 << config->connection_window_bits) - 1); if (r != 0) { - return r; + return -1; } } @@ -1064,6 +1077,39 @@ void Http2Handler::terminate_session(uint32_t error_code) { nghttp2_session_terminate_session(session_, error_code); } +std::vector &Http2Handler::get_extbuf() { return extbuf_; } + +void Http2Handler::set_cache_digest(const StringRef &authority, + std::unique_ptr cache_digest) { + origin_cache_digest_[authority.str()] = std::move(cache_digest); +} + +bool Http2Handler::cache_digest_includes(const StringRef &authority, + const StringRef &uri) const { + uint64_t key; + int rv; + + auto it = origin_cache_digest_.find(authority.str()); + + if (it == std::end(origin_cache_digest_)) { + return false; + } + + auto &cache_digest = (*it).second; + + auto key_nbits = cache_digest->logn + cache_digest->logp; + + rv = cache_digest_hash(key, key_nbits, uri); + + if (rv != 0) { + return false; + } + + const auto &keys = cache_digest->keys; + + return std::binary_search(std::begin(keys), std::end(keys), key); +} + 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) { @@ -1542,6 +1588,20 @@ int hd_on_frame_recv_callback(nghttp2_session *session, hd->remove_settings_timer(); } break; + case NGHTTP2_DRAFT_CACHE_DIGEST: { + auto stream = hd->get_stream(frame->hd.stream_id); + if (!stream) { + return 0; + } + + auto &header = stream->header; + + hd->set_cache_digest(header.authority, + std::unique_ptr( + static_cast(frame->ext.payload))); + + break; + } default: break; } @@ -1549,6 +1609,36 @@ int hd_on_frame_recv_callback(nghttp2_session *session, } } // namespace +namespace { +int before_frame_send_callback(nghttp2_session *session, + const nghttp2_frame *frame, void *user_data) { + auto hd = static_cast(user_data); + + if (frame->hd.type != NGHTTP2_PUSH_PROMISE) { + return 0; + } + + auto promised_stream = hd->get_stream(frame->push_promise.promised_stream_id); + + if (promised_stream == nullptr) { + return 0; + } + + auto &header = promised_stream->header; + + auto uri = header.scheme.str(); + uri += "://"; + uri += header.authority; + uri += header.path; + + if (hd->cache_digest_includes(header.authority, StringRef{uri})) { + return NGHTTP2_ERR_CANCEL; + } + + return 0; +} +} // namespace + namespace { int hd_on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) { @@ -1721,6 +1811,46 @@ int on_stream_close_callback(nghttp2_session *session, int32_t stream_id, } } // namespace +namespace { +int on_extension_chunk_recv_callback(nghttp2_session *session, + const nghttp2_frame_hd *frame_hd, + const uint8_t *data, size_t len, + void *user_data) { + auto hd = static_cast(user_data); + + auto &buf = hd->get_extbuf(); + buf.insert(std::end(buf), data, data + len); + + return 0; +} +} // namespace + +namespace { +int unpack_extension_callback(nghttp2_session *session, void **payload, + const nghttp2_frame_hd *frame_hd, + void *user_data) { + int rv; + auto hd = static_cast(user_data); + + auto cache_digest = make_unique(); + + auto &buf = hd->get_extbuf(); + + rv = cache_digest_decode(cache_digest->keys, cache_digest->logn, + cache_digest->logp, buf.data(), buf.size()); + + buf.clear(); + + if (rv != 0) { + return NGHTTP2_ERR_CANCEL; + } + + *payload = cache_digest.release(); + + return 0; +} +} // namespace + namespace { void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config) { nghttp2_session_callbacks_set_on_stream_close_callback( @@ -1756,6 +1886,15 @@ void fill_callback(nghttp2_session_callbacks *callbacks, const Config *config) { nghttp2_session_callbacks_set_select_padding_callback( callbacks, select_padding_callback); } + + nghttp2_session_callbacks_set_unpack_extension_callback( + callbacks, unpack_extension_callback); + + nghttp2_session_callbacks_set_on_extension_chunk_recv_callback( + callbacks, on_extension_chunk_recv_callback); + + nghttp2_session_callbacks_set_before_frame_send_callback( + callbacks, before_frame_send_callback); } } // namespace diff --git a/src/HttpServer.h b/src/HttpServer.h index 7e4819d9..433e7935 100644 --- a/src/HttpServer.h +++ b/src/HttpServer.h @@ -153,6 +153,12 @@ struct Stream { class Sessions; +struct CacheDigest { + std::vector keys; + uint32_t logn; + uint32_t logp; +}; + class Http2Handler { public: Http2Handler(Sessions *sessions, int fd, SSL *ssl, int64_t session_id); @@ -206,6 +212,15 @@ public: WriteBuf *get_wb(); + std::vector &get_extbuf(); + + // Sets given cache digest. Overwrites existing one if any. + void set_cache_digest(const StringRef &origin, + std::unique_ptr cache_digest); + // Returns true if |uri| is included in cache digest. + bool cache_digest_includes(const StringRef &origin, + const StringRef &uri) const; + private: ev_io wev_; ev_io rev_; @@ -213,6 +228,10 @@ private: std::map> id2stream_; WriteBuf wb_; std::function read_, write_; + // Received cache digest hash keys per origin + std::map> origin_cache_digest_; + // Buffer for extension frame payload + std::vector extbuf_; int64_t session_id_; nghttp2_session *session_; Sessions *sessions_; diff --git a/src/Makefile.am b/src/Makefile.am index cee98e85..53c54c1d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -64,10 +64,10 @@ if ENABLE_APP bin_PROGRAMS += nghttp nghttpd nghttpx HELPER_OBJECTS = util.cc \ - http2.cc timegm.c app_helper.cc nghttp2_gzip.c + http2.cc timegm.c app_helper.cc nghttp2_gzip.c cache_digest.cc HELPER_HFILES = util.h \ http2.h timegm.h app_helper.h nghttp2_config.h \ - nghttp2_gzip.h network.h + nghttp2_gzip.h network.h cache_digest.h HTML_PARSER_OBJECTS = HTML_PARSER_HFILES = HtmlParser.h @@ -138,7 +138,8 @@ NGHTTPX_SRCS = \ shrpx_api_downstream_connection.cc shrpx_api_downstream_connection.h \ shrpx_health_monitor_downstream_connection.cc \ shrpx_health_monitor_downstream_connection.h \ - buffer.h memchunk.h template.h allocator.h + buffer.h memchunk.h template.h allocator.h \ + cache_digest.cc cache_digest.h if HAVE_SPDYLAY NGHTTPX_SRCS += shrpx_spdy_upstream.cc shrpx_spdy_upstream.h @@ -188,7 +189,8 @@ nghttpx_unittest_SOURCES = shrpx-unittest.cc \ buffer_test.cc buffer_test.h \ memchunk_test.cc memchunk_test.h \ template_test.cc template_test.h \ - base64_test.cc base64_test.h + base64_test.cc base64_test.h \ + cache_digest_test.cc cache_digest_test.h nghttpx_unittest_CPPFLAGS = ${AM_CPPFLAGS} \ -DNGHTTP2_SRC_DIR=\"$(top_srcdir)/src\" nghttpx_unittest_LDADD = libnghttpx.a ${LDADD} @CUNIT_LIBS@ @TESTLDADD@ diff --git a/src/app_helper.cc b/src/app_helper.cc index 0b47b48c..26a71c85 100644 --- a/src/app_helper.cc +++ b/src/app_helper.cc @@ -106,6 +106,8 @@ std::string strframetype(uint8_t type) { return "WINDOW_UPDATE"; case NGHTTP2_ALTSVC: return "ALTSVC"; + case NGHTTP2_DRAFT_CACHE_DIGEST: + return "CACHE_DIGSET"; } std::string s = "extension(0x"; diff --git a/src/app_helper.h b/src/app_helper.h index 263bb99c..6675addd 100644 --- a/src/app_helper.h +++ b/src/app_helper.h @@ -41,6 +41,11 @@ namespace nghttp2 { +enum nghttp2_draft_frame_type { + // draft-kazuho-h2-cache-digest-01 + NGHTTP2_DRAFT_CACHE_DIGEST = 0xf1 +}; + int verbose_on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, diff --git a/src/cache_digest.cc b/src/cache_digest.cc new file mode 100644 index 00000000..80f74b5a --- /dev/null +++ b/src/cache_digest.cc @@ -0,0 +1,406 @@ +/* + * 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 "cache_digest.h" + +#include +#include +#include + +#include + +namespace nghttp2 { + +namespace { +int compute_hash_values(std::vector &hash_values, + const std::vector &uris, uint32_t nbits) { + int rv; + + if (nbits > 62) { + return -1; + } + + uint64_t mask = (static_cast(1) << nbits) - 1; + + auto ctx = EVP_MD_CTX_create(); + + hash_values.resize(uris.size()); + + std::array md; + + auto p = std::begin(hash_values); + for (auto &u : uris) { + rv = EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr); + if (rv != 1) { + return -1; + } + + rv = EVP_DigestUpdate(ctx, u.c_str(), u.size()); + if (rv != 1) { + return -1; + } + + unsigned int len = md.size(); + + rv = EVP_DigestFinal_ex(ctx, md.data(), &len); + if (rv != 1) { + return -1; + } + + assert(len == 32); + + uint64_t v; + + v = (static_cast(md[24]) << 56) + + (static_cast(md[25]) << 48) + + (static_cast(md[26]) << 40) + + (static_cast(md[27]) << 32) + (md[28] << 24) + + (md[29] << 16) + (md[30] << 8) + md[31]; + v &= mask; + + *p++ = v; + } + + EVP_MD_CTX_destroy(ctx); + + return 0; +} +} // namespace + +namespace { +std::pair append_uint32(uint8_t *p, size_t b, uint32_t v, + size_t nbits) { + v &= (1 << nbits) - 1; + + if (8 > b + nbits) { + *p |= (v << (8 - b - nbits)); + return {p, b + nbits}; + } + + if (8 == b + nbits) { + *p++ |= v; + return {p, 0}; + } + + auto h = 8 - b; + auto left = nbits - h; + + *p++ |= (v >> left); + b = 0; + + for (; left >= 8; left -= 8) { + *p++ = (v >> (left - 8)) & 0xff; + } + + if (left > 0) { + *p = (v & ((1 << left) - 1)) << (8 - left); + } + + return {p, left}; +} +} // namespace + +namespace { +std::pair append_0bit(uint8_t *p, size_t b, size_t nbits) { + if (8 > b + nbits) { + return {p, b + nbits}; + } + + if (8 == b + nbits) { + return {++p, 0}; + } + + nbits -= 8 - b; + ++p; + + p += nbits / 8; + + return {p, nbits % 8}; +} + +std::pair append_single_1bit(uint8_t *p, size_t b) { + if (8 > b + 1) { + *p |= (1 << (7 - b)); + return {p, b + 1}; + } + + *p++ |= 1; + + return {p, 0}; +} +} // namespace + +ssize_t cache_digest_encode(uint8_t *data, size_t datalen, + const std::vector &uris, + uint32_t logp) { + uint32_t n = 1; + uint32_t logn = 0; + + if (logp > 31) { + return -1; + } + + uint32_t p = 1; + for (auto i = 0; i < logp; ++i, p *= 2) + ; + + for (; n < uris.size(); n *= 2, ++logn) + ; + + if (n - uris.size() > uris.size() - n / 2) { + n /= 2; + --logn; + } + + auto maxlen = 2 * n + n * logp; + if (maxlen > datalen) { + return -1; + } + + std::vector hash_values; + + if (compute_hash_values(hash_values, uris, logn + logp) != 0) { + return -1; + } + + std::sort(std::begin(hash_values), std::end(hash_values)); + + auto last = data; + + size_t b = 0; + + std::fill_n(data, maxlen, 0); + + std::tie(last, b) = append_uint32(last, b, logn, 5); + std::tie(last, b) = append_uint32(last, b, logp, 5); + + auto c = std::numeric_limits::max(); + + for (auto v : hash_values) { + if (v == c) { + continue; + } + auto d = v - c - 1; + auto q = d / p; + auto r = d % p; + + std::tie(last, b) = append_0bit(last, b, q); + std::tie(last, b) = append_single_1bit(last, b); + std::tie(last, b) = append_uint32(last, b, r, logp); + + c = v; + } + + if (b != 0) { + // we already zero-filled. + ++last; + } + + return last - data; +} + +int cache_digest_hash(uint64_t &key, size_t nbits, const StringRef &s) { + int rv; + uint64_t mask = (static_cast(1) << nbits) - 1; + + std::array md; + + auto ctx = EVP_MD_CTX_create(); + + rv = EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr); + if (rv != 1) { + return -1; + } + + rv = EVP_DigestUpdate(ctx, s.c_str(), s.size()); + if (rv != 1) { + return -1; + } + + unsigned int len = md.size(); + + rv = EVP_DigestFinal_ex(ctx, md.data(), &len); + if (rv != 1) { + return -1; + } + + assert(len == 32); + + EVP_MD_CTX_destroy(ctx); + + key = (static_cast(md[24]) << 56) + + (static_cast(md[25]) << 48) + + (static_cast(md[26]) << 40) + + (static_cast(md[27]) << 32) + (md[28] << 24) + + (md[29] << 16) + (md[30] << 8) + md[31]; + + key &= mask; + + return 0; +} + +namespace { +std::pair read_uint32(uint32_t &res, size_t nbits, + const uint8_t *p, size_t b) { + if (b + nbits < 8) { + res = (*p >> (8 - b - nbits)) & ((1 << nbits) - 1); + return {p, b + nbits}; + } + + if (b + nbits == 8) { + res = *p & ((1 << nbits) - 1); + return {++p, 0}; + } + + res = *p & ((1 << (8 - b)) - 1); + + ++p; + nbits -= 8 - b; + + for (; nbits >= 8; nbits -= 8) { + res <<= 8; + res += *p++; + } + + if (nbits) { + res <<= nbits; + res += *p >> (8 - nbits); + } + + return {p, nbits}; +} +} // namespace + +namespace { +size_t leading_zero(uint8_t c) { + for (size_t i = 0; i < 8; ++i) { + if (c & (1 << (7 - i))) { + return i; + } + } + + return 8; +} +} // namespace + +namespace { +std::pair +read_until_1bit(uint32_t &res, const uint8_t *p, size_t b, const uint8_t *end) { + uint8_t mask = (1 << (8 - b)) - 1; + + if (*p & mask) { + res = leading_zero(*p & mask) - b; + b += res + 1; + if (b == 8) { + return {++p, 0}; + } + return {p, b}; + } + + res = 8 - b; + + ++p; + + for (; p != end; ++p, res += 8) { + if (!*p) { + continue; + } + + auto nlz = leading_zero(*p); + + res += nlz; + b = nlz + 1; + + if (b == 8) { + return {++p, 0}; + } + return {p, b}; + } + + return {end, 0}; +} +} // namespace + +int cache_digest_decode(std::vector &keys, uint32_t &logn, + uint32_t &logp, const uint8_t *data, size_t datalen) { + auto last = data; + size_t b = 0; + + auto end = data + datalen; + + if ((end - data) * 8 < 10) { + return -1; + } + + keys.resize(0); + + logn = 0; + logp = 0; + + std::tie(last, b) = read_uint32(logn, 5, last, b); + std::tie(last, b) = read_uint32(logp, 5, last, b); + + uint32_t n = 1, p = 1; + + for (auto i = 0; i < logn; n *= 2, ++i) + ; + + for (auto i = 0; i < logp; p *= 2, ++i) + ; + + uint64_t c = std::numeric_limits::max(); + + for (;;) { + uint32_t q, r; + + auto may_end = end - last == 1 && b > 0; + std::tie(last, b) = read_until_1bit(q, last, b, end); + + if (last == end) { + if (may_end) { + return 0; + } + + return -1; + } + + if ((end - last) * 8 < b + logp) { + return -1; + } + + std::tie(last, b) = read_uint32(r, logp, last, b); + + auto d = static_cast(q) * p + r; + + c += d + 1; + + keys.push_back(c); + + if (last == end) { + return 0; + } + } +} + +} // namespace nghttp2 diff --git a/src/cache_digest.h b/src/cache_digest.h new file mode 100644 index 00000000..4999d808 --- /dev/null +++ b/src/cache_digest.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 CACHE_DIGEST_H +#define CACHE_DIGEST_H + +#include "nghttp2_config.h" + +#include +#include + +#include "template.h" + +namespace nghttp2 { + +ssize_t cache_digest_encode(uint8_t *data, size_t datalen, + const std::vector &uris, + uint32_t logp); + +int cache_digest_decode(std::vector &keys, uint32_t &logn, + uint32_t &logp, const uint8_t *data, size_t datalen); + +int cache_digest_hash(uint64_t &key, size_t nbits, const StringRef &s); + +} // namespace nghttp2 + +#endif // CACHE_DIGEST_H diff --git a/src/cache_digest_test.cc b/src/cache_digest_test.cc new file mode 100644 index 00000000..5b3e30b7 --- /dev/null +++ b/src/cache_digest_test.cc @@ -0,0 +1,76 @@ +/* + * 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 "cache_digest_test.h" + +#include +#include + +#include + +#include "cache_digest.h" +#include "template.h" + +namespace nghttp2 { + +void test_cache_digest_encode_decode(void) { + int rv; + + auto uris = std::vector{"https://nghttp2.org/foo", + "https://nghttp2.org/bar", + "https://nghttp2.org/buzz"}; + + auto pbits = 31; + std::array cdbuf; + auto cdlen = cache_digest_encode(cdbuf.data(), cdbuf.size(), uris, pbits); + + std::vector keys; + uint32_t logn, logp; + + rv = cache_digest_decode(keys, logn, logp, cdbuf.data(), cdlen); + + CU_ASSERT(0 == rv); + + auto query_keys = std::vector(uris.size()); + for (size_t i = 0; i < uris.size(); ++i) { + auto &uri = uris[i]; + + uint64_t key; + + rv = cache_digest_hash(key, logn + logp, StringRef{uri}); + + CU_ASSERT(0 == rv); + + query_keys[i] = key; + } + + CU_ASSERT( + std::binary_search(std::begin(keys), std::end(keys), query_keys[0])); + CU_ASSERT( + std::binary_search(std::begin(keys), std::end(keys), query_keys[1])); + CU_ASSERT( + std::binary_search(std::begin(keys), std::end(keys), query_keys[2])); +} + +} // namespace nghttp2 diff --git a/src/cache_digest_test.h b/src/cache_digest_test.h new file mode 100644 index 00000000..5801bf73 --- /dev/null +++ b/src/cache_digest_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 CACHE_DIGEST_TEST_H +#define CACHE_DIGEST_TEST_H + +#ifdef HAVE_CONFIG_H +#include +#endif // HAVE_CONFIG_H + +namespace nghttp2 { + +void test_cache_digest_encode_decode(void); + +} // namespace nghttp2 + +#endif // CACHE_DIGEST_TEST_H diff --git a/src/nghttp.cc b/src/nghttp.cc index c78c10bf..cf66d09f 100644 --- a/src/nghttp.cc +++ b/src/nghttp.cc @@ -59,6 +59,7 @@ #include "base64.h" #include "ssl.h" #include "template.h" +#include "cache_digest.h" #ifndef O_BINARY #define O_BINARY (0) @@ -98,6 +99,7 @@ Config::Config() padding(0), max_concurrent_streams(100), peer_max_concurrent_streams(100), + cache_digest_bits(7), weight(NGHTTP2_DEFAULT_WEIGHT), multiply(1), timeout(0.), @@ -544,7 +546,8 @@ HttpClient::HttpClient(const nghttp2_session_callbacks *callbacks, state(ClientState::IDLE), upgrade_response_status_code(0), fd(-1), - upgrade_response_complete(false) { + upgrade_response_complete(false), + cache_digest_sent(false) { ev_io_init(&wev, writecb, 0, EV_WRITE); ev_io_init(&rev, readcb, 0, EV_READ); @@ -1997,6 +2000,8 @@ int before_frame_send_callback(nghttp2_session *session, namespace { int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) { + int rv; + if (config.verbose) { verbose_on_frame_send_callback(session, frame, user_data); } @@ -2017,6 +2022,19 @@ int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, req->continue_timer->start(); } + auto client = get_client(user_data); + + if (!client->cache_digest_sent && !config.cache_digest_uris.empty()) { + client->cache_digest_sent = true; + + rv = nghttp2_submit_extension(session, NGHTTP2_DRAFT_CACHE_DIGEST, + NGHTTP2_FLAG_NONE, frame->hd.stream_id, NULL); + + if (nghttp2_is_fatal(rv)) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + } + return 0; } } // namespace @@ -2363,6 +2381,35 @@ ssize_t file_read_callback(nghttp2_session *session, int32_t stream_id, } } // namespace +namespace { +ssize_t pack_cache_digest_frame(nghttp2_session *session, uint8_t *buf, + size_t len, const nghttp2_frame *frame, + void *user_data) { + ssize_t encodedlen; + + encodedlen = cache_digest_encode(buf, len, config.cache_digest_uris, + config.cache_digest_bits); + + if (encodedlen == -1) { + return NGHTTP2_ERR_CANCEL; + } + + return encodedlen; +} +} // namespace + +namespace { +ssize_t pack_extension_callback(nghttp2_session *session, uint8_t *buf, + size_t len, const nghttp2_frame *frame, + void *user_data) { + if (frame->hd.type != NGHTTP2_DRAFT_CACHE_DIGEST) { + return NGHTTP2_ERR_CANCEL; + } + + return pack_cache_digest_frame(session, buf, len, frame, user_data); +} +} // namespace + namespace { int run(char **uris, int n) { nghttp2_session_callbacks *callbacks; @@ -2407,6 +2454,9 @@ int run(char **uris, int n) { callbacks, select_padding_callback); } + nghttp2_session_callbacks_set_pack_extension_callback( + callbacks, pack_extension_callback); + std::string prev_scheme; std::string prev_host; uint16_t prev_port = 0; @@ -2632,6 +2682,13 @@ Options: (up to a short timeout) until the server sends a 100 Continue interim response. This option is ignored unless combined with the -d option. + -C, --cache-digest-uri= + Add to cache digest. Use this option multiple + times to add more than 1 URI to cache digest. + --cache-digest-bits= + Set the number of bits to specify the probability of a + false positive that is acceptable, expressed as "1/". + Default: 7 --version Display version information and exit. -h, --help Display this help and exit. @@ -2672,6 +2729,7 @@ int main(int argc, char **argv) { {"header-table-size", required_argument, nullptr, 'c'}, {"padding", required_argument, nullptr, 'b'}, {"har", required_argument, nullptr, 'r'}, + {"cache-digest-uri", required_argument, nullptr, 'C'}, {"cert", required_argument, &flag, 1}, {"key", required_argument, &flag, 2}, {"color", no_argument, &flag, 3}, @@ -2684,14 +2742,18 @@ int main(int argc, char **argv) { {"no-push", no_argument, &flag, 11}, {"max-concurrent-streams", required_argument, &flag, 12}, {"expect-continue", no_argument, &flag, 13}, + {"cache-digest-bits", required_argument, &flag, 14}, {nullptr, 0, nullptr, 0}}; int option_index = 0; - int c = getopt_long(argc, argv, "M:Oab:c:d:gm:np:r:hH:vst:uw:W:", + int c = getopt_long(argc, argv, "C:M:Oab:c:d:gm:np:r:hH:vst:uw:W:", long_options, &option_index); if (c == -1) { break; } switch (c) { + case 'C': + config.cache_digest_uris.push_back(optarg); + break; case 'M': // peer-max-concurrent-streams option config.peer_max_concurrent_streams = strtoul(optarg, nullptr, 10); @@ -2885,6 +2947,18 @@ int main(int argc, char **argv) { // expect-continue option config.expect_continue = true; break; + case 14: { + // cache-digest-bits + auto n = strtoul(optarg, nullptr, 10); + if (n <= 0 || n >= 32) { + std::cerr + << "--cache-digest-bits: specify in the range [1, 31], inclusive" + << std::endl; + exit(EXIT_FAILURE); + } + config.cache_digest_bits = n; + break; + } } break; default: diff --git a/src/nghttp.h b/src/nghttp.h index 488d46f6..32958210 100644 --- a/src/nghttp.h +++ b/src/nghttp.h @@ -64,6 +64,7 @@ struct Config { Headers headers; Headers trailer; + std::vector cache_digest_uris; std::string certfile; std::string keyfile; std::string datafile; @@ -74,6 +75,7 @@ struct Config { size_t padding; size_t max_concurrent_streams; ssize_t peer_max_concurrent_streams; + uint32_t cache_digest_bits; int32_t weight; int multiply; // milliseconds @@ -283,6 +285,8 @@ struct HttpClient { // 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; + // true if cache digest was sent or there is no need to send it. + bool cache_digest_sent; // SETTINGS payload sent as token68 in HTTP Upgrade std::array settings_payload; diff --git a/src/shrpx-unittest.cc b/src/shrpx-unittest.cc index f9acf880..3b38a794 100644 --- a/src/shrpx-unittest.cc +++ b/src/shrpx-unittest.cc @@ -45,6 +45,7 @@ #include "shrpx_config.h" #include "ssl.h" #include "shrpx_router_test.h" +#include "cache_digest_test.h" static int init_suite1(void) { return 0; } @@ -198,7 +199,9 @@ int main(int argc, char *argv[]) { !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_add_test(pSuite, "base64_decode", nghttp2::test_base64_decode) || + !CU_add_test(pSuite, "cache_digest_encode_decode", + nghttp2::test_cache_digest_encode_decode)) { CU_cleanup_registry(); return CU_get_error(); }