1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2 * contributor license agreements. See the NOTICE file distributed with
3 * this work for additional information regarding copyright ownership.
4 * The ASF licenses this file to You under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #include "mod_proxy.h"
20 module AP_MODULE_DECLARE_DATA proxy_wstunnel_module;
24 apr_time_t idle_timeout;
25 apr_time_t async_delay;
28 typedef struct ws_baton_t {
30 proxy_conn_rec *proxy_connrec;
31 apr_socket_t *server_soc;
32 apr_socket_t *client_soc;
33 apr_pollset_t *pollset;
34 apr_bucket_brigade *bb_i;
35 apr_bucket_brigade *bb_o;
36 apr_pool_t *subpool; /* cleared before each suspend, destroyed when request ends */
37 char *scheme; /* required to release the proxy connection */
40 static void proxy_wstunnel_callback(void *b);
42 static int proxy_wstunnel_pump(ws_baton_t *baton, apr_time_t timeout, int try_async) {
43 request_rec *r = baton->r;
44 conn_rec *c = r->connection;
45 proxy_conn_rec *conn = baton->proxy_connrec;
46 apr_socket_t *sock = conn->sock;
47 conn_rec *backconn = conn->connection;
48 const apr_pollfd_t *signalled;
49 apr_int32_t pollcnt, pi;
50 apr_int16_t pollevent;
51 apr_pollset_t *pollset = baton->pollset;
52 apr_socket_t *client_socket = baton->client_soc;
54 apr_bucket_brigade *bb_i = baton->bb_i;
55 apr_bucket_brigade *bb_o = baton->bb_o;
56 int done = 0, replied = 0;
59 rv = apr_pollset_poll(pollset, timeout, &pollcnt, &signalled);
60 if (rv != APR_SUCCESS) {
61 if (APR_STATUS_IS_EINTR(rv)) {
64 else if (APR_STATUS_IS_TIMEUP(rv)) {
66 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02542) "Attempting to go async");
70 return HTTP_REQUEST_TIME_OUT;
74 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02444) "error apr_poll()");
75 return HTTP_INTERNAL_SERVER_ERROR;
79 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02445)
80 "woke from poll(), i=%d", pollcnt);
82 for (pi = 0; pi < pollcnt; pi++) {
83 const apr_pollfd_t *cur = &signalled[pi];
85 if (cur->desc.s == sock) {
86 pollevent = cur->rtnevents;
87 if (pollevent & (APR_POLLIN | APR_POLLHUP)) {
88 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02446)
90 done |= ap_proxy_transfer_between_connections(r, backconn,
97 else if (pollevent & APR_POLLERR) {
98 ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02447)
100 backconn->aborted = 1;
104 ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02605)
105 "unknown event on backconn %d", pollevent);
109 else if (cur->desc.s == client_socket) {
110 pollevent = cur->rtnevents;
111 if (pollevent & (APR_POLLIN | APR_POLLHUP)) {
112 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02448)
113 "client was readable");
114 done |= ap_proxy_transfer_between_connections(r, c, backconn,
122 else if (pollevent & APR_POLLERR) {
123 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02607)
124 "error on client conn");
129 ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02606)
130 "unknown event on client conn %d", pollevent);
135 ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02449)
136 "unknown socket in pollset");
143 ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
144 "finished with poll() - cleaning up");
147 return HTTP_BAD_GATEWAY;
154 static void proxy_wstunnel_finish(ws_baton_t *baton) {
155 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, baton->r, "proxy_wstunnel_finish");
156 baton->proxy_connrec->close = 1; /* new handshake expected on each back-conn */
157 baton->r->connection->keepalive = AP_CONN_CLOSE;
158 ap_proxy_release_connection(baton->scheme, baton->proxy_connrec, baton->r->server);
159 ap_finalize_request_protocol(baton->r);
160 ap_lingering_close(baton->r->connection);
161 apr_socket_close(baton->client_soc);
162 ap_mpm_resume_suspended(baton->r->connection);
163 ap_process_request_after_handler(baton->r); /* don't touch baton or r after here */
166 /* If neither socket becomes readable in the specified timeout,
167 * this callback will kill the request. We do not have to worry about
168 * having a cancel and a IO both queued.
170 static void proxy_wstunnel_cancel_callback(void *b)
172 ws_baton_t *baton = (ws_baton_t*)b;
173 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, baton->r, "proxy_wstunnel_cancel_callback, IO timed out");
174 proxy_wstunnel_finish(baton);
178 /* Invoked by the event loop when data is ready on either end.
179 * Pump both ends until they'd block and then start over again
180 * We don't need the invoke_mtx, since we never put multiple callback events
183 static void proxy_wstunnel_callback(void *b) {
185 apr_socket_t *sockets[3] = {NULL, NULL, NULL};
186 ws_baton_t *baton = (ws_baton_t*)b;
187 proxyws_dir_conf *dconf = ap_get_module_config(baton->r->per_dir_config, &proxy_wstunnel_module);
188 apr_pool_clear(baton->subpool);
189 status = proxy_wstunnel_pump(baton, dconf->async_delay, dconf->is_async);
190 if (status == SUSPENDED) {
191 sockets[0] = baton->client_soc;
192 sockets[1] = baton->server_soc;
193 ap_mpm_register_socket_callback_timeout(sockets, baton->subpool, 1,
194 proxy_wstunnel_callback,
195 proxy_wstunnel_cancel_callback,
197 dconf->idle_timeout);
198 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, baton->r, "proxy_wstunnel_callback suspend");
201 proxy_wstunnel_finish(baton);
207 * Canonicalise http-like URLs.
208 * scheme is the scheme for the URL
209 * url is the URL starting with the first '/'
210 * def_port is the default port for this scheme.
212 static int proxy_wstunnel_canon(request_rec *r, char *url)
214 char *host, *path, sport[7];
218 apr_port_t port, def_port;
220 /* ap_port_of_scheme() */
221 if (ap_casecmpstrn(url, "ws:", 3) == 0) {
224 def_port = apr_uri_port_of_scheme("http");
226 else if (ap_casecmpstrn(url, "wss:", 4) == 0) {
229 def_port = apr_uri_port_of_scheme("https");
236 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "canonicalising URL %s", url);
239 * do syntactic check.
240 * We break the URL into host, port, path, search
242 err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port);
244 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02439) "error parsing URL %s: %s",
246 return HTTP_BAD_REQUEST;
250 * now parse path/search args, according to rfc1738:
251 * process the path. With proxy-nocanon set (by
252 * mod_proxy) we use the raw, unparsed uri
254 if (apr_table_get(r->notes, "proxy-nocanon")) {
255 path = url; /* this is the raw path */
258 path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0,
263 return HTTP_BAD_REQUEST;
265 apr_snprintf(sport, sizeof(sport), ":%d", port);
267 if (ap_strchr_c(host, ':')) {
268 /* if literal IPv6 address */
269 host = apr_pstrcat(r->pool, "[", host, "]", NULL);
271 r->filename = apr_pstrcat(r->pool, "proxy:", scheme, "//", host, sport,
272 "/", path, (search) ? "?" : "",
273 (search) ? search : "", NULL);
278 * process the request and write the response.
280 static int proxy_wstunnel_request(apr_pool_t *p, request_rec *r,
281 proxy_conn_rec *conn,
282 proxy_worker *worker,
283 proxy_server_conf *conf,
285 char *url, char *server_portstr, char *scheme)
288 apr_pollset_t *pollset;
290 conn_rec *c = r->connection;
291 apr_socket_t *sock = conn->sock;
292 conn_rec *backconn = conn->connection;
294 apr_bucket_brigade *header_brigade;
296 char *old_cl_val = NULL;
297 char *old_te_val = NULL;
298 apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc);
299 apr_socket_t *client_socket = ap_get_conn_socket(c);
300 ws_baton_t *baton = apr_pcalloc(r->pool, sizeof(ws_baton_t));
301 apr_socket_t *sockets[3] = {NULL, NULL, NULL};
303 proxyws_dir_conf *dconf = ap_get_module_config(r->per_dir_config, &proxy_wstunnel_module);
305 header_brigade = apr_brigade_create(p, backconn->bucket_alloc);
307 ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "sending request");
309 rv = ap_proxy_create_hdrbrgd(p, header_brigade, r, conn,
310 worker, conf, uri, url, server_portstr,
311 &old_cl_val, &old_te_val);
316 buf = apr_pstrdup(p, "Upgrade: WebSocket" CRLF "Connection: Upgrade" CRLF CRLF);
317 ap_xlate_proto_to_ascii(buf, strlen(buf));
318 e = apr_bucket_pool_create(buf, strlen(buf), p, c->bucket_alloc);
319 APR_BRIGADE_INSERT_TAIL(header_brigade, e);
321 if ((rv = ap_proxy_pass_brigade(backconn->bucket_alloc, r, conn, backconn,
322 header_brigade, 1)) != OK)
325 apr_brigade_cleanup(header_brigade);
327 ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "setting up poll()");
329 if ((rv = apr_pollset_create(&pollset, 2, p, 0)) != APR_SUCCESS) {
330 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02443)
331 "error apr_pollset_create()");
332 return HTTP_INTERNAL_SERVER_ERROR;
336 apr_socket_opt_set(sock, APR_SO_NONBLOCK, 1);
337 apr_socket_opt_set(sock, APR_SO_KEEPALIVE, 1);
338 apr_socket_opt_set(client_socket, APR_SO_NONBLOCK, 1);
339 apr_socket_opt_set(client_socket, APR_SO_KEEPALIVE, 1);
343 pollfd.desc_type = APR_POLL_SOCKET;
344 pollfd.reqevents = APR_POLLIN | APR_POLLHUP;
345 pollfd.desc.s = sock;
346 pollfd.client_data = NULL;
347 apr_pollset_add(pollset, &pollfd);
349 pollfd.desc.s = client_socket;
350 apr_pollset_add(pollset, &pollfd);
352 ap_remove_input_filter_byhandle(c->input_filters, "reqtimeout");
354 r->output_filters = c->output_filters;
355 r->proto_output_filters = c->output_filters;
356 r->input_filters = c->input_filters;
357 r->proto_input_filters = c->input_filters;
359 /* This handler should take care of the entire connection; make it so that
360 * nothing else is attempted on the connection after returning. */
361 c->keepalive = AP_CONN_CLOSE;
364 baton->pollset = pollset;
365 baton->client_soc = client_socket;
366 baton->server_soc = sock;
367 baton->proxy_connrec = conn;
369 baton->bb_i = header_brigade;
370 baton->scheme = scheme;
371 apr_pool_create(&baton->subpool, r->pool);
373 if (!dconf->is_async) {
374 status = proxy_wstunnel_pump(baton, dconf->idle_timeout, dconf->is_async);
377 status = proxy_wstunnel_pump(baton, dconf->async_delay, dconf->is_async);
378 apr_pool_clear(baton->subpool);
379 if (status == SUSPENDED) {
380 sockets[0] = baton->client_soc;
381 sockets[1] = baton->server_soc;
382 rv = ap_mpm_register_socket_callback_timeout(sockets, baton->subpool, 1,
383 proxy_wstunnel_callback,
384 proxy_wstunnel_cancel_callback,
386 dconf->idle_timeout);
387 if (rv == APR_SUCCESS) {
390 else if (APR_STATUS_IS_ENOTIMPL(rv)) {
391 ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02544) "No async support");
392 status = proxy_wstunnel_pump(baton, dconf->idle_timeout, 0); /* force no async */
395 ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
396 APLOGNO(02543) "error creating websockets tunnel");
397 return HTTP_INTERNAL_SERVER_ERROR;
403 /* Avoid sending error pages down an upgraded connection */
404 if (status != HTTP_REQUEST_TIME_OUT) {
414 static int proxy_wstunnel_handler(request_rec *r, proxy_worker *worker,
415 proxy_server_conf *conf,
416 char *url, const char *proxyname,
417 apr_port_t proxyport)
420 char server_portstr[32];
421 proxy_conn_rec *backend = NULL;
424 conn_rec *c = r->connection;
425 apr_pool_t *p = r->pool;
430 if (ap_casecmpstrn(url, "wss:", 4) == 0) {
434 else if (ap_casecmpstrn(url, "ws:", 3) == 0) {
438 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02450) "declining URL %s", url);
442 upgrade = apr_table_get(r->headers_in, "Upgrade");
443 if (!upgrade || ap_casecmpstr(upgrade, "WebSocket") != 0) {
444 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02900)
445 "declining URL %s (not WebSocket)", url);
449 uri = apr_palloc(p, sizeof(*uri));
450 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02451) "serving URL %s", url);
452 /* create space for state information */
453 status = ap_proxy_acquire_connection(scheme, &backend, worker, r->server);
458 backend->is_ssl = is_ssl;
461 /* Step One: Determine Who To Connect To */
462 status = ap_proxy_determine_connection(p, r, conf, worker, backend,
463 uri, &locurl, proxyname, proxyport,
465 sizeof(server_portstr));
470 /* Step Two: Make the Connection */
471 if (ap_proxy_connect_backend(scheme, backend, worker, r->server)) {
472 ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02452)
473 "failed to make connection to backend: %s",
475 status = HTTP_SERVICE_UNAVAILABLE;
479 /* Step Three: Create conn_rec */
480 if (!backend->connection) {
481 status = ap_proxy_connection_create(scheme, backend, c, r->server);
487 /* Step Three: Process the Request */
488 status = proxy_wstunnel_request(p, r, backend, worker, conf, uri, locurl,
489 server_portstr, scheme);
492 /* Do not close the socket */
493 if (backend && status != SUSPENDED) {
495 ap_proxy_release_connection(scheme, backend, r->server);
500 static void *create_proxyws_dir_config(apr_pool_t *p, char *dummy)
502 proxyws_dir_conf *new =
503 (proxyws_dir_conf *) apr_pcalloc(p, sizeof(proxyws_dir_conf));
505 new->idle_timeout = -1; /* no timeout */
510 static const char * proxyws_set_idle(cmd_parms *cmd, void *conf, const char *val)
512 proxyws_dir_conf *dconf = conf;
513 if (ap_timeout_parameter_parse(val, &(dconf->idle_timeout), "s") != APR_SUCCESS)
514 return "ProxyWebsocketIdleTimeout timeout has wrong format";
517 static const char * proxyws_set_aysnch_delay(cmd_parms *cmd, void *conf, const char *val)
519 proxyws_dir_conf *dconf = conf;
520 if (ap_timeout_parameter_parse(val, &(dconf->async_delay), "s") != APR_SUCCESS)
521 return "ProxyWebsocketAsyncDelay timeout has wrong format";
525 static const command_rec ws_proxy_cmds[] =
527 AP_INIT_FLAG("ProxyWebsocketAsync", ap_set_flag_slot_char, (void*)APR_OFFSETOF(proxyws_dir_conf, is_async),
528 RSRC_CONF|ACCESS_CONF,
529 "on if idle websockets connections should be monitored asyncronously"),
531 AP_INIT_TAKE1("ProxyWebsocketIdleTimeout", proxyws_set_idle, NULL, RSRC_CONF|ACCESS_CONF,
532 "timeout for activity in either direction, unlimited by default"),
534 AP_INIT_TAKE1("ProxyWebsocketAsyncDelay", proxyws_set_aysnch_delay, NULL, RSRC_CONF|ACCESS_CONF,
535 "amount of time to poll before going asyncronous"),
539 static void ap_proxy_http_register_hook(apr_pool_t *p)
541 proxy_hook_scheme_handler(proxy_wstunnel_handler, NULL, NULL, APR_HOOK_FIRST);
542 proxy_hook_canon_handler(proxy_wstunnel_canon, NULL, NULL, APR_HOOK_FIRST);
545 AP_DECLARE_MODULE(proxy_wstunnel) = {
546 STANDARD20_MODULE_STUFF,
547 create_proxyws_dir_config, /* create per-directory config structure */
548 NULL, /* merge per-directory config structures */
549 NULL, /* create per-server config structure */
550 NULL, /* merge per-server config structures */
551 ws_proxy_cmds, /* command apr_table_t */
552 ap_proxy_http_register_hook /* register hooks */