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:
parent
4dea318b5b
commit
c55d7343ca
|
@ -21,6 +21,9 @@ HEADERS = [
|
|||
"content-length",
|
||||
"location",
|
||||
"trailer",
|
||||
"referer",
|
||||
"link",
|
||||
"accept",
|
||||
# disallowed h1 headers
|
||||
'connection',
|
||||
'keep-alive',
|
||||
|
|
335
src/http2.cc
335
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 <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
|
||||
|
|
24
src/http2.h
24
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<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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue