diff --git a/src/app_helper.cc b/src/app_helper.cc index 3298298e..de895001 100644 --- a/src/app_helper.cc +++ b/src/app_helper.cc @@ -125,6 +125,8 @@ const char* strframetype(uint8_t type) return "GOAWAY"; case NGHTTP2_WINDOW_UPDATE: return "WINDOW_UPDATE"; + case NGHTTP2_ALTSVC: + return "ALTSVC"; default: return "UNKNOWN"; } @@ -458,6 +460,25 @@ void print_frame(print_type ptype, const nghttp2_frame *frame) fprintf(outfile, "(window_size_increment=%d)\n", frame->window_update.window_size_increment); break; + case NGHTTP2_ALTSVC: + print_frame_attr_indent(); + fprintf(outfile, "(max-age=%u, port=%u, protocol_id=", + frame->altsvc.max_age, frame->altsvc.port); + if(frame->altsvc.protocol_id_len) { + fwrite(frame->altsvc.protocol_id, frame->altsvc.protocol_id_len, 1, + outfile); + } + fprintf(outfile, ", host="); + if(frame->altsvc.host_len) { + fwrite(frame->altsvc.host, frame->altsvc.host_len, 1, outfile); + } + fprintf(outfile, ", origin="); + if(frame->altsvc.origin_len) { + fwrite(frame->altsvc.origin, frame->altsvc.origin_len, 1, outfile); + } + fprintf(outfile, ")\n"); + + break; default: fprintf(outfile, "\n"); break; diff --git a/src/shrpx-unittest.cc b/src/shrpx-unittest.cc index f9d666cc..d35e9105 100644 --- a/src/shrpx-unittest.cc +++ b/src/shrpx-unittest.cc @@ -112,7 +112,9 @@ int main(int argc, char* argv[]) !CU_add_test(pSuite, "util_inp_strlower", shrpx::test_util_inp_strlower) || !CU_add_test(pSuite, "util_to_base64", - shrpx::test_util_to_base64)) { + shrpx::test_util_to_base64) || + !CU_add_test(pSuite, "util_percent_encode_token", + shrpx::test_util_percent_encode_token)) { CU_cleanup_registry(); return CU_get_error(); } diff --git a/src/shrpx.cc b/src/shrpx.cc index f92ee315..72946ed9 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -443,6 +443,14 @@ void fill_default_config() mod_config()->http2_no_cookie_crumbling = false; mod_config()->upstream_frame_debug = false; mod_config()->padding = 0; + + mod_config()->altsvc_port = 0; + mod_config()->altsvc_protocol_id = nullptr; + mod_config()->altsvc_protocol_id_len = 0; + mod_config()->altsvc_host = nullptr; + mod_config()->altsvc_host_len = 0; + mod_config()->altsvc_origin = nullptr; + mod_config()->altsvc_origin_len = 0; } } // namespace @@ -749,6 +757,19 @@ Misc: downstream request. --no-via Don't append to Via header field. If Via header field is received, it is left unaltered. + --altsvc-port= + Port number of alternative service advertised in + alt-svc header field or HTTP/2 ALTSVC frame. + --altsvc-protocol-id= + ALPN protocol identifier of alternative service + advertised in alt-svc header field or HTTP/2 + ALTSVC frame. + --altsvc-host= + Host name that alternative service is available + upon, which is advertised in HTTP/2 ALTSVC frame. + --altsvc-origin= + Origin that alternative service is applicable to, + which is advertised in HTTP/2 ALTSVC frame. --frontend-http2-dump-request-header= Dumps request headers received by HTTP/2 frontend to the file denoted in . The output is @@ -855,6 +876,10 @@ int main(int argc, char **argv) {"worker-read-burst", required_argument, &flag, 51}, {"worker-write-rate", required_argument, &flag, 52}, {"worker-write-burst", required_argument, &flag, 53}, + {"altsvc-port", required_argument, &flag, 54}, + {"altsvc-protocol-id", required_argument, &flag, 55}, + {"altsvc-host", required_argument, &flag, 56}, + {"altsvc-origin", required_argument, &flag, 57}, {nullptr, 0, nullptr, 0 } }; @@ -1111,6 +1136,22 @@ int main(int argc, char **argv) // --worker-write-burst cmdcfgs.emplace_back(SHRPX_OPT_WORKER_WRITE_BURST, optarg); break; + case 54: + // --altsvc-port + cmdcfgs.emplace_back(SHRPX_OPT_ALTSVC_PORT, optarg); + break; + case 55: + // --altsvc-protocol-id + cmdcfgs.emplace_back(SHRPX_OPT_ALTSVC_PROTOCOL_ID, optarg); + break; + case 56: + // --altsvc-host + cmdcfgs.emplace_back(SHRPX_OPT_ALTSVC_HOST, optarg); + break; + case 57: + // --altsvc-origin + cmdcfgs.emplace_back(SHRPX_OPT_ALTSVC_ORIGIN, optarg); + break; default: break; } diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index 98acddf1..54a73d52 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -121,6 +121,10 @@ const char SHRPX_OPT_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER[] = const char SHRPX_OPT_HTTP2_NO_COOKIE_CRUMBLING[] = "http2-no-cookie-crumbling"; const char SHRPX_OPT_FRONTEND_FRAME_DEBUG[] = "frontend-frame-debug"; const char SHRPX_OPT_PADDING[] = "padding"; +const char SHRPX_OPT_ALTSVC_PORT[] = "altsvc-port"; +const char SHRPX_OPT_ALTSVC_PROTOCOL_ID[] = "altsvc-protocol-id"; +const char SHRPX_OPT_ALTSVC_HOST[] = "altsvc-host"; +const char SHRPX_OPT_ALTSVC_ORIGIN[] = "altsvc-origin"; namespace { Config *config = nullptr; @@ -498,6 +502,32 @@ int parse_config(const char *opt, const char *optarg) mod_config()->upstream_frame_debug = util::strieq(optarg, "yes"); } else if(util::strieq(opt, SHRPX_OPT_PADDING)) { mod_config()->padding = strtoul(optarg, nullptr, 10); + } else if(util::strieq(opt, SHRPX_OPT_ALTSVC_PORT)) { + errno = 0; + + auto port = strtoul(optarg, nullptr, 10); + + if(errno == 0 && + 1 <= port && port <= std::numeric_limits::max()) { + + mod_config()->altsvc_port = port; + } else { + LOG(ERROR) << "altsvc-port is invalid: " << optarg; + return -1; + } + } else if(util::strieq(opt, SHRPX_OPT_ALTSVC_PROTOCOL_ID)) { + set_config_str(&mod_config()->altsvc_protocol_id, optarg); + + mod_config()->altsvc_protocol_id_len = + strlen(get_config()->altsvc_protocol_id); + } else if(util::strieq(opt, SHRPX_OPT_ALTSVC_HOST)) { + set_config_str(&mod_config()->altsvc_host, optarg); + + mod_config()->altsvc_host_len = strlen(get_config()->altsvc_host); + } else if(util::strieq(opt, SHRPX_OPT_ALTSVC_ORIGIN)) { + set_config_str(&mod_config()->altsvc_origin, optarg); + + mod_config()->altsvc_origin_len = strlen(get_config()->altsvc_origin); } else if(util::strieq(opt, "conf")) { LOG(WARNING) << "conf is ignored"; } else { diff --git a/src/shrpx_config.h b/src/shrpx_config.h index c102e96f..8ec70fb6 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -108,6 +108,10 @@ extern const char SHRPX_OPT_FRONTEND_HTTP2_DUMP_RESPONSE_HEADER[]; extern const char SHRPX_OPT_HTTP2_NO_COOKIE_CRUMBLING[]; extern const char SHRPX_OPT_FRONTEND_FRAME_DEBUG[]; extern const char SHRPX_OPT_PADDING[]; +extern const char SHRPX_OPT_ALTSVC_PORT[]; +extern const char SHRPX_OPT_ALTSVC_PROTOCOL_ID[]; +extern const char SHRPX_OPT_ALTSVC_HOST[]; +extern const char SHRPX_OPT_ALTSVC_ORIGIN[]; union sockaddr_union { sockaddr sa; @@ -170,6 +174,9 @@ struct Config { char *client_cert_file; FILE *http2_upstream_dump_request_header; FILE *http2_upstream_dump_response_header; + char *altsvc_protocol_id; + char *altsvc_host; + char *altsvc_origin; size_t downstream_addrlen; size_t num_worker; size_t http2_max_concurrent_streams; @@ -192,6 +199,9 @@ struct Config { // The number of elements in tls_proto_list size_t tls_proto_list_len; size_t padding; + size_t altsvc_protocol_id_len; + size_t altsvc_host_len; + size_t altsvc_origin_len; // downstream protocol; this will be determined by given options. shrpx_proto downstream_proto; int syslog_facility; @@ -202,6 +212,7 @@ struct Config { uint16_t downstream_port; // port in http proxy URI uint16_t downstream_http_proxy_port; + uint16_t altsvc_port; bool verbose; bool daemon; bool verify_client; diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc index 6d2c454d..32672d49 100644 --- a/src/shrpx_http2_upstream.cc +++ b/src/shrpx_http2_upstream.cc @@ -532,13 +532,40 @@ Http2Upstream::Http2Upstream(ClientHandler *handler) rv = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, entry, sizeof(entry)/sizeof(nghttp2_settings_entry)); - assert(rv == 0); + if(rv != 0) { + ULOG(ERROR, this) << "nghttp2_submit_settings() returned error: " + << nghttp2_strerror(rv); + } if(get_config()->http2_upstream_connection_window_bits > 16) { int32_t delta = (1 << get_config()->http2_upstream_connection_window_bits) - 1 - NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE; rv = nghttp2_submit_window_update(session_, NGHTTP2_FLAG_NONE, 0, delta); - assert(rv == 0); + + if(rv != 0) { + ULOG(ERROR, this) << "nghttp2_submit_window_update() returned error: " + << nghttp2_strerror(rv); + } + } + + if(get_config()->altsvc_port != 0 && get_config()->altsvc_protocol_id) { + // Set max_age to 24hrs, which is default for alt-svc header + // field. + rv = nghttp2_submit_altsvc + (session_, NGHTTP2_FLAG_NONE, 0, + 86400, + get_config()->altsvc_port, + reinterpret_cast(get_config()->altsvc_protocol_id), + get_config()->altsvc_protocol_id_len, + reinterpret_cast(get_config()->altsvc_host), + get_config()->altsvc_host_len, + reinterpret_cast(get_config()->altsvc_origin), + get_config()->altsvc_origin_len); + + if(rv != 0) { + ULOG(ERROR, this) << "nghttp2_submit_altsvc() returned error: " + << nghttp2_strerror(rv); + } } } diff --git a/src/shrpx_https_upstream.cc b/src/shrpx_https_upstream.cc index de35f4ce..b41c2055 100644 --- a/src/shrpx_https_upstream.cc +++ b/src/shrpx_https_upstream.cc @@ -678,6 +678,18 @@ int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) } else { hdrs += "Connection: close\r\n"; } + + if(downstream->get_norm_response_header("alt-svc") == end_headers) { + // We won't change or alter alt-svc from backend at the moment. + if(get_config()->altsvc_port != 0 && get_config()->altsvc_protocol_id) { + hdrs += "Alt-Svc: "; + hdrs += util::percent_encode_token(get_config()->altsvc_protocol_id); + hdrs += "="; + hdrs += util::utos(get_config()->altsvc_port); + hdrs += "\r\n"; + } + } + auto via = downstream->get_norm_response_header("via"); if(get_config()->no_via) { if(via != end_headers) { diff --git a/src/util.cc b/src/util.cc index 328872b9..9e386fcc 100644 --- a/src/util.cc +++ b/src/util.cc @@ -86,6 +86,34 @@ std::string percentEncode(const std::string& target) target.size()); } +bool in_token(char c) +{ + static const char extra[] = { + '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~' + }; + + return isAlpha(c) || isDigit(c) || + std::find(&extra[0], &extra[sizeof(extra)], c) != &extra[sizeof(extra)]; +} + +std::string percent_encode_token(const std::string& target) +{ + auto len = target.size(); + std::string dest; + + for(size_t i = 0; i < len; ++i) { + char c = target[i]; + if(c != '%' && in_token(c)) { + dest += c; + } else { + char temp[4]; + snprintf(temp, sizeof(temp), "%%%02X", c); + dest += temp; + } + } + return dest; +} + std::string percentDecode (std::string::const_iterator first, std::string::const_iterator last) { diff --git a/src/util.h b/src/util.h index d4d0ff3c..bdd39402 100644 --- a/src/util.h +++ b/src/util.h @@ -205,6 +205,9 @@ bool isHexDigit(const char c); bool inRFC3986UnreservedChars(const char c); +// Returns true if |c| is in token (HTTP-p1, Section 3.2.6) +bool in_token(char c); + std::string percentEncode(const unsigned char* target, size_t len); std::string percentEncode(const std::string& target); @@ -212,6 +215,9 @@ std::string percentEncode(const std::string& target); std::string percentDecode (std::string::const_iterator first, std::string::const_iterator last); +// Percent encode |target| if character is not in token or '%'. +std::string percent_encode_token(const std::string& target); + std::string format_hex(const unsigned char *s, size_t len); std::string http_date(time_t t); diff --git a/src/util_test.cc b/src/util_test.cc index e3b28e73..c18f9ebf 100644 --- a/src/util_test.cc +++ b/src/util_test.cc @@ -93,4 +93,12 @@ void test_util_to_base64(void) CU_ASSERT("AAA++B/B" == x); } +void test_util_percent_encode_token(void) +{ + CU_ASSERT("h2" == util::percent_encode_token("h2")); + CU_ASSERT("h3~" == util::percent_encode_token("h3~")); + CU_ASSERT("100%25" == util::percent_encode_token("100%")); + CU_ASSERT("http%202" == util::percent_encode_token("http 2")); +} + } // namespace shrpx diff --git a/src/util_test.h b/src/util_test.h index 918c1ab2..7fd1686a 100644 --- a/src/util_test.h +++ b/src/util_test.h @@ -31,6 +31,7 @@ void test_util_streq(void); void test_util_strieq(void); void test_util_inp_strlower(void); void test_util_to_base64(void); +void test_util_percent_encode_token(void); } // namespace shrpx