]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
*) mod_http2: adding checks for websocket support on platform and
authorStefan Eissing <icing@apache.org>
Wed, 21 Jun 2023 12:14:08 +0000 (12:14 +0000)
committerStefan Eissing <icing@apache.org>
Wed, 21 Jun 2023 12:14:08 +0000 (12:14 +0000)
     server versions. Give error message accordingly when trying to
     enable websockets in unsupported configurations.
     Add test and code to check the, finally selected, server of
     a request_rec for websocket support or 501 the request.

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1910535 13f79535-47bb-0310-9956-ffa450edef68

modules/http2/h2.h
modules/http2/h2_c2.c
modules/http2/h2_c2_filter.c
modules/http2/h2_config.c
modules/http2/h2_request.c
modules/http2/h2_stream.c
modules/http2/h2_ws.c
test/modules/http2/test_800_websockets.py

index 4babbf81d6b5bc87d26c6bb5f060e6aa0db4798e..2bb59ecb9c08e15e25e91cdae3a778508e8e48dc 100644 (file)
@@ -33,6 +33,18 @@ struct h2_stream;
 #define H2_USE_PIPES            (APR_FILES_AS_SOCKETS && APR_VERSION_AT_LEAST(1,6,0))
 #endif
 
+#if AP_MODULE_MAGIC_AT_LEAST(20211221, 15)
+#define H2_USE_POLLFD_FROM_CONN 1
+#else
+#define H2_USE_POLLFD_FROM_CONN 0
+#endif
+
+#if H2_USE_POLLFD_FROM_CONN && H2_USE_PIPES
+#define H2_USE_WEBSOCKETS       1
+#else
+#define H2_USE_WEBSOCKETS       0
+#endif
+
 /**
  * The magic PRIamble of RFC 7540 that is always sent when starting
  * a h2 communication.
index 537163bf79d989306af4815e1b3705f3f58ff507..783a297fe0ba58a7a68c30db804c1d6c88a6af24 100644 (file)
@@ -559,6 +559,7 @@ static int c2_hook_pre_connection(conn_rec *c2, void *csd)
     return OK;
 }
 
+#if H2_USE_POLLFD_FROM_CONN
 static apr_status_t c2_get_pollfd_from_conn(conn_rec *c,
                                             struct apr_pollfd_t *pfd,
                                             apr_interval_time_t *ptimeout)
@@ -583,6 +584,7 @@ static apr_status_t c2_get_pollfd_from_conn(conn_rec *c,
     }
     return APR_ENOTIMPL;
 }
+#endif
 
 void h2_c2_register_hooks(void)
 {
@@ -598,12 +600,11 @@ void h2_c2_register_hooks(void)
     ap_hook_post_read_request(c2_post_read_request, NULL, NULL,
                               APR_HOOK_REALLY_FIRST);
     ap_hook_fixups(c2_hook_fixups, NULL, NULL, APR_HOOK_LAST);
-#if AP_MODULE_MAGIC_AT_LEAST(20211221, 15)
+#if H2_USE_POLLFD_FROM_CONN
     ap_hook_get_pollfd_from_conn(c2_get_pollfd_from_conn, NULL, NULL,
                                  APR_HOOK_MIDDLE);
 #endif
 
-
     c2_net_in_filter_handle =
         ap_register_input_filter("H2_C2_NET_IN", h2_c2_filter_in,
                                  NULL, AP_FTYPE_NETWORK);
@@ -788,7 +789,7 @@ static apr_status_t c2_process(h2_conn_ctx_t *conn_ctx, conn_rec *c)
         cs->state = CONN_STATE_WRITE_COMPLETION;
 
 cleanup:
-    return APR_SUCCESS;
+    return rv;
 }
 
 conn_rec *h2_c2_create(conn_rec *c1, apr_pool_t *parent,
index 846344c6b474efef6508bd01c7512a083983b387..97c38b3f6dc002083f386f43e5c2cbea518ab698 100644 (file)
@@ -120,20 +120,28 @@ apr_status_t h2_c2_filter_request_in(ap_filter_t *f,
                 return APR_EGENERAL;
         }
 
+        ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c,
+                      "h2_c2_filter_request_in(%s): adding request bucket",
+                      conn_ctx->id);
+        b = h2_request_create_bucket(req, f->r);
+        APR_BRIGADE_INSERT_TAIL(bb, b);
+
         if (req->http_status != H2_HTTP_STATUS_UNSET) {
             /* error was encountered preparing this request */
+            ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, f->c,
+                          "h2_c2_filter_request_in(%s): adding error bucket %d",
+                          conn_ctx->id, req->http_status);
             b = ap_bucket_error_create(req->http_status, NULL, f->r->pool,
                                        f->c->bucket_alloc);
             APR_BRIGADE_INSERT_TAIL(bb, b);
             return APR_SUCCESS;
         }
 
-        b = h2_request_create_bucket(req, f->r);
-        APR_BRIGADE_INSERT_TAIL(bb, b);
         if (!conn_ctx->beam_in) {
             b = apr_bucket_eos_create(f->c->bucket_alloc);
             APR_BRIGADE_INSERT_TAIL(bb, b);
         }
+
         return APR_SUCCESS;
     }
 
index a8b19739026c556e9710c2268672756f45960899..7f9f18078c06565251364a1f35fa763ea8bc573d 100644 (file)
@@ -694,11 +694,13 @@ static const char *h2_conf_set_websockets(cmd_parms *cmd,
                                           void *dirconf, const char *value)
 {
     if (!strcasecmp(value, "On")) {
-#if H2_USE_PIPES
+#if H2_USE_WEBSOCKETS
         CONFIG_CMD_SET(cmd, dirconf, H2_CONF_WEBSOCKETS, 1);
         return NULL;
-#else
+#elif !H2_USE_PIPES
         return "HTTP/2 WebSockets are not supported on this platform";
+#else
+        return "HTTP/2 WebSockets are not supported in this server version";
 #endif
     }
     else if (!strcasecmp(value, "Off")) {
index b55d5720a02d8f7991038bba4e55302f09702107..4e363ab0aa3eda501e66205bc0ec330aa671cb37 100644 (file)
@@ -287,13 +287,14 @@ apr_bucket *h2_request_create_bucket(const h2_request *req, request_rec *r)
     apr_table_t *headers = apr_table_clone(r->pool, req->headers);
     const char *uri = req->path;
 
+    AP_DEBUG_ASSERT(req->method);
     AP_DEBUG_ASSERT(req->authority);
-    if (req->scheme && (ap_cstr_casecmp(req->scheme,
-                        ap_ssl_conn_is_ssl(c->master? c->master : c)? "https" : "http")
-                        || !ap_cstr_casecmp("CONNECT", req->method))) {
-        /* Client sent a non-matching ':scheme' pseudo header or CONNECT.
-         * In this case, we use an absolute URI.
-         */
+    if (!ap_cstr_casecmp("CONNECT", req->method))  {
+        uri = req->authority;
+    }
+    else if (req->scheme && (ap_cstr_casecmp(req->scheme, "http") &&
+                             ap_cstr_casecmp(req->scheme, "https")))  {
+        /* Client sent a non-http ':scheme', use an absolute URI */
         uri = apr_psprintf(r->pool, "%s://%s%s",
                            req->scheme, req->authority, req->path ? req->path : "");
     }
@@ -379,33 +380,25 @@ request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c,
     AP_DEBUG_ASSERT(req->authority);
     if (is_connect) {
       /* CONNECT MUST NOT have scheme or path */
-      if (req->scheme) {
-        ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458)
-                      "':scheme: %s' header present in CONNECT request",
-                      req->scheme);
-        access_status = HTTP_BAD_REQUEST;
-        goto die;
-      }
-      if (req->path) {
-        ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459)
-                      "':path: %s' header present in CONNECT request",
-                      req->path);
-        access_status = HTTP_BAD_REQUEST;
-        goto die;
-      }
-      r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0",
-                                    req->method, req->authority);
-    }
-    else if (req->protocol) {
-      ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10460)
-                    "':protocol: %s' header present in %s request",
-                    req->protocol, req->method);
-      access_status = HTTP_BAD_REQUEST;
-      goto die;
+        r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0",
+                                      req->method, req->authority);
+        if (req->scheme) {
+            ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458)
+                          "':scheme: %s' header present in CONNECT request",
+                          req->scheme);
+            access_status = HTTP_BAD_REQUEST;
+            goto die;
+        }
+        else if (req->path) {
+            ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459)
+                          "':path: %s' header present in CONNECT request",
+                          req->path);
+            access_status = HTTP_BAD_REQUEST;
+            goto die;
+        }
     }
-    else if (req->scheme &&
-             ap_cstr_casecmp(req->scheme, ap_ssl_conn_is_ssl(c->master? c->master : c)?
-                             "https" : "http")) {
+    else if (req->scheme && ap_cstr_casecmp(req->scheme, "http")
+             && ap_cstr_casecmp(req->scheme, "https")) {
         /* Client sent a ':scheme' pseudo header for something else
          * than what we have on this connection. Make an absolute URI. */
         r->the_request = apr_psprintf(r->pool, "%s %s://%s%s HTTP/2.0",
index 24d0268f38b4cccc89bcd79bfdf6cb1c166d319e..19527600e0817774095f034ab6edd26ff1e94588 100644 (file)
@@ -900,11 +900,23 @@ apr_status_t h2_stream_end_headers(h2_stream *stream, int eos, size_t raw_bytes)
      *      of CONNECT requests (see [RFC7230], Section 5.3)).
      */
     if (!ap_cstr_casecmp(req->method, "CONNECT")) {
-        if (req->protocol && !strcmp("websocket", req->protocol)) {
-            if (!req->scheme || !req->path) {
-                ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1,
-                              H2_STRM_LOG(APLOGNO(10457), stream, "Request to websocket CONNECT "
-                              "without :scheme or :path, sending 400 answer"));
+        if (req->protocol) {
+            if (!strcmp("websocket", req->protocol)) {
+                if (!req->scheme || !req->path) {
+                    ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1,
+                                  H2_STRM_LOG(APLOGNO(10457), stream, "Request to websocket CONNECT "
+                                  "without :scheme or :path, sending 400 answer"));
+                    set_error_response(stream, HTTP_BAD_REQUEST);
+                    goto cleanup;
+                }
+            }
+            else {
+                /* do not know that protocol */
+                ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, stream->session->c1, APLOGNO(10460)
+                              "':protocol: %s' header present in %s request",
+                              req->protocol, req->method);
+                set_error_response(stream, HTTP_NOT_IMPLEMENTED);
+                goto cleanup;
             }
         }
         else if (req->scheme || req->path) {
index e3bdadb32d9bca3e8de9749b8548da22ffa386b1..d2a51af6b7b18d454e3cbdd01e0651aaa0993c30 100644 (file)
@@ -43,6 +43,8 @@
 #include "h2_request.h"
 #include "h2_ws.h"
 
+#if H2_USE_WEBSOCKETS
+
 static ap_filter_rec_t *c2_ws_out_filter_handle;
 
 struct ws_filter_ctx {
@@ -318,9 +320,41 @@ static apr_status_t h2_c2_ws_filter_out(ap_filter_t* f, apr_bucket_brigade* bb)
     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) */
index 58ac4eb4e3988b7e55afcc84167d90d3e5a6f5e9..76f9fe9475aa13e4b33fbc28308831cfd06161b4 100644 (file)
@@ -5,11 +5,8 @@ import shutil
 import subprocess
 import time
 from datetime import timedelta, datetime
-from typing import Tuple, Union, List
-import packaging.version
 
 import pytest
-import websockets
 from pyhttpd.result import ExecResult
 from pyhttpd.ws_util import WsFrameReader, WsFrame
 
@@ -18,18 +15,15 @@ from .env import H2Conf, H2TestEnv
 
 log = logging.getLogger(__name__)
 
-ws_version = packaging.version.parse(websockets.version.version)
-ws_version_min = packaging.version.Version('10.4')
 
-
-def ws_run(env: H2TestEnv, path, do_input=None,
-           inbytes=None, send_close=True,
-           timeout=5, scenario='ws-stdin',
-           wait_close: float = 0.0) -> Tuple[ExecResult, List[str], Union[List[WsFrame], bytes]]:
+def ws_run(env: H2TestEnv, path, authority=None, do_input=None, inbytes=None,
+           send_close=True, timeout=5, scenario='ws-stdin',
+           wait_close: float = 0.0):
     """ Run the h2ws test client in various scenarios with given input and
         timings.
     :param env: the test environment
     :param path: the path on the Apache server to CONNECt to
+    :param authority: the host:port to use as
     :param do_input: a Callable for sending input to h2ws
     :param inbytes: fixed bytes to send to h2ws, unless do_input is given
     :param send_close: send a CLOSE WebSockets frame at the end
@@ -41,9 +35,11 @@ def ws_run(env: H2TestEnv, path, do_input=None,
     h2ws = os.path.join(env.clients_dir, 'h2ws')
     if not os.path.exists(h2ws):
         pytest.fail(f'test client not build: {h2ws}')
+    if authority is None:
+        authority = f'cgi.{env.http_tld}:{env.http_port}'
     args = [
         h2ws, '-vv', '-c', f'localhost:{env.http_port}',
-        f'ws://cgi.{env.http_tld}:{env.http_port}{path}',
+        f'ws://{authority}{path}',
         scenario
     ]
     # we write all output to files, because we manipulate input timings
@@ -80,8 +76,8 @@ def ws_run(env: H2TestEnv, path, do_input=None,
 
 
 @pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
-@pytest.mark.skipif(condition=ws_version < ws_version_min,
-                    reason=f'websockets is {ws_version}, need at least {ws_version_min}')
+@pytest.mark.skipif(condition=not H2TestEnv().httpd_is_at_least("2.5.0"),
+                    reason=f'need at least httpd 2.5.0 for this')
 class TestWebSockets:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -97,6 +93,7 @@ class TestWebSockets:
             ]
         })
         conf.add_vhost_cgi(proxy_self=True, h2proxy_self=True).install()
+        conf.add_vhost_test1(proxy_self=True, h2proxy_self=True).install()
         assert env.apache_restart() == 0
 
     def ws_check_alive(self, env, timeout=5):
@@ -150,7 +147,7 @@ class TestWebSockets:
     def test_h2_800_02_fail_proto(self, env: H2TestEnv, ws_server):
         r, infos, frames = ws_run(env, path='/ws/echo/', scenario='fail-proto')
         assert r.exit_code == 0, f'{r}'
-        assert infos == ['[1] :status: 400', '[1] EOF'], f'{r}'
+        assert infos == ['[1] :status: 501', '[1] EOF'], f'{r}'
 
     # CONNECT to a URL path that does not exist on the server
     def test_h2_800_03_not_found(self, env: H2TestEnv, ws_server):
@@ -193,11 +190,18 @@ class TestWebSockets:
         assert infos == ['[1] RST'], f'{r}'
 
     # CONNECT missing the :authority header
-    def test_h2_800_09_miss_authority(self, env: H2TestEnv, ws_server):
+    def test_h2_800_09a_miss_authority(self, env: H2TestEnv, ws_server):
         r, infos, frames = ws_run(env, path='/ws/echo/', scenario='miss-authority')
         assert r.exit_code == 0, f'{r}'
         assert infos == ['[1] RST'], f'{r}'
 
+    # CONNECT to authority with disabled websockets
+    def test_h2_800_09b_unsupported(self, env: H2TestEnv, ws_server):
+        r, infos, frames = ws_run(env, path='/ws/echo/',
+                                  authority=f'test1.{env.http_tld}:{env.http_port}')
+        assert r.exit_code == 0, f'{r}'
+        assert infos == ['[1] :status: 501', '[1] EOF'], f'{r}'
+
     # CONNECT and exchange a PING
     def test_h2_800_10_ws_ping(self, env: H2TestEnv, ws_server):
         ping = WsFrame.client_ping(b'12345')