From c55d7343cac0c7e856abb89e8036d47233dff2c4 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 7 Feb 2015 16:09:49 +0900 Subject: [PATCH] nghttpx: Support server push using Link header field nghttpx server push is initiated by looking for Link header field from backend server response. Currently we only enable server push for HTTP/1 backend and without HTTP/2 proxy mode. The URIs which have rel=preload are eligible to resource to be pushed. --- genheaderfunc.py | 3 + src/http2.cc | 335 ++++++++++++++++++++++++++++++++++ src/http2.h | 24 +++ src/http2_test.cc | 355 ++++++++++++++++++++++++++++++++++++ src/http2_test.h | 2 + src/shrpx-unittest.cc | 3 + src/shrpx_http2_upstream.cc | 179 ++++++++++++++++++ src/shrpx_http2_upstream.h | 3 + src/util.cc | 6 + src/util.h | 2 + 10 files changed, 912 insertions(+) diff --git a/genheaderfunc.py b/genheaderfunc.py index c4fba627..079896ea 100755 --- a/genheaderfunc.py +++ b/genheaderfunc.py @@ -21,6 +21,9 @@ HEADERS = [ "content-length", "location", "trailer", + "referer", + "link", + "accept", # disallowed h1 headers 'connection', 'keep-alive', diff --git a/src/http2.cc b/src/http2.cc index a9a57dd2..d8da3cf9 100644 --- a/src/http2.cc +++ b/src/http2.cc @@ -438,6 +438,11 @@ int lookup_token(const uint8_t *name, size_t namelen) { break; case 4: switch (name[namelen - 1]) { + case 'k': + if (util::streq("lin", name, 3)) { + return HD_LINK; + } + break; case 't': if (util::streq("hos", name, 3)) { return HD_HOST; @@ -472,6 +477,9 @@ int lookup_token(const uint8_t *name, size_t namelen) { } break; case 't': + if (util::streq("accep", name, 5)) { + return HD_ACCEPT; + } if (util::streq("expec", name, 5)) { return HD_EXPECT; } @@ -499,6 +507,9 @@ int lookup_token(const uint8_t *name, size_t namelen) { } break; case 'r': + if (util::streq("refere", name, 6)) { + return HD_REFERER; + } if (util::streq("traile", name, 6)) { return HD_TRAILER; } @@ -668,6 +679,330 @@ const Headers::value_type *get_header(const HeaderIndex &hdidx, int token, return &nva[i]; } +namespace { +template 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 +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 { +std::pair +parse_next_link_header_once(const char *first, const char *last) { + first = skip_to_next_field(first, last); + if (first == last || *first != '<') { + return {{{0, 0}}, last}; + } + auto url_first = ++first; + first = std::find(first, last, '>'); + if (first == last) { + return {{{0, 0}}, first}; + } + auto url_last = first++; + if (first == last) { + return {{{0, 0}}, first}; + } + // we expect ';' or ',' here + switch (*first) { + case ',': + return {{{0, 0}}, ++first}; + case ';': + ++first; + break; + default: + return {{{0, 0}}, last}; + } + + auto ok = false; + for (;;) { + first = skip_lws(first, last); + if (first == last) { + return {{{0, 0}}, first}; + } + // we expect link-param + + // we are only interested in rel=preload parameter. Others are + // simply skipped. + static const char PL[] = "rel=preload"; + if (last - first >= sizeof(PL) - 1) { + if (memcmp(PL, first, sizeof(PL) - 1) == 0) { + if (first + sizeof(PL) - 1 == last) { + ok = true; + // this is the end of sequence + return {{{url_first, url_last}}, last}; + } + switch (*(first + sizeof(PL) - 1)) { + case ',': + ok = true; + // skip including ',' + first += sizeof(PL); + return {{{url_first, url_last}}, first}; + case ';': + ok = true; + // skip including ';' + first += sizeof(PL); + // continue parse next link-param + continue; + } + } + } + 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 {{{0, 0}}, last}; + } + if (param_first == first) { + // empty parmname + return {{{0, 0}}, 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 lin-param value + assert(*first == '='); + ++first; + if (first == last) { + // empty value is not acceptable + return {{{0, 0}}, first}; + } + if (*first == '"') { + // quoted-string + first = std::find(first + 1, last, '"'); + if (first == last) { + return {{{0, 0}}, first}; + } + ++first; + if (first == last || *first == ',') { + goto almost_done; + } + if (*first == ';') { + ++first; + // parse next link-param + continue; + } + return {{{0, 0}}, last}; + } + // not quoted-string, skip to next ',' or ';' + if (*first == ',' || *first == ';') { + // empty value + return {{{0, 0}}, 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 == ',') { + ++first; + } + if (ok) { + return {{{url_first, url_last}}, first}; + } + return {{{0, 0}}, first}; +} +} // namespace + +std::vector parse_link_header(const char *src, size_t len) { + auto first = src; + auto last = src + len; + std::vector res; + for (; first != last;) { + auto rv = parse_next_link_header_once(first, last); + first = rv.second; + if (rv.first.url.first != 0 || rv.first.url.second != 0) { + res.push_back(rv.first); + } + } + return res; +} + +namespace { +void eat_file(std::string &path) { + if (path.empty()) { + path = "/"; + return; + } + auto p = path.size() - 1; + if (path[p] == '/') { + return; + } + p = path.rfind('/', p); + if (p == std::string::npos) { + // this should not happend in normal case, where we expect path + // starts with '/' + path = "/"; + return; + } + path.erase(std::begin(path) + p + 1, std::end(path)); +} +} // namespace + +namespace { +void eat_dir(std::string &path) { + if (path.empty()) { + path = "/"; + return; + } + auto p = path.size() - 1; + if (path[p] != '/') { + p = path.rfind('/', p); + if (p == std::string::npos) { + // this should not happend in normal case, where we expect path + // starts with '/' + path = "/"; + return; + } + } + if (path[p] == '/') { + if (p == 0) { + return; + } + --p; + } + p = path.rfind('/', p); + if (p == std::string::npos) { + // this should not happend in normal case, where we expect path + // starts with '/' + path = "/"; + return; + } + path.erase(std::begin(path) + p + 1, std::end(path)); +} +} // namespace + +std::string path_join(const char *base_path, size_t base_pathlen, + const char *base_query, size_t base_querylen, + const char *rel_path, size_t rel_pathlen, + const char *rel_query, size_t rel_querylen) { + std::string res; + if (rel_pathlen == 0) { + if (base_pathlen == 0) { + res = "/"; + } else { + res.assign(base_path, base_pathlen); + } + if (rel_querylen == 0) { + if (base_querylen) { + res += "?"; + res.append(base_query, base_querylen); + } + return res; + } + res += "?"; + res.append(rel_query, rel_querylen); + return res; + } + + auto first = rel_path; + auto last = rel_path + rel_pathlen; + + if (rel_path[0] == '/') { + res = "/"; + ++first; + } else if (base_pathlen == 0) { + res = "/"; + } else { + res.assign(base_path, base_pathlen); + } + + for (; first != last;) { + if (*first == '.') { + if (first + 1 == last) { + break; + } + if (*(first + 1) == '/') { + first += 2; + continue; + } + if (*(first + 1) == '.') { + if (first + 2 == last) { + eat_dir(res); + break; + } + if (*(first + 2) == '/') { + eat_dir(res); + first += 3; + continue; + } + } + } + if (res.back() != '/') { + eat_file(res); + } + auto slash = std::find(first, last, '/'); + if (slash == last) { + res.append(first, last); + break; + } + res.append(first, slash + 1); + first = slash + 1; + for (; first != last && *first == '/'; ++first) + ; + } + if (rel_querylen) { + res += "?"; + res.append(rel_query, rel_querylen); + } + return res; +} + } // namespace http2 } // namespace nghttp2 diff --git a/src/http2.h b/src/http2.h index d93bd0ac..b704cf32 100644 --- a/src/http2.h +++ b/src/http2.h @@ -189,6 +189,7 @@ enum { HD__PATH, HD__SCHEME, HD__STATUS, + HD_ACCEPT, HD_ALT_SVC, HD_CONNECTION, HD_CONTENT_LENGTH, @@ -198,8 +199,10 @@ enum { HD_HTTP2_SETTINGS, HD_IF_MODIFIED_SINCE, HD_KEEP_ALIVE, + HD_LINK, HD_LOCATION, HD_PROXY_CONNECTION, + HD_REFERER, HD_SERVER, HD_TE, HD_TRAILER, @@ -247,6 +250,27 @@ bool http2_mandatory_request_headers_presence(const HeaderIndex &hdidx); const Headers::value_type *get_header(const HeaderIndex &hdidx, int token, const Headers &nva); +struct LinkHeader { + std::pair url; +}; + +// Returns next URI-reference in Link header field value |src| of +// length |len|. 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 parse_link_header(const char *src, size_t len); + +// Constructs path by combining base path |base_path| of length +// |base_pathlen| with another path |rel_path| of length +// |rel_pathlen|. The base path and another path can have optional +// query component. This function assumes |base_path| is +// cannibalized. In other words, it does not contain ".." or "." path +// components and starts with "/" if it is not empty. +std::string path_join(const char *base_path, size_t base_pathlen, + const char *base_query, size_t base_querylen, + const char *rel_path, size_t rel_pathlen, + const char *rel_query, size_t rel_querylen); + } // namespace http2 } // namespace nghttp2 diff --git a/src/http2_test.cc b/src/http2_test.cc index 871cc392..0216152b 100644 --- a/src/http2_test.cc +++ b/src/http2_test.cc @@ -290,4 +290,359 @@ void test_http2_mandatory_request_headers_presence(void) { CU_ASSERT(http2::http2_mandatory_request_headers_presence(hdidx)); } +void test_http2_parse_link_header(void) { + { + // only URI appears; we don't extract URI unless it bears rel=preload + const char s[] = ""; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(0 == res.size()); + } + { + // URI url should be extracted + const char s[] = "; rel=preload"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + } + { + // With extra link-param. URI url should be extracted + const char s[] = "; rel=preload; as=file"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + } + { + // With extra link-param. URI url should be extracted + const char s[] = "; as=file; rel=preload"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + } + { + // With extra link-param and quote-string. URI url should be + // extracted + const char s[] = R"(; rel=preload; title="foo,bar")"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + } + { + // With extra link-param and quote-string. URI url should be + // extracted + const char s[] = R"(; title="foo,bar"; rel=preload)"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + } + { + // ',' after quote-string + const char s[] = R"(; title="foo,bar", ; rel=preload)"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[25], &s[28]) == res[0].url); + } + { + // Only first URI should be extracted. + const char s[] = "; rel=preload, "; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + } + { + // Both have rel=preload, so both urls should be extracted + const char s[] = "; rel=preload, ; rel=preload"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(2 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + CU_ASSERT(std::make_pair(&s[21], &s[24]) == res[1].url); + } + { + // Second URI uri should be extracted. + const char s[] = ", ;rel=preload"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[8], &s[11]) == res[0].url); + } + { + // Error if input ends with ';' + const char s[] = ";rel=preload;"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(0 == res.size()); + } + { + // OK if input ends with ',' + const char s[] = ";rel=preload,"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[1], &s[4]) == res[0].url); + } + { + // Multiple repeated ','s between fields is OK + const char s[] = ",,,;rel=preload"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[9], &s[12]) == res[0].url); + } + { + // Error if url is not enclosed by <> + const char s[] = "url>;rel=preload;"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(0 == res.size()); + } + { + // Error if url is not enclosed by <> + const char s[] = "' is not followed by ';' + const char s[] = " rel=preload"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(0 == res.size()); + } + { + // Starting with whitespace is no problem. + const char s[] = " ; rel=preload"; + auto res = http2::parse_link_header(s, sizeof(s) - 1); + CU_ASSERT(1 == res.size()); + CU_ASSERT(std::make_pair(&s[3], &s[6]) == res[0].url); + } +} + +void test_http2_path_join(void) { + { + const char base[] = "/"; + const char rel[] = "/"; + CU_ASSERT("/" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + const char base[] = "/"; + const char rel[] = "/alpha"; + CU_ASSERT("/alpha" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, + rel, sizeof(rel) - 1, nullptr, 0)); + } + { + // rel ends with trailing '/' + const char base[] = "/"; + const char rel[] = "/alpha/"; + CU_ASSERT("/alpha/" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, + rel, sizeof(rel) - 1, nullptr, 0)); + } + { + // rel contains multiple components + const char base[] = "/"; + const char rel[] = "/alpha/bravo"; + CU_ASSERT("/alpha/bravo" == http2::path_join(base, sizeof(base) - 1, + nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // rel is relative + const char base[] = "/"; + const char rel[] = "alpha/bravo"; + CU_ASSERT("/alpha/bravo" == http2::path_join(base, sizeof(base) - 1, + nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // rel is relative + const char base[] = "/alpha"; + const char rel[] = "bravo/charlie"; + CU_ASSERT("/bravo/charlie" == + http2::path_join(base, sizeof(base) - 1, nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // rel contains repeated '/'s + const char base[] = "/"; + const char rel[] = "/alpha/////bravo/////"; + CU_ASSERT("/alpha/bravo/" == http2::path_join(base, sizeof(base) - 1, + nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // base ends with '/', so '..' eats 'bravo' + const char base[] = "/alpha/bravo/"; + const char rel[] = "../charlie/delta"; + CU_ASSERT("/alpha/charlie/delta" == + http2::path_join(base, sizeof(base) - 1, nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // base does not end with '/', so '..' eats 'alpha/bravo' + const char base[] = "/alpha/bravo"; + const char rel[] = "../charlie"; + CU_ASSERT("/charlie" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, + rel, sizeof(rel) - 1, nullptr, 0)); + } + { + // 'charlie' is eaten by following '..' + const char base[] = "/alpha/bravo/"; + const char rel[] = "../charlie/../delta"; + CU_ASSERT("/alpha/delta" == http2::path_join(base, sizeof(base) - 1, + nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // excessive '..' results in '/' + const char base[] = "/alpha/bravo/"; + const char rel[] = "../../../"; + CU_ASSERT("/" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // excessive '..' and path component + const char base[] = "/alpha/bravo/"; + const char rel[] = "../../../charlie"; + CU_ASSERT("/charlie" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, + rel, sizeof(rel) - 1, nullptr, 0)); + } + { + // rel ends with '..' + const char base[] = "/alpha/bravo/"; + const char rel[] = "charlie/.."; + CU_ASSERT("/alpha/bravo/" == http2::path_join(base, sizeof(base) - 1, + nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // base empty and rel contains '..' + const char base[] = ""; + const char rel[] = "charlie/.."; + CU_ASSERT("/" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // '.' is ignored + const char base[] = "/"; + const char rel[] = "charlie/././././delta"; + CU_ASSERT("/charlie/delta" == + http2::path_join(base, sizeof(base) - 1, nullptr, 0, rel, + sizeof(rel) - 1, nullptr, 0)); + } + { + // '.' is ignored + const char base[] = "/"; + const char rel[] = "charlie/."; + CU_ASSERT("/charlie/" == http2::path_join(base, sizeof(base) - 1, nullptr, + 0, rel, sizeof(rel) - 1, nullptr, + 0)); + } + { + // query + const char base[] = "/"; + const char rel[] = "/"; + const char relq[] = "q"; + CU_ASSERT("/?q" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, rel, + sizeof(rel) - 1, relq, + sizeof(relq) - 1)); + } + { + // empty rel and query + const char base[] = "/alpha"; + const char rel[] = ""; + const char relq[] = "q"; + CU_ASSERT("/alpha?q" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, + rel, sizeof(rel) - 1, relq, + sizeof(relq) - 1)); + } + { + // both rel and query are empty + const char base[] = "/alpha"; + const char baseq[] = "r"; + const char rel[] = ""; + const char relq[] = ""; + CU_ASSERT("/alpha?r" == + http2::path_join(base, sizeof(base) - 1, baseq, sizeof(baseq) - 1, + rel, sizeof(rel) - 1, relq, sizeof(relq) - 1)); + } + { + // empty base + const char base[] = ""; + const char rel[] = "/alpha"; + CU_ASSERT("/alpha" == http2::path_join(base, sizeof(base) - 1, nullptr, 0, + rel, sizeof(rel) - 1, nullptr, 0)); + } + { + // everything is empty + CU_ASSERT("/" == + http2::path_join(nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0)); + } + { + // only baseq is not empty + const char base[] = ""; + const char baseq[] = "r"; + const char rel[] = ""; + CU_ASSERT("/?r" == http2::path_join(base, sizeof(base) - 1, baseq, + sizeof(baseq) - 1, rel, sizeof(rel) - 1, + nullptr, 0)); + } +} + } // namespace shrpx diff --git a/src/http2_test.h b/src/http2_test.h index f8127dc7..1171b926 100644 --- a/src/http2_test.h +++ b/src/http2_test.h @@ -39,6 +39,8 @@ void test_http2_lookup_token(void); void test_http2_check_http2_pseudo_header(void); void test_http2_http2_header_allowed(void); void test_http2_mandatory_request_headers_presence(void); +void test_http2_parse_link_header(void); +void test_http2_path_join(void); } // namespace shrpx diff --git a/src/shrpx-unittest.cc b/src/shrpx-unittest.cc index bb6fd05f..7e3e901a 100644 --- a/src/shrpx-unittest.cc +++ b/src/shrpx-unittest.cc @@ -93,6 +93,9 @@ int main(int argc, char *argv[]) { shrpx::test_http2_http2_header_allowed) || !CU_add_test(pSuite, "http2_mandatory_request_headers_presence", shrpx::test_http2_mandatory_request_headers_presence) || + !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, "downstream_index_request_headers", shrpx::test_downstream_index_request_headers) || !CU_add_test(pSuite, "downstream_index_response_headers", diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc index 88828285..218daaba 100644 --- a/src/shrpx_http2_upstream.cc +++ b/src/shrpx_http2_upstream.cc @@ -540,6 +540,48 @@ int on_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, upstream->start_settings_timer(); } break; + case NGHTTP2_PUSH_PROMISE: { + auto downstream = make_unique( + upstream, frame->push_promise.promised_stream_id, 0); + + downstream->disable_upstream_rtimer(); + + downstream->set_request_major(2); + downstream->set_request_minor(0); + + for (size_t i = 0; i < frame->push_promise.nvlen; ++i) { + auto &nv = frame->push_promise.nva[i]; + auto token = http2::lookup_token(nv.name, nv.namelen); + switch (token) { + case http2::HD__METHOD: + downstream->set_request_method({nv.value, nv.value + nv.valuelen}); + break; + case http2::HD__SCHEME: + downstream->set_request_http2_scheme( + {nv.value, nv.value + nv.valuelen}); + break; + case http2::HD__AUTHORITY: + downstream->set_request_http2_authority( + {nv.value, nv.value + nv.valuelen}); + break; + case http2::HD__PATH: + downstream->set_request_path({nv.value, nv.value + nv.valuelen}); + break; + } + } + + downstream->inspect_http2_request(); + + downstream->set_request_state(Downstream::MSG_COMPLETE); + + // a bit weird but start_downstream() expects that given + // downstream is in pending queue. + auto ptr = downstream.get(); + upstream->add_pending_downstream(std::move(downstream)); + upstream->start_downstream(ptr); + + break; + } case NGHTTP2_GOAWAY: if (LOG_ENABLED(INFO)) { auto debug_data = util::ascii_dump(frame->goaway.opaque_data, @@ -1283,6 +1325,18 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { return -1; } + if (get_config()->downstream_proto == PROTO_HTTP && + (downstream->get_stream_id() % 2) && + downstream->get_response_header(http2::HD_LINK) && + downstream->get_response_http_status() == 200 && + (downstream->get_request_method() == "GET" || + downstream->get_request_method() == "POST")) { + + if (prepare_push_promise(downstream) != 0) { + return -1; + } + } + return 0; } @@ -1447,4 +1501,129 @@ int Http2Upstream::on_downstream_reset(bool no_retry) { MemchunkPool *Http2Upstream::get_mcpool() { return &mcpool_; } +int Http2Upstream::prepare_push_promise(Downstream *downstream) { + int rv; + http_parser_url u; + memset(&u, 0, sizeof(u)); + rv = http_parser_parse_url(downstream->get_request_path().c_str(), + downstream->get_request_path().size(), 0, &u); + if (rv != 0) { + return 0; + } + const char *base; + size_t baselen; + if (u.field_set & (1 << UF_PATH)) { + auto &f = u.field_data[UF_PATH]; + base = downstream->get_request_path().c_str() + f.off; + baselen = f.len; + } else { + base = "/"; + baselen = 1; + } + for (auto &kv : downstream->get_response_headers()) { + auto token = http2::lookup_token(kv.name); + if (token != http2::HD_LINK) { + continue; + } + for (auto &link : + http2::parse_link_header(kv.value.c_str(), kv.value.size())) { + auto link_url = link.url.first; + auto link_urllen = link.url.second - link.url.first; + + const char *rel; + size_t rellen; + const char *relq = nullptr; + size_t relqlen = 0; + + http_parser_url v; + memset(&v, 0, sizeof(v)); + rv = http_parser_parse_url(link_url, link_urllen, 0, &v); + if (rv != 0) { + assert(link_urllen); + if (link_url[0] == '/') { + continue; + } + // treat link_url as relative URI. + auto end = std::find(link_url, link_url + link_urllen, '#'); + auto q = std::find(link_url, end, '?'); + rel = link_url; + rellen = q - link_url; + if (q != end) { + relq = q + 1; + relqlen = end - relq; + } + } else { + if (v.field_set & (1 << UF_HOST)) { + continue; + } + if (v.field_set & (1 << UF_PATH)) { + auto &f = v.field_data[UF_PATH]; + rel = link_url + f.off; + rellen = f.len; + } else { + rel = "/"; + rellen = 1; + } + + if (v.field_set & (1 << UF_QUERY)) { + auto &f = v.field_data[UF_QUERY]; + relq = link_url + f.off; + relqlen = f.len; + } + } + auto path = http2::path_join(base, baselen, nullptr, 0, rel, rellen, relq, + relqlen); + rv = submit_push_promise(path, downstream); + if (rv != 0) { + return -1; + } + } + } + return 0; +} + +int Http2Upstream::submit_push_promise(const std::string &path, + Downstream *downstream) { + int rv; + std::vector nva; + nva.reserve(downstream->get_request_headers().size()); + for (auto &kv : downstream->get_request_headers()) { + auto token = http2::lookup_token(kv.name); + switch (token) { + case http2::HD__METHOD: + // juse use "GET" for now + nva.push_back(http2::make_nv_lc(":method", "GET")); + continue; + case http2::HD__PATH: + nva.push_back(http2::make_nv_ls(":path", path)); + continue; + case http2::HD_ACCEPT: + // browser tends to change accept header field value depending + // on requesting resource. So just omit it for now. + continue; + case http2::HD_REFERER: + // TODO construct referer + continue; + } + nva.push_back(http2::make_nv(kv.name, kv.value, kv.no_index)); + } + + rv = nghttp2_submit_push_promise(session_, NGHTTP2_FLAG_NONE, + downstream->get_stream_id(), nva.data(), + nva.size(), nullptr); + + if (rv != 0) { + if (LOG_ENABLED(INFO)) { + ULOG(INFO, this) << "nghttp2_submit_push_promise() failed: " + << nghttp2_strerror(rv); + } + if (nghttp2_is_fatal(rv)) { + return -1; + } + return 0; + } + + return 0; +} + } // namespace shrpx diff --git a/src/shrpx_http2_upstream.h b/src/shrpx_http2_upstream.h index 8e1eaf5d..1278ebc3 100644 --- a/src/shrpx_http2_upstream.h +++ b/src/shrpx_http2_upstream.h @@ -100,6 +100,9 @@ public: void submit_goaway(); void check_shutdown(); + int prepare_push_promise(Downstream *downstream); + int submit_push_promise(const std::string &path, Downstream *downstream); + private: // must be put before downstream_queue_ std::unique_ptr pre_upstream_; diff --git a/src/util.cc b/src/util.cc index 9bcf1d3a..9d133e6f 100644 --- a/src/util.cc +++ b/src/util.cc @@ -99,6 +99,12 @@ bool in_token(char c) { &extra[sizeof(extra)]; } +bool in_attr_char(char c) { + static const char bad[] = {'*', '\'', '%'}; + return util::in_token(c) && + std::find(std::begin(bad), std::end(bad) - 1, c) == std::end(bad) - 1; +} + std::string percent_encode_token(const std::string &target) { auto len = target.size(); std::string dest; diff --git a/src/util.h b/src/util.h index 0d66c931..9e42b647 100644 --- a/src/util.h +++ b/src/util.h @@ -163,6 +163,8 @@ bool inRFC3986UnreservedChars(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); + std::string percentEncode(const unsigned char *target, size_t len); std::string percentEncode(const std::string &target);