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
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17#include "mod_proxy.h"
18
19module AP_MODULE_DECLARE_DATA proxy_wstunnel_module;
20
21/*
22 * Canonicalise http-like URLs.
23 * scheme is the scheme for the URL
24 * url is the URL starting with the first '/'
25 * def_port is the default port for this scheme.
26 */
27static int proxy_wstunnel_canon(request_rec *r, char *url)
28{
29    char *host, *path, sport[7];
30    char *search = NULL;
31    const char *err;
32    char *scheme;
33    apr_port_t port, def_port;
34
35    /* ap_port_of_scheme() */
36    if (strncasecmp(url, "ws:", 3) == 0) {
37        url += 3;
38        scheme = "ws:";
39        def_port = apr_uri_port_of_scheme("http");
40    }
41    else if (strncasecmp(url, "wss:", 4) == 0) {
42        url += 4;
43        scheme = "wss:";
44        def_port = apr_uri_port_of_scheme("https");
45    }
46    else {
47        return DECLINED;
48    }
49
50    port = def_port;
51    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "canonicalising URL %s", url);
52
53    /*
54     * do syntactic check.
55     * We break the URL into host, port, path, search
56     */
57    err = ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port);
58    if (err) {
59        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02439) "error parsing URL %s: %s",
60                      url, err);
61        return HTTP_BAD_REQUEST;
62    }
63
64    /*
65     * now parse path/search args, according to rfc1738:
66     * process the path. With proxy-nocanon set (by
67     * mod_proxy) we use the raw, unparsed uri
68     */
69    if (apr_table_get(r->notes, "proxy-nocanon")) {
70        path = url;   /* this is the raw path */
71    }
72    else {
73        path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0,
74                                 r->proxyreq);
75        search = r->args;
76    }
77    if (path == NULL)
78        return HTTP_BAD_REQUEST;
79
80    apr_snprintf(sport, sizeof(sport), ":%d", port);
81
82    if (ap_strchr_c(host, ':')) {
83        /* if literal IPv6 address */
84        host = apr_pstrcat(r->pool, "[", host, "]", NULL);
85    }
86    r->filename = apr_pstrcat(r->pool, "proxy:", scheme, "//", host, sport,
87                              "/", path, (search) ? "?" : "",
88                              (search) ? search : "", NULL);
89    return OK;
90}
91
92
93static int proxy_wstunnel_transfer(request_rec *r, conn_rec *c_i, conn_rec *c_o,
94                                     apr_bucket_brigade *bb, char *name)
95{
96    int rv;
97#ifdef DEBUGGING
98    apr_off_t len;
99#endif
100
101    do {
102        apr_brigade_cleanup(bb);
103        rv = ap_get_brigade(c_i->input_filters, bb, AP_MODE_READBYTES,
104                            APR_NONBLOCK_READ, AP_IOBUFSIZE);
105        if (rv == APR_SUCCESS) {
106            if (c_o->aborted)
107                return APR_EPIPE;
108            if (APR_BRIGADE_EMPTY(bb))
109                break;
110#ifdef DEBUGGING
111            len = -1;
112            apr_brigade_length(bb, 0, &len);
113            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02440)
114                          "read %" APR_OFF_T_FMT
115                          " bytes from %s", len, name);
116#endif
117            rv = ap_pass_brigade(c_o->output_filters, bb);
118            if (rv == APR_SUCCESS) {
119                ap_fflush(c_o->output_filters, bb);
120            }
121            else {
122                ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02441)
123                              "error on %s - ap_pass_brigade",
124                              name);
125            }
126        } else if (!APR_STATUS_IS_EAGAIN(rv) && !APR_STATUS_IS_EOF(rv)) {
127            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, rv, r, APLOGNO(02442)
128                          "error on %s - ap_get_brigade",
129                          name);
130        }
131    } while (rv == APR_SUCCESS);
132
133    if (APR_STATUS_IS_EAGAIN(rv)) {
134        rv = APR_SUCCESS;
135    }
136    return rv;
137}
138
139/* Search thru the input filters and remove the reqtimeout one */
140static void remove_reqtimeout(ap_filter_t *next)
141{
142    ap_filter_t *reqto = NULL;
143    ap_filter_rec_t *filter;
144
145    filter = ap_get_input_filter_handle("reqtimeout");
146    if (!filter) {
147        return;
148    }
149
150    while (next) {
151        if (next->frec == filter) {
152            reqto = next;
153            break;
154        }
155        next = next->next;
156    }
157    if (reqto) {
158        ap_remove_input_filter(reqto);
159    }
160}
161
162/*
163 * process the request and write the response.
164 */
165static int ap_proxy_wstunnel_request(apr_pool_t *p, request_rec *r,
166                                proxy_conn_rec *conn,
167                                proxy_worker *worker,
168                                proxy_server_conf *conf,
169                                apr_uri_t *uri,
170                                char *url, char *server_portstr)
171{
172    apr_status_t rv = APR_SUCCESS;
173    apr_pollset_t *pollset;
174    apr_pollfd_t pollfd;
175    const apr_pollfd_t *signalled;
176    apr_int32_t pollcnt, pi;
177    apr_int16_t pollevent;
178    conn_rec *c = r->connection;
179    apr_socket_t *sock = conn->sock;
180    conn_rec *backconn = conn->connection;
181    int client_error = 0;
182    char *buf;
183    apr_bucket_brigade *header_brigade;
184    apr_bucket *e;
185    char *old_cl_val = NULL;
186    char *old_te_val = NULL;
187    apr_bucket_brigade *bb = apr_brigade_create(p, c->bucket_alloc);
188    apr_socket_t *client_socket = ap_get_conn_socket(c);
189
190    header_brigade = apr_brigade_create(p, backconn->bucket_alloc);
191
192    ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "sending request");
193
194    rv = ap_proxy_create_hdrbrgd(p, header_brigade, r, conn,
195                                 worker, conf, uri, url, server_portstr,
196                                 &old_cl_val, &old_te_val);
197    if (rv != OK) {
198        return rv;
199    }
200
201    buf = apr_pstrcat(p, "Upgrade: WebSocket", CRLF, "Connection: Upgrade", CRLF, CRLF, NULL);
202    ap_xlate_proto_to_ascii(buf, strlen(buf));
203    e = apr_bucket_pool_create(buf, strlen(buf), p, c->bucket_alloc);
204    APR_BRIGADE_INSERT_TAIL(header_brigade, e);
205
206    if ((rv = ap_proxy_pass_brigade(c->bucket_alloc, r, conn, backconn,
207                                    header_brigade, 1)) != OK)
208        return rv;
209
210    ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "setting up poll()");
211
212    if ((rv = apr_pollset_create(&pollset, 2, p, 0)) != APR_SUCCESS) {
213        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02443)
214                      "error apr_pollset_create()");
215        return HTTP_INTERNAL_SERVER_ERROR;
216    }
217
218#if 0
219    apr_socket_opt_set(sock, APR_SO_NONBLOCK, 1);
220    apr_socket_opt_set(sock, APR_SO_KEEPALIVE, 1);
221    apr_socket_opt_set(client_socket, APR_SO_NONBLOCK, 1);
222    apr_socket_opt_set(client_socket, APR_SO_KEEPALIVE, 1);
223#endif
224
225    pollfd.p = p;
226    pollfd.desc_type = APR_POLL_SOCKET;
227    pollfd.reqevents = APR_POLLIN;
228    pollfd.desc.s = sock;
229    pollfd.client_data = NULL;
230    apr_pollset_add(pollset, &pollfd);
231
232    pollfd.desc.s = client_socket;
233    apr_pollset_add(pollset, &pollfd);
234
235
236    r->output_filters = c->output_filters;
237    r->proto_output_filters = c->output_filters;
238    r->input_filters = c->input_filters;
239    r->proto_input_filters = c->input_filters;
240
241    remove_reqtimeout(r->input_filters);
242
243    while (1) { /* Infinite loop until error (one side closes the connection) */
244        if ((rv = apr_pollset_poll(pollset, -1, &pollcnt, &signalled))
245            != APR_SUCCESS) {
246            if (APR_STATUS_IS_EINTR(rv)) {
247                continue;
248            }
249            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(02444) "error apr_poll()");
250            return HTTP_INTERNAL_SERVER_ERROR;
251        }
252        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02445)
253                      "woke from poll(), i=%d", pollcnt);
254
255        for (pi = 0; pi < pollcnt; pi++) {
256            const apr_pollfd_t *cur = &signalled[pi];
257
258            if (cur->desc.s == sock) {
259                pollevent = cur->rtnevents;
260                if (pollevent & APR_POLLIN) {
261                    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02446)
262                                  "sock was readable");
263                    rv = proxy_wstunnel_transfer(r, backconn, c, bb, "sock");
264                    }
265                else if ((pollevent & APR_POLLERR)
266                         || (pollevent & APR_POLLHUP)) {
267                         rv = APR_EPIPE;
268                         ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02447)
269                                       "err/hup on backconn");
270                }
271                else {
272                    rv = APR_EGENERAL;
273                    ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02605)
274                            "unknown event on backconn %d", pollevent);
275                }
276                if (rv != APR_SUCCESS)
277                    client_error = 1;
278            }
279            else if (cur->desc.s == client_socket) {
280                pollevent = cur->rtnevents;
281                if (pollevent & APR_POLLIN) {
282                    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02448)
283                                  "client was readable");
284                    rv = proxy_wstunnel_transfer(r, c, backconn, bb, "client");
285                }
286                else if ((pollevent & APR_POLLERR)
287                        || (pollevent & APR_POLLHUP)) {
288                    rv = APR_EPIPE;
289                    c->aborted = 1;
290                    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, APLOGNO(02607)
291                            "err/hup on client conn");
292                }
293                else {
294                    rv = APR_EGENERAL;
295                    ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, APLOGNO(02606)
296                            "unknown event on client conn %d", pollevent);
297                }
298            }
299            else {
300                rv = APR_EBADF;
301                ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(02449)
302                              "unknown socket in pollset");
303            }
304
305        }
306        if (rv != APR_SUCCESS) {
307            break;
308        }
309    }
310
311    ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
312                  "finished with poll() - cleaning up");
313
314    if (client_error) {
315        return HTTP_INTERNAL_SERVER_ERROR;
316    }
317    return OK;
318}
319
320/*
321 */
322static int proxy_wstunnel_handler(request_rec *r, proxy_worker *worker,
323                             proxy_server_conf *conf,
324                             char *url, const char *proxyname,
325                             apr_port_t proxyport)
326{
327    int status;
328    char server_portstr[32];
329    proxy_conn_rec *backend = NULL;
330    char *scheme;
331    int retry;
332    conn_rec *c = r->connection;
333    apr_pool_t *p = r->pool;
334    apr_uri_t *uri;
335
336    if (strncasecmp(url, "wss:", 4) == 0) {
337        scheme = "WSS";
338    }
339    else if (strncasecmp(url, "ws:", 3) == 0) {
340        scheme = "WS";
341    }
342    else {
343        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02450) "declining URL %s", url);
344        return DECLINED;
345    }
346
347    uri = apr_palloc(p, sizeof(*uri));
348    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(02451) "serving URL %s", url);
349
350    /* create space for state information */
351    status = ap_proxy_acquire_connection(scheme, &backend, worker,
352                                         r->server);
353    if (status != OK) {
354        if (backend) {
355            backend->close = 1;
356            ap_proxy_release_connection(scheme, backend, r->server);
357        }
358        return status;
359    }
360
361    backend->is_ssl = 0;
362    backend->close = 0;
363
364    retry = 0;
365    while (retry < 2) {
366        char *locurl = url;
367        /* Step One: Determine Who To Connect To */
368        status = ap_proxy_determine_connection(p, r, conf, worker, backend,
369                                               uri, &locurl, proxyname, proxyport,
370                                               server_portstr,
371                                               sizeof(server_portstr));
372
373        if (status != OK)
374            break;
375
376        /* Step Two: Make the Connection */
377        if (ap_proxy_connect_backend(scheme, backend, worker, r->server)) {
378            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02452)
379                          "failed to make connection to backend: %s",
380                          backend->hostname);
381            status = HTTP_SERVICE_UNAVAILABLE;
382            break;
383        }
384        /* Step Three: Create conn_rec */
385        if (!backend->connection) {
386            if ((status = ap_proxy_connection_create(scheme, backend,
387                                                     c, r->server)) != OK)
388                break;
389         }
390
391        /* Step Three: Process the Request */
392        status = ap_proxy_wstunnel_request(p, r, backend, worker, conf, uri, locurl,
393                                      server_portstr);
394        break;
395    }
396
397    /* Do not close the socket */
398    ap_proxy_release_connection(scheme, backend, r->server);
399    return status;
400}
401
402static void ap_proxy_http_register_hook(apr_pool_t *p)
403{
404    proxy_hook_scheme_handler(proxy_wstunnel_handler, NULL, NULL, APR_HOOK_FIRST);
405    proxy_hook_canon_handler(proxy_wstunnel_canon, NULL, NULL, APR_HOOK_FIRST);
406}
407
408AP_DECLARE_MODULE(proxy_wstunnel) = {
409    STANDARD20_MODULE_STUFF,
410    NULL,                       /* create per-directory config structure */
411    NULL,                       /* merge per-directory config structures */
412    NULL,                       /* create per-server config structure */
413    NULL,                       /* merge per-server config structures */
414    NULL,                       /* command apr_table_t */
415    ap_proxy_http_register_hook /* register hooks */
416};
417