From 7930e87462def9bf8d06150e53ba2fde31ce256b Mon Sep 17 00:00:00 2001 From: Jim Jagielski Date: Tue, 5 Mar 2013 17:29:28 +0000 Subject: [PATCH] Rough start for simple, tunneling websocket proxy support. Compiles at this stage and that's all I know :) git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1452911 13f79535-47bb-0310-9956-ffa450edef68 --- modules/proxy/config.m4 | 3 + modules/proxy/mod_proxy_websocket.c | 346 ++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 modules/proxy/mod_proxy_websocket.c diff --git a/modules/proxy/config.m4 b/modules/proxy/config.m4 index a5535a2d8f..0878363820 100644 --- a/modules/proxy/config.m4 +++ b/modules/proxy/config.m4 @@ -20,6 +20,7 @@ proxy_fcgi_objs="mod_proxy_fcgi.lo" proxy_scgi_objs="mod_proxy_scgi.lo" proxy_fdpass_objs="mod_proxy_fdpass.lo" proxy_ajp_objs="mod_proxy_ajp.lo ajp_header.lo ajp_link.lo ajp_msg.lo ajp_utils.lo" +proxy_websocket_objs="mod_proxy_websocket.lo" proxy_balancer_objs="mod_proxy_balancer.lo" case "$host" in @@ -33,6 +34,7 @@ case "$host" in proxy_scgi_objs="$proxy_scgi_objs mod_proxy.la" proxy_fdpass_objs="$proxy_fdpass_objs mod_proxy.la" proxy_ajp_objs="$proxy_ajp_objs mod_proxy.la" + proxy_websocket_objs="$proxy_websocket_objs mod_proxy.la" proxy_balancer_objs="$proxy_balancer_objs mod_proxy.la" ;; esac @@ -52,6 +54,7 @@ APACHE_MODULE(proxy_fdpass, Apache proxy to Unix Daemon Socket module. Requires enable_proxy_fdpass=no fi ],proxy) +APACHE_MODULE(proxy_websocket, Apache proxy websocket module. Requires and is enabled by --enable-proxy., $proxy_websocket_objs, , $proxy_mods_enable,, proxy) APACHE_MODULE(proxy_ajp, Apache proxy AJP module. Requires and is enabled by --enable-proxy., $proxy_ajp_objs, , $proxy_mods_enable,, proxy) APACHE_MODULE(proxy_balancer, Apache proxy BALANCER module. Requires and is enabled by --enable-proxy., $proxy_balancer_objs, , $proxy_mods_enable,, proxy) diff --git a/modules/proxy/mod_proxy_websocket.c b/modules/proxy/mod_proxy_websocket.c new file mode 100644 index 0000000000..1a6b6947a7 --- /dev/null +++ b/modules/proxy/mod_proxy_websocket.c @@ -0,0 +1,346 @@ +/* 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 "mod_proxy.h" + +module AP_MODULE_DECLARE_DATA proxy_websocket_module; + +/* + * Canonicalise http-like URLs. + * scheme is the scheme for the URL + * url is the URL starting with the first '/' + * def_port is the default port for this scheme. + */ +static int proxy_websocket_canon(request_rec *r, char *url) +{ + char *host, *path, sport[7]; + char *search = NULL; + const char *err; + char *scheme; + apr_port_t port, def_port; + + /* ap_port_of_scheme() */ + if (strncasecmp(url, "ws:", 3) == 0) { + url += 3; + scheme = "ws"; + def_port = apr_uri_port_of_scheme("http"); + } + else if (strncasecmp(url, "wss:", 4) == 0) { + url += 4; + scheme = "wss"; + def_port = apr_uri_port_of_scheme("https"); + } + else { + return DECLINED; + } + + port = def_port; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "canonicalising URL %s", url); + + /* + * do syntactic check. + * We break the URL into host, port, path, search + */ + err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port); + if (err) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO() "error parsing URL %s: %s", + url, err); + return HTTP_BAD_REQUEST; + } + + /* + * now parse path/search args, according to rfc1738: + * process the path. With proxy-nocanon set (by + * mod_proxy) we use the raw, unparsed uri + */ + if (apr_table_get(r->notes, "proxy-nocanon")) { + path = url; /* this is the raw path */ + } + else { + path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0, + r->proxyreq); + search = r->args; + } + if (path == NULL) + return HTTP_BAD_REQUEST; + + apr_snprintf(sport, sizeof(sport), ":%d", port); + + if (ap_strchr_c(host, ':')) { + /* if literal IPv6 address */ + host = apr_pstrcat(r->pool, "[", host, "]", NULL); + } + r->filename = apr_pstrcat(r->pool, "proxy:", scheme, "//", host, sport, + "/", path, (search) ? "?" : "", + (search) ? search : "", NULL); + return OK; +} + + +static int proxy_websocket_transfer(request_rec *r, conn_rec *c_i, conn_rec *c_o, + apr_bucket_brigade *bb, char *name) +{ + int rv; +#ifdef DEBUGGING + apr_off_t len; +#endif + + do { + apr_brigade_cleanup(bb); + rv = ap_get_brigade(c_i->input_filters, bb, AP_MODE_READBYTES, + APR_NONBLOCK_READ, AP_IOBUFSIZE); + if (rv == APR_SUCCESS) { + if (c_o->aborted) + return APR_EPIPE; + if (APR_BRIGADE_EMPTY(bb)) + break; +#ifdef DEBUGGING + len = -1; + apr_brigade_length(bb, 0, &len); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO() + "read %" APR_OFF_T_FMT + " bytes from %s", len, name); +#endif + rv = ap_pass_brigade(c_o->output_filters, bb); + if (rv == APR_SUCCESS) { + ap_fflush(c_o->output_filters, bb); + } + else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO() + "error on %s - ap_pass_brigade", + name); + } + } else if (!APR_STATUS_IS_EAGAIN(rv)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, APLOGNO() + "error on %s - ap_get_brigade", + name); + } + } while (rv == APR_SUCCESS); + + if (APR_STATUS_IS_EAGAIN(rv)) { + rv = APR_SUCCESS; + } + return rv; +} + + +/* + * process the request and write the response. + */ +static int ap_proxy_websocket_request(apr_pool_t *p, request_rec *r, + proxy_conn_rec *conn, + conn_rec *origin, + proxy_dir_conf *conf, + apr_uri_t *uri, + char *url, char *server_portstr) +{ + apr_status_t rv = APR_SUCCESS; + apr_pollset_t *pollset; + apr_pollfd_t pollfd; + const apr_pollfd_t *signalled; + apr_int32_t pollcnt, pi; + apr_int16_t pollevent; + conn_rec *c = r->connection; + apr_socket_t *sock = conn->sock; + conn_rec *backconn = conn->connection; + int client_error = 0; + apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc); + apr_socket_t *client_socket = ap_get_conn_socket(c); + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "setting up poll()"); + + if ((rv = apr_pollset_create(&pollset, 2, p, 0)) != APR_SUCCESS) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO() + "error apr_pollset_create()"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + pollfd.p = p; + pollfd.desc_type = APR_POLL_SOCKET; + pollfd.reqevents = APR_POLLIN; + pollfd.desc.s = sock; + pollfd.client_data = NULL; + apr_pollset_add(pollset, &pollfd); + + pollfd.desc.s = client_socket; + apr_pollset_add(pollset, &pollfd); + + + r->output_filters = c->output_filters; + r->proto_output_filters = c->output_filters; + r->input_filters = c->input_filters; + r->proto_input_filters = c->input_filters; + + while (1) { /* Infinite loop until error (one side closes the connection) */ + if ((rv = apr_pollset_poll(pollset, -1, &pollcnt, &signalled)) + != APR_SUCCESS) { + if (APR_STATUS_IS_EINTR(rv)) { + continue; + } + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO() "error apr_poll()"); + return HTTP_INTERNAL_SERVER_ERROR; + } +#ifdef DEBUGGING + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO() + "woke from poll(), i=%d", pollcnt); +#endif + + for (pi = 0; pi < pollcnt; pi++) { + const apr_pollfd_t *cur = &signalled[pi]; + + if (cur->desc.s == sock) { + pollevent = cur->rtnevents; + if (pollevent & APR_POLLIN) { +#ifdef DEBUGGING + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO() + "sock was readable"); +#endif + rv = proxy_websocket_transfer(r, backconn, c, bb, "sock"); + } + else if ((pollevent & APR_POLLERR) + || (pollevent & APR_POLLHUP)) { + rv = APR_EPIPE; + ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO() + "err/hup on backconn"); + } + if (rv != APR_SUCCESS) + client_error = 1; + } + else if (cur->desc.s == client_socket) { + pollevent = cur->rtnevents; + if (pollevent & APR_POLLIN) { +#ifdef DEBUGGING + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO() + "client was readable"); +#endif + rv = proxy_websocket_transfer(r, c, backconn, bb, "client"); + } + } + else { + rv = APR_EBADF; + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO() + "unknown socket in pollset"); + } + + } + if (rv != APR_SUCCESS) { + break; + } + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "finished with poll() - cleaning up"); + + if (client_error) { + return HTTP_INTERNAL_SERVER_ERROR; + } + return OK; +} + +/* + */ +static int proxy_websocket_handler(request_rec *r, proxy_worker *worker, + proxy_server_conf *conf, + char *url, const char *proxyname, + apr_port_t proxyport) +{ + int status; + char server_portstr[32]; + conn_rec *origin = NULL; + proxy_conn_rec *backend = NULL; + char *scheme; + int retry; + proxy_dir_conf *dconf = ap_get_module_config(r->per_dir_config, + &proxy_module); + apr_pool_t *p = r->pool; + apr_uri_t *uri; + + if (strncasecmp(url, "wss:", 4) == 0) { + scheme = "WSS"; + } + else if (strncasecmp(url, "ws:", 3) == 0) { + scheme = "WS"; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO() "declining URL %s", url); + return DECLINED; + } + + uri = apr_palloc(p, sizeof(*uri)); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO() "serving URL %s", url); + + /* create space for state information */ + status = ap_proxy_acquire_connection(scheme, &backend, worker, + r->server); + if (status != OK) { + if (backend) { + backend->close = 1; + ap_proxy_release_connection(scheme, backend, r->server); + } + return status; + } + + backend->is_ssl = 0; + backend->close = 0; + + retry = 0; + while (retry < 2) { + char *locurl = url; + /* Step One: Determine Who To Connect To */ + status = ap_proxy_determine_connection(p, r, conf, worker, backend, + uri, &locurl, proxyname, proxyport, + server_portstr, + sizeof(server_portstr)); + + if (status != OK) + break; + + /* Step Two: Make the Connection */ + if (ap_proxy_connect_backend(scheme, backend, worker, r->server)) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO() + "failed to make connection to backend: %s", + backend->hostname); + status = HTTP_SERVICE_UNAVAILABLE; + break; + } + + /* Step Three: Process the Request */ + status = ap_proxy_websocket_request(p, r, backend, origin, dconf, uri, locurl, + server_portstr); + break; + } + + /* Do not close the socket */ + ap_proxy_release_connection(scheme, backend, r->server); + return status; +} + +static void ap_proxy_http_register_hook(apr_pool_t *p) +{ + proxy_hook_scheme_handler(proxy_websocket_handler, NULL, NULL, APR_HOOK_FIRST); + proxy_hook_canon_handler(proxy_websocket_canon, NULL, NULL, APR_HOOK_FIRST); +} + +AP_DECLARE_MODULE(proxy_websocket) = { + STANDARD20_MODULE_STUFF, + NULL, /* create per-directory config structure */ + NULL, /* merge per-directory config structures */ + NULL, /* create per-server config structure */ + NULL, /* merge per-server config structures */ + NULL, /* command apr_table_t */ + ap_proxy_http_register_hook /* register hooks */ +}; + -- 2.40.0