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