nghttpx: Add experimental websocket with extended CONNECT
This commit is contained in:
parent
a26eb08a89
commit
19d28365e9
|
@ -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',
|
||||
|
|
22
src/http2.cc
22
src/http2.cc
|
@ -438,6 +438,11 @@ void copy_headers_to_nva_internal(std::vector<nghttp2_nv> &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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<Timestamp> 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().
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<nghttp2_settings_entry, 3> entry;
|
||||
size_t nentry = 2;
|
||||
std::array<nghttp2_settings_entry, 4> 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue