| /* Licensed to the Apache Software Foundation (ASF) under one or more | |
| * contributor license agreements. See the NOTICE file distributed with | |
| * this work for additional information regarding copyright ownership. | |
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |
| * (the "License"); you may not use this file except in compliance with | |
| * the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include <assert.h> | |
| #include "apr.h" | |
| #include "apr_strings.h" | |
| #include "apr_lib.h" | |
| #include "apr_sha1.h" | |
| #include "apr_strmatch.h" | |
| #include <ap_mmn.h> | |
| #include <httpd.h> | |
| #include <http_core.h> | |
| #include <http_connection.h> | |
| #include <http_protocol.h> | |
| #include <http_request.h> | |
| #include <http_log.h> | |
| #include <http_ssl.h> | |
| #include <http_vhost.h> | |
| #include <util_filter.h> | |
| #include <ap_mpm.h> | |
| #include "h2_private.h" | |
| #include "h2_config.h" | |
| #include "h2_conn_ctx.h" | |
| #include "h2_headers.h" | |
| #include "h2_request.h" | |
| #include "h2_ws.h" | |
| #if H2_USE_WEBSOCKETS | |
| #include "apr_encode.h" /* H2_USE_WEBSOCKETS is conditional on APR 1.7+ */ | |
| static ap_filter_rec_t *c2_ws_out_filter_handle; | |
| struct ws_filter_ctx { | |
| const char *ws_accept_base64; | |
| int has_final_response; | |
| int override_body; | |
| }; | |
| /** | |
| * Generate the "Sec-WebSocket-Accept" header field for the given key | |
| * (base64 encoded) as defined in RFC 6455 ch. 4.2.2 step 5.3 | |
| */ | |
| static const char *gen_ws_accept(conn_rec *c, const char *key_base64) | |
| { | |
| apr_byte_t dgst[APR_SHA1_DIGESTSIZE]; | |
| const char ws_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; | |
| apr_sha1_ctx_t sha1_ctx; | |
| apr_sha1_init(&sha1_ctx); | |
| apr_sha1_update(&sha1_ctx, key_base64, (unsigned int)strlen(key_base64)); | |
| apr_sha1_update(&sha1_ctx, ws_guid, (unsigned int)strlen(ws_guid)); | |
| apr_sha1_final(dgst, &sha1_ctx); | |
| return apr_pencode_base64_binary(c->pool, dgst, sizeof(dgst), | |
| APR_ENCODE_NONE, NULL); | |
| } | |
| const h2_request *h2_ws_rewrite_request(const h2_request *req, | |
| conn_rec *c2, int no_body) | |
| { | |
| h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(c2); | |
| h2_request *wsreq; | |
| unsigned char key_raw[16]; | |
| const char *key_base64, *accept_base64; | |
| struct ws_filter_ctx *ws_ctx; | |
| apr_status_t rv; | |
| if (!conn_ctx || !req->protocol || strcmp("websocket", req->protocol)) | |
| return req; | |
| if (ap_cstr_casecmp("CONNECT", req->method)) { | |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, | |
| "h2_c2(%s-%d): websocket request with method %s", | |
| conn_ctx->id, conn_ctx->stream_id, req->method); | |
| return req; | |
| } | |
| if (!req->scheme) { | |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, | |
| "h2_c2(%s-%d): websocket CONNECT without :scheme", | |
| conn_ctx->id, conn_ctx->stream_id); | |
| return req; | |
| } | |
| if (!req->path) { | |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, | |
| "h2_c2(%s-%d): websocket CONNECT without :path", | |
| conn_ctx->id, conn_ctx->stream_id); | |
| return req; | |
| } | |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, | |
| "h2_c2(%s-%d): websocket CONNECT for %s", | |
| conn_ctx->id, conn_ctx->stream_id, req->path); | |
| /* Transform the HTTP/2 extended CONNECT to an internal GET using | |
| * the HTTP/1.1 version of websocket connection setup. */ | |
| wsreq = h2_request_clone(c2->pool, req); | |
| wsreq->method = "GET"; | |
| wsreq->protocol = NULL; | |
| apr_table_set(wsreq->headers, "Upgrade", "websocket"); | |
| apr_table_add(wsreq->headers, "Connection", "Upgrade"); | |
| /* add Sec-WebSocket-Key header */ | |
| rv = apr_generate_random_bytes(key_raw, sizeof(key_raw)); | |
| if (rv != APR_SUCCESS) { | |
| ap_log_error(APLOG_MARK, APLOG_CRIT, rv, NULL, APLOGNO(10461) | |
| "error generating secret"); | |
| return NULL; | |
| } | |
| key_base64 = apr_pencode_base64_binary(c2->pool, key_raw, sizeof(key_raw), | |
| APR_ENCODE_NONE, NULL); | |
| apr_table_set(wsreq->headers, "Sec-WebSocket-Key", key_base64); | |
| /* This is now the request to process internally */ | |
| /* When this request gets processed and delivers a 101 response, | |
| * we expect it to carry a "Sec-WebSocket-Accept" header with | |
| * exactly the following value, as per RFC 6455. */ | |
| accept_base64 = gen_ws_accept(c2, key_base64); | |
| /* Add an output filter that intercepts generated responses: | |
| * - if a valid WebSocket negotiation happens, transform the | |
| * 101 response to a 200 | |
| * - if a 2xx response happens, that does not pass the Accept test, | |
| * return a 502 indicating that the URI seems not support the websocket | |
| * protocol (RFC 8441 does not define this, but it seems the best | |
| * choice) | |
| * - if a 3xx, 4xx or 5xx response happens, forward this unchanged. | |
| */ | |
| ws_ctx = apr_pcalloc(c2->pool, sizeof(*ws_ctx)); | |
| ws_ctx->ws_accept_base64 = accept_base64; | |
| /* insert our filter just before the C2 core filter */ | |
| ap_remove_output_filter_byhandle(c2->output_filters, "H2_C2_NET_OUT"); | |
| ap_add_output_filter("H2_C2_WS_OUT", ws_ctx, NULL, c2); | |
| ap_add_output_filter("H2_C2_NET_OUT", NULL, NULL, c2); | |
| /* Mark the connection as being an Upgrade, with some special handling | |
| * since the request needs an EOS, without the stream being closed */ | |
| conn_ctx->is_upgrade = 1; | |
| return wsreq; | |
| } | |
| static apr_bucket *make_valid_resp(conn_rec *c2, int status, | |
| apr_table_t *headers, apr_table_t *notes) | |
| { | |
| apr_table_t *nheaders, *nnotes; | |
| ap_assert(headers); | |
| nheaders = apr_table_clone(c2->pool, headers); | |
| apr_table_unset(nheaders, "Connection"); | |
| apr_table_unset(nheaders, "Upgrade"); | |
| apr_table_unset(nheaders, "Sec-WebSocket-Accept"); | |
| nnotes = notes? apr_table_clone(c2->pool, notes) : | |
| apr_table_make(c2->pool, 10); | |
| #if AP_HAS_RESPONSE_BUCKETS | |
| return ap_bucket_response_create(status, NULL, nheaders, nnotes, | |
| c2->pool, c2->bucket_alloc); | |
| #else | |
| return h2_bucket_headers_create(c2->bucket_alloc, | |
| h2_headers_create(status, nheaders, | |
| nnotes, 0, c2->pool)); | |
| #endif | |
| } | |
| static apr_bucket *make_invalid_resp(conn_rec *c2, int status, | |
| apr_table_t *notes) | |
| { | |
| apr_table_t *nheaders, *nnotes; | |
| nheaders = apr_table_make(c2->pool, 10); | |
| apr_table_setn(nheaders, "Content-Length", "0"); | |
| nnotes = notes? apr_table_clone(c2->pool, notes) : | |
| apr_table_make(c2->pool, 10); | |
| #if AP_HAS_RESPONSE_BUCKETS | |
| return ap_bucket_response_create(status, NULL, nheaders, nnotes, | |
| c2->pool, c2->bucket_alloc); | |
| #else | |
| return h2_bucket_headers_create(c2->bucket_alloc, | |
| h2_headers_create(status, nheaders, | |
| nnotes, 0, c2->pool)); | |
| #endif | |
| } | |
| static void ws_handle_resp(conn_rec *c2, h2_conn_ctx_t *conn_ctx, | |
| struct ws_filter_ctx *ws_ctx, apr_bucket *b) | |
| { | |
| #if AP_HAS_RESPONSE_BUCKETS | |
| ap_bucket_response *resp = b->data; | |
| #else /* AP_HAS_RESPONSE_BUCKETS */ | |
| h2_headers *resp = h2_bucket_headers_get(b); | |
| #endif /* !AP_HAS_RESPONSE_BUCKETS */ | |
| apr_bucket *b_override = NULL; | |
| int is_final = 0; | |
| int override_body = 0; | |
| if (ws_ctx->has_final_response) { | |
| /* already did, nop */ | |
| return; | |
| } | |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c2, | |
| "h2_c2(%s-%d): H2_C2_WS_OUT inspecting response %d", | |
| conn_ctx->id, conn_ctx->stream_id, resp->status); | |
| if (resp->status == HTTP_SWITCHING_PROTOCOLS) { | |
| /* The resource agreed to switch protocol. But this is only valid | |
| * if it send back the correct Sec-WebSocket-Accept header value */ | |
| const char *hd = apr_table_get(resp->headers, "Sec-WebSocket-Accept"); | |
| if (hd && !strcmp(ws_ctx->ws_accept_base64, hd)) { | |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, | |
| "h2_c2(%s-%d): websocket CONNECT, valid 101 Upgrade" | |
| ", converting to 200 response", | |
| conn_ctx->id, conn_ctx->stream_id); | |
| b_override = make_valid_resp(c2, HTTP_OK, resp->headers, resp->notes); | |
| is_final = 1; | |
| } | |
| else { | |
| if (!hd) { | |
| /* This points to someone being confused */ | |
| ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c2, APLOGNO(10462) | |
| "h2_c2(%s-%d): websocket CONNECT, got 101 response " | |
| "without Sec-WebSocket-Accept header", | |
| conn_ctx->id, conn_ctx->stream_id); | |
| } | |
| else { | |
| /* This points to a bug, either in our WebSockets negotiation | |
| * or in the request processings implementation of WebSockets */ | |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, c2, APLOGNO(10463) | |
| "h2_c2(%s-%d): websocket CONNECT, 101 response " | |
| "without 'Sec-WebSocket-Accept: %s' but expected %s", | |
| conn_ctx->id, conn_ctx->stream_id, hd, | |
| ws_ctx->ws_accept_base64); | |
| } | |
| b_override = make_invalid_resp(c2, HTTP_BAD_GATEWAY, resp->notes); | |
| override_body = is_final = 1; | |
| } | |
| } | |
| else if (resp->status < HTTP_OK) { | |
| /* other intermediate response, pass through */ | |
| } | |
| else if (resp->status < HTTP_MULTIPLE_CHOICES) { | |
| /* Failure, we might be talking to a plain http resource */ | |
| ap_log_cerror(APLOG_MARK, APLOG_TRACE1, 0, c2, | |
| "h2_c2(%s-%d): websocket CONNECT, invalid response %d", | |
| conn_ctx->id, conn_ctx->stream_id, resp->status); | |
| b_override = make_invalid_resp(c2, HTTP_BAD_GATEWAY, resp->notes); | |
| override_body = is_final = 1; | |
| } | |
| else { | |
| /* error response, pass through. */ | |
| ws_ctx->has_final_response = 1; | |
| } | |
| if (b_override) { | |
| APR_BUCKET_INSERT_BEFORE(b, b_override); | |
| apr_bucket_delete(b); | |
| b = b_override; | |
| } | |
| if (override_body) { | |
| APR_BUCKET_INSERT_AFTER(b, apr_bucket_eos_create(c2->bucket_alloc)); | |
| ws_ctx->override_body = 1; | |
| } | |
| if (is_final) { | |
| ws_ctx->has_final_response = 1; | |
| conn_ctx->has_final_response = 1; | |
| } | |
| } | |
| static apr_status_t h2_c2_ws_filter_out(ap_filter_t* f, apr_bucket_brigade* bb) | |
| { | |
| struct ws_filter_ctx *ws_ctx = f->ctx; | |
| h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(f->c); | |
| apr_bucket *b, *bnext; | |
| ap_assert(conn_ctx); | |
| if (ws_ctx->override_body) { | |
| /* We have overridden the original response and also its body. | |
| * If this filter is called again, we signal a hard abort to | |
| * allow processing to terminate at the earliest. */ | |
| f->c->aborted = 1; | |
| return APR_ECONNABORTED; | |
| } | |
| /* Inspect the brigade, looking for RESPONSE/HEADER buckets. | |
| * Remember, this filter is only active for client websocket CONNECT | |
| * requests that we translated to an internal GET with websocket | |
| * headers. | |
| * We inspect the repsone to see if the internal resource actually | |
| * agrees to talk websocket or is "just" a normal HTTP resource that | |
| * ignored the websocket request headers. */ | |
| for (b = APR_BRIGADE_FIRST(bb); | |
| b != APR_BRIGADE_SENTINEL(bb); | |
| b = bnext) | |
| { | |
| bnext = APR_BUCKET_NEXT(b); | |
| if (APR_BUCKET_IS_METADATA(b)) { | |
| #if AP_HAS_RESPONSE_BUCKETS | |
| if (AP_BUCKET_IS_RESPONSE(b)) { | |
| #else | |
| if (H2_BUCKET_IS_HEADERS(b)) { | |
| #endif /* !AP_HAS_RESPONSE_BUCKETS */ | |
| ws_handle_resp(f->c, conn_ctx, ws_ctx, b); | |
| continue; | |
| } | |
| } | |
| else if (ws_ctx->override_body) { | |
| apr_bucket_delete(b); | |
| } | |
| } | |
| return ap_pass_brigade(f->next, bb); | |
| } | |
| static int ws_post_read(request_rec *r) | |
| { | |
| if (r->connection->master) { | |
| h2_conn_ctx_t *conn_ctx = h2_conn_ctx_get(r->connection); | |
| if (conn_ctx && conn_ctx->is_upgrade && | |
| !h2_config_sgeti(r->server, H2_CONF_WEBSOCKETS)) { | |
| return HTTP_NOT_IMPLEMENTED; | |
| } | |
| } | |
| return DECLINED; | |
| } | |
| void h2_ws_register_hooks(void) | |
| { | |
| ap_hook_post_read_request(ws_post_read, NULL, NULL, APR_HOOK_MIDDLE); | |
| c2_ws_out_filter_handle = | |
| ap_register_output_filter("H2_C2_WS_OUT", h2_c2_ws_filter_out, | |
| NULL, AP_FTYPE_NETWORK); | |
| } | |
| #else /* H2_USE_WEBSOCKETS */ | |
| const h2_request *h2_ws_rewrite_request(const h2_request *req, | |
| conn_rec *c2, int no_body) | |
| { | |
| (void)c2; | |
| (void)no_body; | |
| /* no rewriting */ | |
| return req; | |
| } | |
| void h2_ws_register_hooks(void) | |
| { | |
| /* NOP */ | |
| } | |
| #endif /* H2_USE_WEBSOCKETS (else part) */ |