diff --git a/genheaderfunc.py b/genheaderfunc.py index 4f342364..af7ea801 100755 --- a/genheaderfunc.py +++ b/genheaderfunc.py @@ -9,6 +9,7 @@ HEADERS = [ ':scheme', ':status', ':host', # for spdy + ':protocol', 'expect', 'host', 'if-modified-since', @@ -31,6 +32,7 @@ HEADERS = [ "user-agent", "date", "content-type", + "sec-websocket-accept", # disallowed h1 headers 'connection', 'keep-alive', diff --git a/src/http2.cc b/src/http2.cc index 6db59744..dbce869f 100644 --- a/src/http2.cc +++ b/src/http2.cc @@ -438,6 +438,11 @@ void copy_headers_to_nva_internal(std::vector &nva, kv = &(*it_via); it_via = it; break; + case HD_SEC_WEBSOCKET_ACCEPT: + if (flags & HDOP_STRIP_SEC_WEBSOCKET_ACCEPT) { + continue; + } + break; } nva.push_back( make_nv_internal(kv->name, kv->value, kv->no_index, nv_flags)); @@ -821,6 +826,11 @@ int lookup_token(const uint8_t *name, size_t namelen) { return HD_FORWARDED; } break; + case 'l': + if (util::streq_l(":protoco", name, 8)) { + return HD__PROTOCOL; + } + break; } break; case 10: @@ -926,6 +936,15 @@ int lookup_token(const uint8_t *name, size_t namelen) { break; } break; + case 20: + switch (name[19]) { + case 't': + if (util::streq_l("sec-websocket-accep", name, 19)) { + return HD_SEC_WEBSOCKET_ACCEPT; + } + break; + } + break; } return -1; } @@ -1313,7 +1332,8 @@ std::string path_join(const StringRef &base_path, const StringRef &base_query, } bool expect_response_body(int status_code) { - return status_code / 100 != 1 && status_code != 304 && status_code != 204; + return status_code == 101 || + (status_code / 100 != 1 && status_code != 304 && status_code != 204); } bool expect_response_body(const std::string &method, int status_code) { diff --git a/src/http2.h b/src/http2.h index eb6a41d3..de63b616 100644 --- a/src/http2.h +++ b/src/http2.h @@ -206,6 +206,8 @@ enum HeaderBuildOp { // Strip above all header fields. HDOP_STRIP_ALL = HDOP_STRIP_FORWARDED | HDOP_STRIP_X_FORWARDED_FOR | HDOP_STRIP_X_FORWARDED_PROTO | HDOP_STRIP_VIA, + // Sec-WebSocket-Accept header field must be stripped. + HDOP_STRIP_SEC_WEBSOCKET_ACCEPT = 1 << 4, }; // Appends headers in |headers| to |nv|. |headers| must be indexed @@ -293,6 +295,7 @@ enum { HD__HOST, HD__METHOD, HD__PATH, + HD__PROTOCOL, HD__SCHEME, HD__STATUS, HD_ACCEPT_ENCODING, @@ -313,6 +316,7 @@ enum { HD_LINK, HD_LOCATION, HD_PROXY_CONNECTION, + HD_SEC_WEBSOCKET_ACCEPT, HD_SERVER, HD_TE, HD_TRAILER, diff --git a/src/shrpx_client_handler.cc b/src/shrpx_client_handler.cc index ea8cea16..0d489272 100644 --- a/src/shrpx_client_handler.cc +++ b/src/shrpx_client_handler.cc @@ -978,7 +978,7 @@ ClientHandler::get_downstream_connection(int &err, Downstream *downstream) { StringRef path; // CONNECT method does not have path. But we requires path in // host-path mapping. As workaround, we assume that path is "/". - if (req.method != HTTP_CONNECT) { + if (!req.regular_connect_method()) { path = req.path; } diff --git a/src/shrpx_downstream.cc b/src/shrpx_downstream.cc index cdd33bb8..bb431562 100644 --- a/src/shrpx_downstream.cc +++ b/src/shrpx_downstream.cc @@ -722,7 +722,12 @@ bool Downstream::validate_response_recv_body_length() const { void Downstream::check_upgrade_fulfilled() { if (req_.method == HTTP_CONNECT) { - upgraded_ = 200 <= resp_.http_status && resp_.http_status < 300; + if (req_.connect_proto) { + // TODO For websocket, check Sec-WebSocket-Accept header field. + upgraded_ = resp_.http_status == 101; + } else { + upgraded_ = 200 <= resp_.http_status && resp_.http_status < 300; + } return; } diff --git a/src/shrpx_downstream.h b/src/shrpx_downstream.h index c81fcf68..5bd0b225 100644 --- a/src/shrpx_downstream.h +++ b/src/shrpx_downstream.h @@ -134,6 +134,12 @@ private: bool trailer_key_prev_; }; +// Protocols allowed in HTTP/2 :protocol header field. +enum shrpx_connect_proto { + CONNECT_PROTO_NONE, + CONNECT_PROTO_WEBSOCKET, +}; + struct Request { Request(BlockAllocator &balloc) : fs(balloc, 16), @@ -153,6 +159,14 @@ struct Request { unconsumed_body_length -= len; } + bool regular_connect_method() const { + return method == HTTP_CONNECT && !connect_proto; + } + + bool extended_connect_method() const { + return method == HTTP_CONNECT && connect_proto; + } + FieldStore fs; // Timestamp when all request header fields are received. std::shared_ptr tstamp; @@ -176,6 +190,9 @@ struct Request { int method; // HTTP major and minor version int http_major, http_minor; + // connect_protocol specified in HTTP/2 :protocol pseudo header + // field which enables extended CONNECT method. + int connect_proto; // Returns true if the request is HTTP upgrade (HTTP Upgrade or // CONNECT method). Upgrade to HTTP/2 is excluded. For HTTP/2 // Upgrade, check get_http2_upgrade_request(). diff --git a/src/shrpx_http2_downstream_connection.cc b/src/shrpx_http2_downstream_connection.cc index 7a2d5598..fa2d1bd5 100644 --- a/src/shrpx_http2_downstream_connection.cc +++ b/src/shrpx_http2_downstream_connection.cc @@ -250,7 +250,7 @@ int Http2DownstreamConnection::push_request_headers() { auto &http2conf = config->http2; auto no_host_rewrite = httpconf.no_host_rewrite || config->http2_proxy || - req.method == HTTP_CONNECT; + req.regular_connect_method(); // http2session_ has already in CONNECTED state, so we can get // addr_idx here. @@ -288,7 +288,7 @@ int Http2DownstreamConnection::push_request_headers() { nva.push_back( http2::make_nv_ls_nocopy(":method", http2::to_method_string(req.method))); - if (req.method != HTTP_CONNECT) { + if (!req.regular_connect_method()) { assert(!req.scheme.empty()); auto addr = http2session_->get_addr(); @@ -339,7 +339,7 @@ int Http2DownstreamConnection::push_request_headers() { if (fwdconf.params) { auto params = fwdconf.params; - if (config->http2_proxy || req.method == HTTP_CONNECT) { + if (config->http2_proxy || req.regular_connect_method()) { params &= ~FORWARDED_PROTO; } @@ -380,7 +380,7 @@ int Http2DownstreamConnection::push_request_headers() { nva.push_back(http2::make_nv_ls_nocopy("x-forwarded-for", xff->value)); } - if (!config->http2_proxy && req.method != HTTP_CONNECT) { + if (!config->http2_proxy && !req.regular_connect_method()) { auto xfp = xfpconf.strip_incoming ? nullptr : req.fs.header(http2::HD_X_FORWARDED_PROTO); diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc index df78dfdf..42209c55 100644 --- a/src/shrpx_http2_upstream.cc +++ b/src/shrpx_http2_upstream.cc @@ -391,6 +391,19 @@ int Http2Upstream::on_request_headers(Downstream *downstream, } } + if (!config->http2_proxy) { + auto connect_proto = req.fs.header(http2::HD__PROTOCOL); + if (connect_proto) { + if (connect_proto->value != "websocket") { + if (error_reply(downstream, 400) != 0) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + return 0; + } + req.connect_proto = CONNECT_PROTO_WEBSOCKET; + } + } + if (!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) { req.http2_expect_body = true; } else if (req.fs.content_length == -1) { @@ -1001,8 +1014,8 @@ Http2Upstream::Http2Upstream(ClientHandler *handler) flow_control_ = true; // TODO Maybe call from outside? - std::array entry; - size_t nentry = 2; + std::array entry; + size_t nentry = 3; entry[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; entry[0].value = http2conf.upstream.max_concurrent_streams; @@ -1014,6 +1027,9 @@ Http2Upstream::Http2Upstream(ClientHandler *handler) entry[1].value = http2conf.upstream.window_size; } + entry[2].settings_id = NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL; + entry[2].value = 1; + if (http2conf.upstream.decoder_dynamic_table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { entry[nentry].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; @@ -1662,11 +1678,11 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { nva.reserve(resp.fs.headers().size() + 5 + httpconf.add_response_headers.size()); - auto response_status = http2::stringify_status(balloc, resp.http_status); - - nva.push_back(http2::make_nv_ls_nocopy(":status", response_status)); - if (downstream->get_non_final_response()) { + auto response_status = http2::stringify_status(balloc, resp.http_status); + + nva.push_back(http2::make_nv_ls_nocopy(":status", response_status)); + http2::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), http2::HDOP_STRIP_ALL); @@ -1688,8 +1704,19 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { return 0; } - http2::copy_headers_to_nva_nocopy( - nva, resp.fs.headers(), http2::HDOP_STRIP_ALL & ~http2::HDOP_STRIP_VIA); + auto striphd_flags = http2::HDOP_STRIP_ALL & ~http2::HDOP_STRIP_VIA; + StringRef response_status; + + if (req.connect_proto && resp.http_status == 101) { + response_status = http2::stringify_status(balloc, 200); + striphd_flags |= http2::HDOP_STRIP_SEC_WEBSOCKET_ACCEPT; + } else { + response_status = http2::stringify_status(balloc, resp.http_status); + } + + nva.push_back(http2::make_nv_ls_nocopy(":status", response_status)); + + http2::copy_headers_to_nva_nocopy(nva, resp.fs.headers(), striphd_flags); if (!config->http2_proxy && !httpconf.no_server_rewrite) { nva.push_back(http2::make_nv_ls_nocopy("server", httpconf.server_name)); @@ -1700,7 +1727,7 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { } } - if (req.method != HTTP_CONNECT || !downstream->get_upgraded()) { + if (!req.regular_connect_method() || !downstream->get_upgraded()) { auto affinity_cookie = downstream->get_affinity_cookie_to_send(); if (affinity_cookie) { auto dconn = downstream->get_downstream_connection(); @@ -1874,7 +1901,7 @@ int Http2Upstream::on_downstream_abort_request_with_https_redirect( int Http2Upstream::redirect_to_https(Downstream *downstream) { auto &req = downstream->request(); - if (req.method == HTTP_CONNECT || req.scheme != "http") { + if (req.regular_connect_method() || req.scheme != "http") { return error_reply(downstream, 400); } diff --git a/src/shrpx_http_downstream_connection.cc b/src/shrpx_http_downstream_connection.cc index f50c0f4f..2228a3bc 100644 --- a/src/shrpx_http_downstream_connection.cc +++ b/src/shrpx_http_downstream_connection.cc @@ -484,7 +484,7 @@ int HttpDownstreamConnection::push_request_headers() { auto &balloc = downstream_->get_block_allocator(); - auto connect_method = req.method == HTTP_CONNECT; + auto connect_method = req.regular_connect_method(); auto config = get_config(); auto &httpconf = config->http; @@ -508,7 +508,8 @@ int HttpDownstreamConnection::push_request_headers() { auto buf = downstream_->get_request_buf(); // Assume that method and request path do not contain \r\n. - auto meth = http2::to_method_string(req.method); + auto meth = http2::to_method_string( + req.connect_proto == CONNECT_PROTO_WEBSOCKET ? HTTP_GET : req.method); buf->append(meth); buf->append(' '); @@ -552,7 +553,8 @@ int HttpDownstreamConnection::push_request_headers() { // set transfer-encoding only when content-length is unknown and // request body is expected. - if (!connect_method && req.http2_expect_body && req.fs.content_length == -1) { + if (req.method != HTTP_CONNECT && req.http2_expect_body && + req.fs.content_length == -1) { downstream_->set_chunked_request(true); buf->append("Transfer-Encoding: chunked\r\n"); } @@ -561,7 +563,11 @@ int HttpDownstreamConnection::push_request_headers() { buf->append("Connection: close\r\n"); } - if (!connect_method && req.upgrade_request) { + if (req.connect_proto == CONNECT_PROTO_WEBSOCKET) { + // TODO Generate Sec-WebSocket-Key + buf->append("Upgrade: websocket\r\nConnection: " + "Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"); + } else if (!connect_method && req.upgrade_request) { auto connection = req.fs.header(http2::HD_CONNECTION); if (connection) { buf->append("Connection: "); @@ -693,7 +699,7 @@ int HttpDownstreamConnection::push_request_headers() { // Don't call signal_write() if we anticipate request body. We call // signal_write() when we received request body chunk, and it // enables us to send headers and data in one writev system call. - if (connect_method || + if (connect_method || req.connect_proto || (!req.http2_expect_body && req.fs.content_length == 0)) { signal_write(); } @@ -901,7 +907,7 @@ int htp_hdrs_completecb(http_parser *htp) { return -1; } } else if (resp.http_status / 100 == 1 || - (resp.http_status == 200 && req.method == HTTP_CONNECT)) { + (resp.http_status == 200 && req.regular_connect_method())) { if (resp.fs.header(http2::HD_CONTENT_LENGTH) || resp.fs.header(http2::HD_TRANSFER_ENCODING)) { return -1;