diff options
Diffstat (limited to '')
-rw-r--r-- | modules/proxy/mod_proxy_wstunnel.c | 391 |
1 files changed, 391 insertions, 0 deletions
diff --git a/modules/proxy/mod_proxy_wstunnel.c b/modules/proxy/mod_proxy_wstunnel.c new file mode 100644 index 0000000..9dda010 --- /dev/null +++ b/modules/proxy/mod_proxy_wstunnel.c @@ -0,0 +1,391 @@ +/* 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_wstunnel_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_wstunnel_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(02439) "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; +} + +/* + * process the request and write the response. + */ +static int proxy_wstunnel_request(apr_pool_t *p, request_rec *r, + proxy_conn_rec *conn, + proxy_worker *worker, + proxy_server_conf *conf, + apr_uri_t *uri, + char *url, char *server_portstr) +{ + apr_status_t rv; + 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; + char *buf; + apr_bucket_brigade *header_brigade; + apr_bucket *e; + char *old_cl_val = NULL; + char *old_te_val = NULL; + apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc); + apr_socket_t *client_socket = ap_get_conn_socket(c); + int done = 0, replied = 0; + const char *upgrade_method = *worker->s->upgrade ? worker->s->upgrade : "WebSocket"; + + header_brigade = apr_brigade_create(p, backconn->bucket_alloc); + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "sending request"); + + rv = ap_proxy_create_hdrbrgd(p, header_brigade, r, conn, + worker, conf, uri, url, server_portstr, + &old_cl_val, &old_te_val); + if (rv != OK) { + return rv; + } + + if (ap_cstr_casecmp(upgrade_method, "NONE") == 0) { + buf = apr_pstrdup(p, "Upgrade: WebSocket" CRLF "Connection: Upgrade" CRLF CRLF); + } else if (ap_cstr_casecmp(upgrade_method, "ANY") == 0) { + const char *upgrade; + upgrade = apr_table_get(r->headers_in, "Upgrade"); + buf = apr_pstrcat(p, "Upgrade: ", upgrade, CRLF "Connection: Upgrade" CRLF CRLF, NULL); + } else { + buf = apr_pstrcat(p, "Upgrade: ", upgrade_method, CRLF "Connection: Upgrade" CRLF CRLF, NULL); + } + ap_xlate_proto_to_ascii(buf, strlen(buf)); + e = apr_bucket_pool_create(buf, strlen(buf), p, c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(header_brigade, e); + + if ((rv = ap_proxy_pass_brigade(backconn->bucket_alloc, r, conn, backconn, + header_brigade, 1)) != OK) + return rv; + + apr_brigade_cleanup(header_brigade); + + 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(02443) + "error apr_pollset_create()"); + return HTTP_INTERNAL_SERVER_ERROR; + } + +#if 0 + apr_socket_opt_set(sock, APR_SO_NONBLOCK, 1); + apr_socket_opt_set(sock, APR_SO_KEEPALIVE, 1); + apr_socket_opt_set(client_socket, APR_SO_NONBLOCK, 1); + apr_socket_opt_set(client_socket, APR_SO_KEEPALIVE, 1); +#endif + + pollfd.p = p; + pollfd.desc_type = APR_POLL_SOCKET; + pollfd.reqevents = APR_POLLIN | APR_POLLHUP; + pollfd.desc.s = sock; + pollfd.client_data = NULL; + apr_pollset_add(pollset, &pollfd); + + pollfd.desc.s = client_socket; + apr_pollset_add(pollset, &pollfd); + + ap_remove_input_filter_byhandle(c->input_filters, "reqtimeout"); + + 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; + + /* This handler should take care of the entire connection; make it so that + * nothing else is attempted on the connection after returning. */ + c->keepalive = AP_CONN_CLOSE; + + do { /* Loop until done (one side closes the connection, or an error) */ + rv = apr_pollset_poll(pollset, -1, &pollcnt, &signalled); + if (rv != APR_SUCCESS) { + if (APR_STATUS_IS_EINTR(rv)) { + continue; + } + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02444) "error apr_poll()"); + return HTTP_INTERNAL_SERVER_ERROR; + } + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02445) + "woke from poll(), i=%d", pollcnt); + + 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 | APR_POLLHUP)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02446) + "sock was readable"); + done |= ap_proxy_transfer_between_connections(r, backconn, + c, + header_brigade, + bb, "sock", + &replied, + AP_IOBUFSIZE, + 0) + != APR_SUCCESS; + } + else if (pollevent & APR_POLLERR) { + ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02447) + "error on backconn"); + backconn->aborted = 1; + done = 1; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02605) + "unknown event on backconn %d", pollevent); + done = 1; + } + } + else if (cur->desc.s == client_socket) { + pollevent = cur->rtnevents; + if (pollevent & (APR_POLLIN | APR_POLLHUP)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02448) + "client was readable"); + done |= ap_proxy_transfer_between_connections(r, c, + backconn, bb, + header_brigade, + "client", + NULL, + AP_IOBUFSIZE, + 0) + != APR_SUCCESS; + } + else if (pollevent & APR_POLLERR) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02607) + "error on client conn"); + c->aborted = 1; + done = 1; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02606) + "unknown event on client conn %d", pollevent); + done = 1; + } + } + else { + ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02449) + "unknown socket in pollset"); + done = 1; + } + + } + } while (!done); + + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "finished with poll() - cleaning up"); + + if (!replied) { + return HTTP_BAD_GATEWAY; + } + else { + return OK; + } + + return OK; +} + +/* + */ +static int proxy_wstunnel_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]; + proxy_conn_rec *backend = NULL; + char *scheme; + int retry; + apr_pool_t *p = r->pool; + apr_uri_t *uri; + int is_ssl = 0; + const char *upgrade_method = *worker->s->upgrade ? worker->s->upgrade : "WebSocket"; + + if (strncasecmp(url, "wss:", 4) == 0) { + scheme = "WSS"; + is_ssl = 1; + } + else if (strncasecmp(url, "ws:", 3) == 0) { + scheme = "WS"; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02450) "declining URL %s", url); + return DECLINED; + } + + if (ap_cstr_casecmp(upgrade_method, "NONE") != 0) { + const char *upgrade; + upgrade = apr_table_get(r->headers_in, "Upgrade"); + if (!upgrade || (ap_cstr_casecmp(upgrade, upgrade_method) != 0 && + ap_cstr_casecmp(upgrade_method, "ANY") !=0)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02900) + "declining URL %s (not %s, Upgrade: header is %s)", + url, upgrade_method, upgrade ? upgrade : "missing"); + return DECLINED; + } + } + + uri = apr_palloc(p, sizeof(*uri)); + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02451) "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 = is_ssl; + 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(02452) + "failed to make connection to backend: %s", + backend->hostname); + status = HTTP_SERVICE_UNAVAILABLE; + break; + } + + /* Step Three: Create conn_rec */ + if (!backend->connection) { + status = ap_proxy_connection_create_ex(scheme, backend, r); + if (status != OK) { + break; + } + } + + backend->close = 1; /* must be after ap_proxy_determine_connection */ + + + /* Step Three: Process the Request */ + status = proxy_wstunnel_request(p, r, backend, worker, conf, 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_wstunnel_handler, NULL, NULL, APR_HOOK_FIRST); + proxy_hook_canon_handler(proxy_wstunnel_canon, NULL, NULL, APR_HOOK_FIRST); +} + +AP_DECLARE_MODULE(proxy_wstunnel) = { + 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 */ +}; |