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.
This commit is contained in:
Tatsuhiro Tsujikawa 2015-02-07 16:09:49 +09:00
parent 4dea318b5b
commit c55d7343ca
10 changed files with 912 additions and 0 deletions

View File

@ -21,6 +21,9 @@ HEADERS = [
"content-length",
"location",
"trailer",
"referer",
"link",
"accept",
# disallowed h1 headers
'connection',
'keep-alive',

View File

@ -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 <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 {
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 {{{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<LinkHeader> parse_link_header(const char *src, size_t len) {
auto first = src;
auto last = src + len;
std::vector<LinkHeader> 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

View File

@ -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<const char *, const char *> 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<LinkHeader> 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

View File

@ -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[] = "<url>";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// URI url should be extracted
const char s[] = "<url>; 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[] = "<url>; 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[] = "<url>; 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"(<url>; 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"(<url>; 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"(<url>; title="foo,bar", <url>; 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[] = "<url>; rel=preload, <url>";
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[] = "<url>; rel=preload, <url>; 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[] = "<url>, <url>;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[] = "<url>;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[] = "<url>;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[] = "<url>,,,<url>;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[] = "<url;rel=preload;";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// Empty parameter value is not allowed
const char s[] = "<url>;rel=preload; as=";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// Empty parameter value is not allowed
const char s[] = "<url>;as=;rel=preload";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// Empty parameter value is not allowed
const char s[] = "<url>;as=, <url>;rel=preload";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// Empty parameter name is not allowed
const char s[] = "<url>; =file; rel=preload";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// Without whitespaces
const char s[] = "<url>;as=file;rel=preload,<url>;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[27], &s[30]) == res[1].url);
}
{
// link-extension may have no value
const char s[] = "<url>; as; 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);
}
{
// ext-name-star
const char s[] = "<url>; 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);
}
{
// '*' is not allowed expect for trailing one
const char s[] = "<url>; *=bar; rel=preload";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// '*' is not allowed expect for trailing one
const char s[] = "<url>; foo*bar=buzz; rel=preload";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// ext-name-star must be followed by '='
const char s[] = "<url>; foo*; rel=preload";
auto res = http2::parse_link_header(s, sizeof(s) - 1);
CU_ASSERT(0 == res.size());
}
{
// '>' is not followed by ';'
const char s[] = "<url> 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[] = " <url>; 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

View File

@ -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

View File

@ -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",

View File

@ -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<Downstream>(
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<nghttp2_nv> 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

View File

@ -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<HttpsUpstream> pre_upstream_;

View File

@ -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;

View File

@ -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);