[doc] NEWS
[lighttpd.git] / src / mod_wstunnel.c
blobf05daac69e19cb6bddf0b6e0f4694db6c143e6b2
1 /*
2 * mod_wstunnel originally based off https://github.com/nori0428/mod_websocket
3 * Portions of this module Copyright(c) 2017, Glenn Strauss, All rights reserved
4 * Portions of this module Copyright(c) 2010, Norio Kobota, All rights reserved.
5 */
7 /*
8 * Copyright(c) 2010, Norio Kobota, All rights reserved.
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions are met:
13 * - Redistributions of source code must retain the above copyright notice,
14 * this list of conditions and the following disclaimer.
15 * - Redistributions in binary form must reproduce the above copyright notice,
16 * this list of conditions and the following disclaimer in the documentation
17 * and/or other materials provided with the distribution.
18 * - Neither the name of the 'incremental' nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
22 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
32 * THE POSSIBILITY OF SUCH DAMAGE.
35 /* NOTES:
37 * mod_wstunnel has been largely rewritten from Norio Kobota mod_websocket.
39 * highlighted differences from Norio Kobota mod_websocket
40 * - re-coded to use lighttpd 1.4.46 buffer, chunkqueue, and gw_backend APIs
41 * - websocket.server "ext" value is no longer regex;
42 * operates similar to mod_proxy for either path prefix or extension match
43 * - validation of "origins" value is no longer regex; operates as suffix match
44 * (admin could use lighttpd.conf regex on "Origin" or "Sec-WebSocket-Origin"
45 * and reject non-matches with mod_access if such regex validation required)
46 * - websocket transparent proxy mode removed; functionality is now in mod_proxy
47 * Backend server which responds to Connection: upgrade and Upgrade: websocket
48 * should check "Origin" and/or "Sec-WebSocket-Origin". lighttpd.conf could
49 * additionally be configured to check
50 * $REQUEST_HEADER["Sec-WebSocket-Origin"] !~ "..."
51 * with regex, and mod_access used to reject non-matches, if desired.
52 * - connections to backend no longer block, but only first address returned
53 * by getaddrinfo() is used; lighttpd does not cycle through all addresses
54 * returned by DNS resolution. Note: DNS resolution occurs once at startup.
55 * - directives renamed from websocket.* to wstunnel.*
56 * - directive websocket.ping_interval replaced with wstunnel.ping-interval
57 * (note the '_' changed to '-')
58 * - directive websocket.timeout should be replaced with server.max-read-idle
59 * - attribute "type" is an independent directive wstunnel.frame-type
60 * (default is "text" unless "binary" is specified)
61 * - attribute "origins" is an independent directive wstunnel.origins
62 * - attribute "proto" removed; mod_proxy can proxy to backend websocket server
63 * - attribute "subproto" should be replaced with mod_setenv directive
64 * setenv.set-response-header = ( "Sec-WebSocket-Protocol" => "..." )
65 * if header is required
67 * not reviewed:
68 * - websocket protocol compliance has not been reviewed
69 * e.g. when to send 1000 Normal Closure and when to send 1001 Going Away
70 * - websocket protocol sanity checking has not been reviewed
72 * References:
73 * https://en.wikipedia.org/wiki/WebSocket
74 * https://tools.ietf.org/html/rfc6455
75 * https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00
77 #include "first.h"
79 #include <sys/types.h>
80 #include <limits.h>
81 #include <stdlib.h>
82 #include <string.h>
84 #include "gw_backend.h"
86 #include "base.h"
87 #include "array.h"
88 #include "buffer.h"
89 #include "chunk.h"
90 #include "fdevent.h"
91 #include "http_header.h"
92 #include "joblist.h"
93 #include "log.h"
95 #define MOD_WEBSOCKET_LOG_NONE 0
96 #define MOD_WEBSOCKET_LOG_ERR 1
97 #define MOD_WEBSOCKET_LOG_WARN 2
98 #define MOD_WEBSOCKET_LOG_INFO 3
99 #define MOD_WEBSOCKET_LOG_DEBUG 4
101 #define DEBUG_LOG(level, format, ...) \
102 if (hctx->gw.conf.debug >= (level)) { \
103 log_error_write(hctx->srv, __FILE__, __LINE__, (format), __VA_ARGS__); \
106 typedef struct {
107 gw_plugin_config gw;
108 buffer *frame_type;
109 array *origins;
110 unsigned short int ping_interval;
111 } plugin_config;
113 typedef struct plugin_data {
114 PLUGIN_DATA;
115 plugin_config **config_storage;
116 plugin_config conf;
117 } plugin_data;
119 typedef enum {
120 MOD_WEBSOCKET_FRAME_STATE_INIT,
122 /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
123 MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH,
124 MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH,
125 MOD_WEBSOCKET_FRAME_STATE_READ_MASK,
126 /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
128 MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD
129 } mod_wstunnel_frame_state_t;
131 typedef enum {
132 MOD_WEBSOCKET_FRAME_TYPE_TEXT,
133 MOD_WEBSOCKET_FRAME_TYPE_BIN,
134 MOD_WEBSOCKET_FRAME_TYPE_CLOSE,
136 /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
137 MOD_WEBSOCKET_FRAME_TYPE_PING,
138 MOD_WEBSOCKET_FRAME_TYPE_PONG
139 /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
141 } mod_wstunnel_frame_type_t;
143 typedef struct {
144 uint64_t siz;
146 /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
147 int siz_cnt;
148 int mask_cnt;
149 #define MOD_WEBSOCKET_MASK_CNT 4
150 unsigned char mask[MOD_WEBSOCKET_MASK_CNT];
151 /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
153 } mod_wstunnel_frame_control_t;
155 typedef struct {
156 mod_wstunnel_frame_state_t state;
157 mod_wstunnel_frame_control_t ctl;
158 mod_wstunnel_frame_type_t type, type_before, type_backend;
159 buffer *payload;
160 } mod_wstunnel_frame_t;
162 typedef struct {
163 gw_handler_ctx gw;
164 mod_wstunnel_frame_t frame;
166 int hybivers;
167 time_t ping_ts;
168 int subproto;
170 server *srv; /*(for mod_wstunnel module-specific DEBUG_LOG() macro)*/
171 plugin_config conf;
172 } handler_ctx;
174 /* prototypes */
175 static handler_t mod_wstunnel_handshake_create_response(handler_ctx *);
176 static int mod_wstunnel_frame_send(handler_ctx *, mod_wstunnel_frame_type_t, const char *, size_t);
177 static int mod_wstunnel_frame_recv(handler_ctx *);
178 #define _MOD_WEBSOCKET_SPEC_IETF_00_
179 #define _MOD_WEBSOCKET_SPEC_RFC_6455_
181 INIT_FUNC(mod_wstunnel_init) {
182 return calloc(1, sizeof(plugin_data));
185 FREE_FUNC(mod_wstunnel_free) {
186 plugin_data *p = p_d;
187 if (p->config_storage) {
188 for (size_t i = 0; i < srv->config_context->used; ++i) {
189 plugin_config *s = p->config_storage[i];
190 if (NULL == s) continue;
191 buffer_free(s->frame_type);
192 array_free(s->origins);
193 /*assert(0 == offsetof(s->gw));*/
194 gw_plugin_config_free(&s->gw);
195 /*free(s);*//*free'd by gw_plugin_config_free()*/
197 free(p->config_storage);
199 free(p);
200 return HANDLER_GO_ON;
203 SETDEFAULTS_FUNC(mod_wstunnel_set_defaults) {
204 plugin_data *p = p_d;
205 data_unset *du;
206 config_values_t cv[] = {
207 { "wstunnel.server", NULL, T_CONFIG_LOCAL, T_CONFIG_SCOPE_CONNECTION },
208 { "wstunnel.debug", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION },
209 { "wstunnel.balance", NULL, T_CONFIG_LOCAL, T_CONFIG_SCOPE_CONNECTION },
210 { "wstunnel.map-extensions",NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION },
211 { "wstunnel.frame-type", NULL, T_CONFIG_STRING,T_CONFIG_SCOPE_CONNECTION },
212 { "wstunnel.origins", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION },
213 { "wstunnel.ping-interval", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION },
214 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
217 p->config_storage = calloc(srv->config_context->used, sizeof(plugin_config *));
218 force_assert(p->config_storage);
219 for (size_t i = 0; i < srv->config_context->used; ++i) {
220 array *ca = ((data_config *)(srv->config_context->data[i]))->value;
221 plugin_config *s = calloc(1, sizeof(plugin_config));
222 force_assert(s);
224 s->gw.debug = 0; /* MOD_WEBSOCKET_LOG_NONE */
225 s->gw.ext_mapping = array_init();
226 s->frame_type = buffer_init();
227 s->origins = array_init();
228 s->ping_interval = 0; /* do not send ping */
230 cv[0].destination = NULL; /* T_CONFIG_LOCAL */
231 cv[1].destination = &(s->gw.debug);
232 cv[2].destination = NULL; /* T_CONFIG_LOCAL */
233 cv[3].destination = s->gw.ext_mapping;
234 cv[4].destination = s->frame_type;
235 cv[5].destination = s->origins;
236 cv[6].destination = &(s->ping_interval);
238 p->config_storage[i] = s;
240 if (0 != config_insert_values_global(srv, ca, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
241 return HANDLER_ERROR;
244 du = array_get_element(ca, "wstunnel.server");
245 if (!gw_set_defaults_backend(srv, (gw_plugin_data *)p, du, i, 0)) {
246 return HANDLER_ERROR;
249 du = array_get_element(ca, "wstunnel.balance");
250 if (!gw_set_defaults_balance(srv, &s->gw, du)) {
251 return HANDLER_ERROR;
254 /* disable check-local for all exts (default enabled) */
255 if (s->gw.exts) { /*(check after gw_set_defaults_backend())*/
256 for (size_t j = 0; j < s->gw.exts->used; ++j) {
257 gw_extension *ex = s->gw.exts->exts[j];
258 for (size_t n = 0; n < ex->used; ++n) {
259 ex->hosts[n]->check_local = 0;
264 /* error if "mode" = "authorizer"; wstunnel can not act as authorizer */
265 /*(check after gw_set_defaults_backend())*/
266 if (s->gw.exts_auth && s->gw.exts_auth->used) {
267 log_error_write(srv, __FILE__, __LINE__, "s",
268 "wstunnel.server must not define any hosts "
269 "with attribute \"mode\" = \"authorizer\"");
270 return HANDLER_ERROR;
273 /*(default frame-type to "text" unless "binary" is specified)*/
274 if (!buffer_is_empty(s->frame_type)
275 && !buffer_is_equal_caseless_string(s->frame_type,
276 CONST_STR_LEN("binary"))) {
277 buffer_clear(s->frame_type);
280 if (!array_is_vlist(s->origins)) {
281 log_error_write(srv, __FILE__, __LINE__, "s",
282 "unexpected value for wstunnel.origins; expected wstunnel.origins = ( \"...\", \"...\" )");
283 return HANDLER_ERROR;
285 for (size_t j = 0; j < s->origins->used; ++j) {
286 if (buffer_string_is_empty(((data_string *)s->origins->data[j])->value)) {
287 log_error_write(srv, __FILE__, __LINE__, "s",
288 "unexpected empty string in wstunnel.origins");
289 return HANDLER_ERROR;
294 /*assert(0 == offsetof(s->gw));*/
295 return HANDLER_GO_ON;
298 static handler_t wstunnel_create_env(server *srv, gw_handler_ctx *gwhctx) {
299 handler_ctx *hctx = (handler_ctx *)gwhctx;
300 connection *con = hctx->gw.remote_conn;
301 handler_t rc;
302 if (0 == con->request.content_length) {
303 http_response_upgrade_read_body_unknown(srv, con);
304 chunkqueue_append_chunkqueue(con->request_content_queue,
305 con->read_queue);
307 rc = mod_wstunnel_handshake_create_response(hctx);
308 if (rc != HANDLER_GO_ON) return rc;
310 con->http_status = 101; /* Switching Protocols */
311 con->file_started = 1;
313 hctx->ping_ts = srv->cur_ts;
314 gw_set_transparent(srv, &hctx->gw);
316 return HANDLER_GO_ON;
319 static handler_t wstunnel_stdin_append(server *srv, gw_handler_ctx *gwhctx) {
320 /* prepare websocket frames to backend */
321 /* (caller should verify con->request_content_queue) */
322 /*assert(!chunkqueue_is_empty(con->request_content_queue));*/
323 handler_ctx *hctx = (handler_ctx *)gwhctx;
324 if (0 == mod_wstunnel_frame_recv(hctx))
325 return HANDLER_GO_ON;
326 else {
327 /*(error)*/
328 /* future: might differentiate client close request from client error,
329 * and then send 1000 or 1001 */
330 connection *con = hctx->gw.remote_conn;
331 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "sds",
332 "disconnected from client ( fd =", con->fd, ")");
333 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sds",
334 "send close response to client ( fd =", con->fd, ")");
335 mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, CONST_STR_LEN("1000")); /* 1000 Normal Closure */
336 gw_connection_reset(srv, con, hctx->gw.plugin_data);
337 return HANDLER_FINISHED;
341 static handler_t wstunnel_recv_parse(server *srv, connection *con, http_response_opts *opts, buffer *b, size_t n) {
342 handler_ctx *hctx = (handler_ctx *)opts->pdata;
343 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx",
344 "recv data from backend ( fd =", hctx->gw.fd, "), size =", n);
345 if (0 == n) return HANDLER_FINISHED;
346 if (mod_wstunnel_frame_send(hctx,hctx->frame.type_backend,b->ptr,n) < 0) {
347 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "fail to send data to client");
348 return HANDLER_ERROR;
350 buffer_clear(b);
351 UNUSED(srv);
352 UNUSED(con);
353 return HANDLER_GO_ON;
356 #define PATCH(x) p->conf.x = s->x
357 #define PATCH_GW(x) p->conf.gw.x = s->gw.x
358 static void mod_wstunnel_patch_connection(server *srv, connection *con, plugin_data *p) {
359 size_t i, j;
360 plugin_config *s = p->config_storage[0];
362 PATCH_GW(exts);
363 PATCH_GW(exts_auth);
364 PATCH_GW(exts_resp);
365 PATCH_GW(debug);
366 PATCH_GW(balance);
367 PATCH_GW(ext_mapping);
368 PATCH(frame_type);
369 PATCH(origins);
370 PATCH(ping_interval);
372 /* skip the first, the global context */
373 for (i = 1; i < srv->config_context->used; i++) {
374 data_config *dc = (data_config *)srv->config_context->data[i];
375 s = p->config_storage[i];
377 /* condition didn't match */
378 if (!config_check_cond(srv, con, dc)) {
379 continue;
381 /* merge config */
382 for (j = 0; j < dc->value->used; j++) {
383 data_unset *du = dc->value->data[j];
385 if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.server"))) {
386 PATCH_GW(exts);
387 /*(wstunnel can not act as authorizer,
388 * but p->conf.exts_auth must not be NULL)*/
389 PATCH_GW(exts_auth);
390 PATCH_GW(exts_resp);
391 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.debug"))) {
392 PATCH_GW(debug);
393 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.balance"))) {
394 PATCH_GW(balance);
395 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.map-extensions"))) {
396 PATCH_GW(ext_mapping);
397 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.frame-type"))) {
398 PATCH(frame_type);
399 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.origins"))) {
400 PATCH(origins);
401 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.ping-interval"))) {
402 PATCH(ping_interval);
407 #undef PATCH_GW
408 #undef PATCH
410 static int header_contains_token (buffer *b, const char *m, size_t mlen)
412 for (char *s = b->ptr; s; s = strchr(s, ',')) {
413 while (*s == ' ' || *s == '\t' || *s == ',') ++s;
414 if (0 == strncasecmp(s, m, mlen)) {
415 s += mlen;
416 if (*s == '\0' || *s == ' ' || *s == '\t' || *s == ',' || *s == ';')
417 return 1;
420 return 0;
423 static int wstunnel_is_allowed_origin(connection *con, handler_ctx *hctx) {
424 /* If allowed origins is set (and not empty list), fail closed if no match.
425 * Note that origin provided in request header has not been normalized, so
426 * change in case or other non-normal forms might not match allowed list */
427 const array * const allowed_origins = hctx->conf.origins;
428 buffer *origin = NULL;
429 size_t olen;
431 if (0 == allowed_origins->used) {
432 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "s", "allowed origins not specified");
433 return 1;
436 /* "Origin" header is preferred
437 * ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */
438 origin = http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Origin"));
439 if (NULL == origin) {
440 origin =
441 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Origin"));
443 olen = buffer_string_length(origin);
444 if (0 == olen) {
445 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Origin header is invalid");
446 con->http_status = 400; /* Bad Request */
447 return 0;
450 for (size_t i = 0; i < allowed_origins->used; ++i) {
451 buffer *b = ((data_string *)allowed_origins->data[i])->value;
452 size_t blen = buffer_string_length(b);
453 if ((olen > blen ? origin->ptr[olen-blen-1] == '.' : olen == blen)
454 && buffer_is_equal_right_len(origin, b, blen)) {
455 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "bsb",
456 origin, "matches allowed origin:", b);
457 return 1;
460 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "bs",
461 origin, "does not match any allowed origins");
462 con->http_status = 403; /* Forbidden */
463 return 0;
466 static int wstunnel_check_request(connection *con, handler_ctx *hctx) {
467 const buffer * const vers =
468 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Version"));
469 const long hybivers = (NULL != vers) ? strtol(vers->ptr, NULL, 10) : 0;
470 if (hybivers < 0 || hybivers > INT_MAX) {
471 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "invalid Sec-WebSocket-Version");
472 con->http_status = 400; /* Bad Request */
473 return -1;
476 /*(redundant since HTTP/1.1 required in mod_wstunnel_check_extension())*/
477 if (buffer_is_empty(con->request.http_host)) {
478 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Host header does not exist");
479 con->http_status = 400; /* Bad Request */
480 return -1;
483 if (!wstunnel_is_allowed_origin(con, hctx)) {
484 return -1;
487 return (int)hybivers;
490 static void wstunnel_backend_error(gw_handler_ctx *gwhctx) {
491 handler_ctx *hctx = (handler_ctx *)gwhctx;
492 if (hctx->gw.state == GW_STATE_WRITE || hctx->gw.state == GW_STATE_READ) {
493 mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, CONST_STR_LEN("1001")); /* 1001 Going Away */
497 static void wstunnel_handler_ctx_free(void *gwhctx) {
498 handler_ctx *hctx = (handler_ctx *)gwhctx;
499 chunk_buffer_release(hctx->frame.payload);
502 static handler_t wstunnel_handler_setup (server *srv, connection *con, plugin_data *p) {
503 handler_ctx *hctx = con->plugin_ctx[p->id];
504 int binary;
505 int hybivers;
506 hctx->srv = srv; /*(for mod_wstunnel module-specific DEBUG_LOG() macro)*/
507 hctx->conf = p->conf; /*(copies struct)*/
508 hybivers = wstunnel_check_request(con, hctx);
509 if (hybivers < 0) return HANDLER_FINISHED;
510 hctx->hybivers = hybivers;
511 if (0 == hybivers) {
512 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO,"s","WebSocket Version = hybi-00");
514 else {
515 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO,"sd","WebSocket Version =",hybivers);
518 hctx->gw.opts.backend = BACKEND_PROXY; /*(act proxy-like; not used)*/
519 hctx->gw.opts.pdata = hctx;
520 hctx->gw.opts.parse = wstunnel_recv_parse;
521 hctx->gw.stdin_append = wstunnel_stdin_append;
522 hctx->gw.create_env = wstunnel_create_env;
523 hctx->gw.handler_ctx_free = wstunnel_handler_ctx_free;
524 hctx->gw.backend_error = wstunnel_backend_error;
525 hctx->gw.response = chunk_buffer_acquire();
527 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
528 hctx->frame.ctl.siz = 0;
529 hctx->frame.payload = chunk_buffer_acquire();
531 binary = !buffer_is_empty(hctx->conf.frame_type); /*("binary")*/
532 if (!binary) {
533 buffer *vb =
534 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Protocol"));
535 if (NULL != vb) {
536 for (const char *s = vb->ptr; *s; ++s) {
537 while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s;
538 if (0 == strncasecmp(s, "binary", sizeof("binary")-1)) {
539 s += sizeof("binary")-1;
540 while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s;
541 if (*s==','||*s=='\0') {
542 hctx->subproto = 1;
543 binary = 1;
544 break;
547 else if (0 == strncasecmp(s, "base64", sizeof("base64")-1)) {
548 s += sizeof("base64")-1;
549 while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s;
550 if (*s==','||*s=='\0') {
551 hctx->subproto = -1;
552 break;
555 s = strchr(s, ',');
556 if (NULL == s) break;
561 if (binary) {
562 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "s",
563 "will recv binary data from backend");
564 hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_BIN;
565 hctx->frame.type_before = MOD_WEBSOCKET_FRAME_TYPE_BIN;
566 hctx->frame.type_backend = MOD_WEBSOCKET_FRAME_TYPE_BIN;
568 else {
569 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "s",
570 "will recv text data from backend");
571 hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
572 hctx->frame.type_before = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
573 hctx->frame.type_backend = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
576 return HANDLER_GO_ON;
579 static handler_t mod_wstunnel_check_extension(server *srv, connection *con, void *p_d) {
580 plugin_data *p = p_d;
581 buffer *vb;
582 handler_t rc;
584 if (con->mode != DIRECT)
585 return HANDLER_GO_ON;
586 if (con->request.http_method != HTTP_METHOD_GET)
587 return HANDLER_GO_ON;
588 if (con->request.http_version != HTTP_VERSION_1_1)
589 return HANDLER_GO_ON;
592 * Connection: upgrade, keep-alive, ...
593 * Upgrade: WebSocket, ...
595 vb = http_header_request_get(con, HTTP_HEADER_UPGRADE, CONST_STR_LEN("Upgrade"));
596 if (NULL == vb
597 || !header_contains_token(vb, CONST_STR_LEN("websocket")))
598 return HANDLER_GO_ON;
599 vb = http_header_request_get(con, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"));
600 if (NULL == vb
601 || !header_contains_token(vb, CONST_STR_LEN("upgrade")))
602 return HANDLER_GO_ON;
604 mod_wstunnel_patch_connection(srv, con, p);
605 if (NULL == p->conf.gw.exts) return HANDLER_GO_ON;
607 rc = gw_check_extension(srv,con,(gw_plugin_data *)p,1,sizeof(handler_ctx));
608 return (HANDLER_GO_ON == rc && con->mode == p->id)
609 ? wstunnel_handler_setup(srv, con, p)
610 : rc;
613 TRIGGER_FUNC(mod_wstunnel_handle_trigger) {
614 const plugin_data * const p = p_d;
615 const time_t cur_ts = srv->cur_ts + 1;
617 gw_handle_trigger(srv, p_d);
619 for (size_t i = 0; i < srv->conns->used; ++i) {
620 connection *con = srv->conns->ptr[i];
621 handler_ctx *hctx = con->plugin_ctx[p->id];
622 if (NULL == hctx || con->mode != p->id)
623 continue;
625 if (hctx->gw.state != GW_STATE_WRITE && hctx->gw.state != GW_STATE_READ)
626 continue;
628 if (cur_ts - con->read_idle_ts > con->conf.max_read_idle) {
629 DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "sds",
630 "timeout client ( fd =", con->fd, ")");
631 mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, NULL, 0);
632 gw_connection_reset(srv, con, p_d);
633 joblist_append(srv, con);
634 /* avoid server.c closing connection with error due to max_read_idle
635 * (might instead run joblist after plugins_call_handle_trigger())*/
636 con->read_idle_ts = cur_ts;
637 continue;
640 if (0 != hctx->hybivers
641 && hctx->conf.ping_interval > 0
642 && (time_t)hctx->conf.ping_interval + hctx->ping_ts < cur_ts) {
643 hctx->ping_ts = cur_ts;
644 mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_PING, CONST_STR_LEN("ping"));
645 joblist_append(srv, con);
646 continue;
650 return HANDLER_GO_ON;
653 int mod_wstunnel_plugin_init(plugin *p);
654 int mod_wstunnel_plugin_init(plugin *p) {
655 p->version = LIGHTTPD_VERSION_ID;
656 p->name = buffer_init_string("wstunnel");
657 p->init = mod_wstunnel_init;
658 p->cleanup = mod_wstunnel_free;
659 p->set_defaults = mod_wstunnel_set_defaults;
660 p->connection_reset = gw_connection_reset;
661 p->handle_uri_clean = mod_wstunnel_check_extension;
662 p->handle_subrequest = gw_handle_subrequest;
663 p->handle_trigger = mod_wstunnel_handle_trigger;
664 p->handle_waitpid = gw_handle_waitpid_cb;
665 p->data = NULL;
666 return 0;
673 * modified from Norio Kobota mod_websocket_handshake.c
676 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
678 #include "sys-endian.h" /* lighttpd */
679 #include "md5.h" /* lighttpd */
681 static int get_key3(connection *con, char *buf) {
682 /* 8 bytes should have been sent with request
683 * for draft-ietf-hybi-thewebsocketprotocol-00 */
684 chunkqueue *cq = con->request_content_queue;
685 size_t bytes = 8;
686 /*(caller should ensure bytes available prior to calling this routine)*/
687 /*assert(chunkqueue_length(cq) >= 8);*/
688 for (chunk *c = cq->first; NULL != c; c = c->next) {
689 /*(chunk_remaining_length() on MEM_CHUNK)*/
690 size_t n = (size_t)(buffer_string_length(c->mem) - c->offset);
691 /*(expecting 8 bytes to be in memory directly after headers)*/
692 if (c->type != MEM_CHUNK) break; /* FILE_CHUNK not handled here */
693 if (n > bytes) n = bytes;
694 memcpy(buf, c->mem->ptr+c->offset, n);
695 buf += n;
696 if (0 == (bytes -= n)) break;
698 if (0 != bytes) return -1;
699 chunkqueue_mark_written(cq, 8);
700 return 0;
703 static int get_key_number(uint32_t *ret, const buffer *b) {
704 const char * const s = b->ptr;
705 size_t j = 0;
706 unsigned long n;
707 uint32_t sp = 0;
708 char tmp[10 + 1]; /* #define UINT32_MAX_STRLEN 10 */
710 for (size_t i = 0, used = buffer_string_length(b); i < used; ++i) {
711 if (light_isdigit(s[i])) {
712 tmp[j] = s[i];
713 if (++j >= sizeof(tmp)) return -1;
715 else if (s[i] == ' ') ++sp; /* count num spaces */
717 tmp[j] = '\0';
718 n = strtoul(tmp, NULL, 10);
719 if (n > UINT32_MAX || 0 == sp) return -1;
720 *ret = (uint32_t)n / sp;
721 return 0;
724 static int create_MD5_sum(connection *con) {
725 uint32_t buf[4]; /* MD5 binary hash len */
726 li_MD5_CTX ctx;
728 const buffer *key1 =
729 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key1"));
730 const buffer *key2 =
731 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key2"));
733 if (NULL == key1 || get_key_number(buf+0, key1) < 0 ||
734 NULL == key2 || get_key_number(buf+1, key2) < 0 ||
735 get_key3(con, (char *)(buf+2)) < 0) {
736 return -1;
738 #ifdef __BIG_ENDIAN__
739 #define ws_htole32(s,u)\
740 (s)[0]=((u)>>24); \
741 (s)[1]=((u)>>16); \
742 (s)[2]=((u)>>8); \
743 (s)[3]=((u))
744 ws_htole32((unsigned char *)(buf+0), buf[0]);
745 ws_htole32((unsigned char *)(buf+1), buf[1]);
746 #endif
747 li_MD5_Init(&ctx);
748 li_MD5_Update(&ctx, buf, sizeof(buf));
749 li_MD5_Final((unsigned char *)buf, &ctx); /*(overwrite buf[] with result)*/
750 chunkqueue_append_mem(con->write_queue, (char *)buf, sizeof(buf));
751 return 0;
754 static int create_response_ietf_00(handler_ctx *hctx) {
755 connection *con = hctx->gw.remote_conn;
756 buffer *value = hctx->srv->tmp_buf;
758 /* "Origin" header is preferred
759 * ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */
760 buffer *origin = http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Origin"));
761 if (NULL == origin) {
762 origin =
763 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Origin"));
765 if (NULL == origin) {
766 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Origin header is invalid");
767 return -1;
769 if (buffer_is_empty(con->request.http_host)) {
770 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Host header does not exist");
771 return -1;
774 /* calc MD5 sum from keys */
775 if (create_MD5_sum(con) < 0) {
776 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Sec-WebSocket-Key is invalid");
777 return -1;
780 http_header_response_set(con, HTTP_HEADER_UPGRADE,
781 CONST_STR_LEN("Upgrade"),
782 CONST_STR_LEN("websocket"));
783 #if 0 /*(added later in http_response_write_header())*/
784 http_header_response_append(con, HTTP_HEADER_CONNECTION,
785 CONST_STR_LEN("Connection"),
786 CONST_STR_LEN("upgrade"));
787 #endif
788 #if 0 /*(Sec-WebSocket-Origin header is not required for hybi-00)*/
789 /* Note: it is insecure to simply reflect back origin provided by client
790 * (if admin did not configure restricted list of valid origins)
791 * (see wstunnel_check_request()) */
792 http_header_response_set(con, HTTP_HEADER_OTHER,
793 CONST_STR_LEN("Sec-WebSocket-Origin"),
794 CONST_BUF_LEN(origin));
795 #endif
797 if (buffer_is_equal_string(con->uri.scheme, CONST_STR_LEN("https")))
798 buffer_copy_string_len(value, CONST_STR_LEN("wss://"));
799 else
800 buffer_copy_string_len(value, CONST_STR_LEN("ws://"));
801 buffer_append_string_buffer(value, con->request.http_host);
802 buffer_append_string_buffer(value, con->uri.path);
803 http_header_response_set(con, HTTP_HEADER_OTHER,
804 CONST_STR_LEN("Sec-WebSocket-Location"),
805 CONST_BUF_LEN(value));
807 return 0;
810 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
813 #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
815 #include "algo_sha1.h" /* lighttpd */
816 #include "base64.h" /* lighttpd */
818 static int create_response_rfc_6455(handler_ctx *hctx) {
819 connection *con = hctx->gw.remote_conn;
820 SHA_CTX sha;
821 unsigned char sha_digest[SHA_DIGEST_LENGTH];
823 buffer *value =
824 http_header_request_get(con, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key"));
825 if (NULL == value) {
826 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Sec-WebSocket-Key is invalid");
827 return -1;
830 /* get SHA1 hash of key */
831 /* refer: RFC-6455 Sec.1.3 Opening Handshake */
832 SHA1_Init(&sha);
833 SHA1_Update(&sha, (const unsigned char *)CONST_BUF_LEN(value));
834 SHA1_Update(&sha, (const unsigned char *)CONST_STR_LEN("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
835 SHA1_Final(sha_digest, &sha);
837 http_header_response_set(con, HTTP_HEADER_UPGRADE,
838 CONST_STR_LEN("Upgrade"),
839 CONST_STR_LEN("websocket"));
840 #if 0 /*(added later in http_response_write_header())*/
841 http_header_response_append(con, HTTP_HEADER_CONNECTION,
842 CONST_STR_LEN("Connection"),
843 CONST_STR_LEN("upgrade"));
844 #endif
846 value = hctx->srv->tmp_buf;
847 buffer_clear(value);
848 buffer_append_base64_encode(value, sha_digest, SHA_DIGEST_LENGTH, BASE64_STANDARD);
849 http_header_response_set(con, HTTP_HEADER_OTHER,
850 CONST_STR_LEN("Sec-WebSocket-Accept"),
851 CONST_BUF_LEN(value));
853 if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_BIN)
854 http_header_response_set(con, HTTP_HEADER_OTHER,
855 CONST_STR_LEN("Sec-WebSocket-Protocol"),
856 CONST_STR_LEN("binary"));
857 else if (-1 == hctx->subproto)
858 http_header_response_set(con, HTTP_HEADER_OTHER,
859 CONST_STR_LEN("Sec-WebSocket-Protocol"),
860 CONST_STR_LEN("base64"));
862 return 0;
865 #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
868 handler_t mod_wstunnel_handshake_create_response(handler_ctx *hctx) {
869 connection *con = hctx->gw.remote_conn;
870 #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
871 if (hctx->hybivers >= 8) {
872 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "send handshake response");
873 if (0 != create_response_rfc_6455(hctx)) {
874 con->http_status = 400; /* Bad Request */
875 return HANDLER_ERROR;
877 return HANDLER_GO_ON;
879 #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
881 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
882 if (hctx->hybivers == 0) {
883 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
884 /* 8 bytes should have been sent with request
885 * for draft-ietf-hybi-thewebsocketprotocol-00 */
886 chunkqueue *cq = con->request_content_queue;
887 if (chunkqueue_length(cq) < 8)
888 return HANDLER_WAIT_FOR_EVENT;
889 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
891 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "send handshake response");
892 if (0 != create_response_ietf_00(hctx)) {
893 con->http_status = 400; /* Bad Request */
894 return HANDLER_ERROR;
896 return HANDLER_GO_ON;
898 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
900 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "not supported WebSocket Version");
901 con->http_status = 503; /* Service Unavailable */
902 return HANDLER_ERROR;
909 * modified from Norio Kobota mod_websocket_frame.c
912 #include "base64.h" /* lighttpd */
913 #include "http_chunk.h" /* lighttpd */
915 #define MOD_WEBSOCKET_BUFMAX (0x0fffff)
917 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
919 #include <stdlib.h>
920 static int send_ietf_00(handler_ctx *hctx, mod_wstunnel_frame_type_t type, const char *payload, size_t siz) {
921 static const char head = 0; /* 0x00 */
922 static const char tail = ~0; /* 0xff */
923 server *srv = hctx->srv;
924 connection *con = hctx->gw.remote_conn;
925 char *mem;
926 size_t len;
928 switch (type) {
929 case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
930 if (0 == siz) return 0;
931 http_chunk_append_mem(srv, con, &head, 1);
932 http_chunk_append_mem(srv, con, payload, siz);
933 http_chunk_append_mem(srv, con, &tail, 1);
934 len = siz+2;
935 break;
936 case MOD_WEBSOCKET_FRAME_TYPE_BIN:
937 if (0 == siz) return 0;
938 http_chunk_append_mem(srv, con, &head, 1);
939 len = 4*(siz/3)+4+1;
940 /* avoid accumulating too much data in memory; send to tmpfile */
941 mem = malloc(len);
942 force_assert(mem);
943 len=li_to_base64(mem,len,(unsigned char *)payload,siz,BASE64_STANDARD);
944 http_chunk_append_mem(srv, con, mem, len);
945 free(mem);
946 http_chunk_append_mem(srv, con, &tail, 1);
947 len += 2;
948 break;
949 case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
950 http_chunk_append_mem(srv, con, &tail, 1);
951 http_chunk_append_mem(srv, con, &head, 1);
952 len = 2;
953 break;
954 default:
955 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "invalid frame type");
956 return -1;
958 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx",
959 "send data to client ( fd =", con->fd, "), frame size =", len);
960 return 0;
963 static int recv_ietf_00(handler_ctx *hctx) {
964 connection *con = hctx->gw.remote_conn;
965 chunkqueue *cq = con->request_content_queue;
966 buffer *payload = hctx->frame.payload;
967 char *mem;
968 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx",
969 "recv data from client ( fd =", con->fd,
970 "), size =", chunkqueue_length(cq));
971 for (chunk *c = cq->first; c; c = c->next) {
972 char *frame = c->mem->ptr+c->offset;
973 /*(chunk_remaining_length() on MEM_CHUNK)*/
974 size_t flen = (size_t)(buffer_string_length(c->mem) - c->offset);
975 /*(FILE_CHUNK not handled, but might need to add support)*/
976 force_assert(c->type == MEM_CHUNK);
977 for (size_t i = 0; i < flen; ) {
978 switch (hctx->frame.state) {
979 case MOD_WEBSOCKET_FRAME_STATE_INIT:
980 hctx->frame.ctl.siz = 0;
981 if (frame[i] == 0x00) {
982 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD;
983 i++;
985 else if (((unsigned char *)frame)[i] == 0xff) {
986 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG,"s","recv close frame");
987 return -1;
989 else {
990 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG,"s","recv invalid frame");
991 return -1;
993 break;
994 case MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD:
995 mem = (char *)memchr(frame+i, 0xff, flen - i);
996 if (mem == NULL) {
997 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
998 "got continuous payload, size =", flen - i);
999 hctx->frame.ctl.siz += flen - i;
1000 if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
1001 DEBUG_LOG(MOD_WEBSOCKET_LOG_WARN, "sx",
1002 "frame size has been exceeded:",
1003 MOD_WEBSOCKET_BUFMAX);
1004 return -1;
1006 buffer_append_string_len(payload, frame+i, flen - i);
1007 i += flen - i;
1009 else {
1010 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1011 "got final payload, size =", (mem - frame+i));
1012 hctx->frame.ctl.siz += (mem - frame+i);
1013 if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
1014 DEBUG_LOG(MOD_WEBSOCKET_LOG_WARN, "sx",
1015 "frame size has been exceeded:",
1016 MOD_WEBSOCKET_BUFMAX);
1017 return -1;
1019 buffer_append_string_len(payload, frame+i, mem - frame+i);
1020 i += (mem - frame+i);
1021 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
1023 i++;
1024 if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_TEXT
1025 && !buffer_is_empty(payload)) {
1026 hctx->frame.ctl.siz = 0;
1027 chunkqueue_append_buffer(hctx->gw.wb, payload);
1028 buffer_clear(payload);
1030 else {
1031 if (hctx->frame.state == MOD_WEBSOCKET_FRAME_STATE_INIT
1032 && !buffer_is_empty(payload)) {
1033 buffer *b;
1034 size_t len = buffer_string_length(payload);
1035 len = (len+3)/4*3+1;
1036 chunkqueue_get_memory(hctx->gw.wb, &len);
1037 b = hctx->gw.wb->last->mem;
1038 len = buffer_string_length(b);
1039 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "ss",
1040 "try to base64 decode:", payload->ptr);
1041 if (NULL == buffer_append_base64_decode(b, CONST_BUF_LEN(payload), BASE64_STANDARD)) {
1042 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s",
1043 "fail to base64-decode");
1044 return -1;
1046 buffer_clear(payload);
1047 /*chunkqueue_use_memory()*/
1048 hctx->gw.wb->bytes_in += buffer_string_length(b)-len;
1051 break;
1052 default: /* never reach */
1053 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR,"s", "BUG: unknown state");
1054 return -1;
1058 /* XXX: should add ability to handle and preserve partial frames above */
1059 /*(not chunkqueue_reset(); do not reset cq->bytes_in, cq->bytes_out)*/
1060 chunkqueue_mark_written(cq, chunkqueue_length(cq));
1061 return 0;
1064 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
1067 #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
1069 #define MOD_WEBSOCKET_OPCODE_CONT 0x00
1070 #define MOD_WEBSOCKET_OPCODE_TEXT 0x01
1071 #define MOD_WEBSOCKET_OPCODE_BIN 0x02
1072 #define MOD_WEBSOCKET_OPCODE_CLOSE 0x08
1073 #define MOD_WEBSOCKET_OPCODE_PING 0x09
1074 #define MOD_WEBSOCKET_OPCODE_PONG 0x0A
1076 #define MOD_WEBSOCKET_FRAME_LEN16 0x7E
1077 #define MOD_WEBSOCKET_FRAME_LEN63 0x7F
1078 #define MOD_WEBSOCKET_FRAME_LEN16_CNT 2
1079 #define MOD_WEBSOCKET_FRAME_LEN63_CNT 8
1081 static int send_rfc_6455(handler_ctx *hctx, mod_wstunnel_frame_type_t type, const char *payload, size_t siz) {
1082 server *srv = hctx->srv;
1083 connection *con = hctx->gw.remote_conn;
1084 char mem[10];
1085 size_t len;
1087 /* allowed null payload for ping, pong, close frame */
1088 if (payload == NULL && ( type == MOD_WEBSOCKET_FRAME_TYPE_TEXT
1089 || type == MOD_WEBSOCKET_FRAME_TYPE_BIN )) {
1090 return -1;
1093 switch (type) {
1094 case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
1095 mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_TEXT);
1096 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = text");
1097 break;
1098 case MOD_WEBSOCKET_FRAME_TYPE_BIN:
1099 mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_BIN);
1100 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = binary");
1101 break;
1102 case MOD_WEBSOCKET_FRAME_TYPE_PING:
1103 mem[0] = (char) (0x80 | MOD_WEBSOCKET_OPCODE_PING);
1104 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = ping");
1105 break;
1106 case MOD_WEBSOCKET_FRAME_TYPE_PONG:
1107 mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_PONG);
1108 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = pong");
1109 break;
1110 case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
1111 default:
1112 mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_CLOSE);
1113 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = close");
1114 break;
1117 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", "payload size =", siz);
1118 if (siz < MOD_WEBSOCKET_FRAME_LEN16) {
1119 mem[1] = siz;
1120 len = 2;
1122 else if (siz <= UINT16_MAX) {
1123 mem[1] = MOD_WEBSOCKET_FRAME_LEN16;
1124 mem[2] = (siz >> 8) & 0xff;
1125 mem[3] = siz & 0xff;
1126 len = 1+MOD_WEBSOCKET_FRAME_LEN16_CNT+1;
1128 else {
1129 mem[1] = MOD_WEBSOCKET_FRAME_LEN63;
1130 mem[2] = 0;
1131 mem[3] = 0;
1132 mem[4] = 0;
1133 mem[5] = 0;
1134 mem[6] = (siz >> 24) & 0xff;
1135 mem[7] = (siz >> 16) & 0xff;
1136 mem[8] = (siz >> 8) & 0xff;
1137 mem[9] = siz & 0xff;
1138 len = 1+MOD_WEBSOCKET_FRAME_LEN63_CNT+1;
1140 http_chunk_append_mem(srv, con, mem, len);
1141 if (siz) http_chunk_append_mem(srv, con, payload, siz);
1142 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx",
1143 "send data to client ( fd =",con->fd,"), frame size =",len+siz);
1144 return 0;
1147 static void unmask_payload(handler_ctx *hctx) {
1148 buffer * const b = hctx->frame.payload;
1149 for (size_t i = 0, used = buffer_string_length(b); i < used; ++i) {
1150 b->ptr[i] ^= hctx->frame.ctl.mask[hctx->frame.ctl.mask_cnt];
1151 hctx->frame.ctl.mask_cnt = (hctx->frame.ctl.mask_cnt + 1) % 4;
1155 static int recv_rfc_6455(handler_ctx *hctx) {
1156 connection *con = hctx->gw.remote_conn;
1157 chunkqueue *cq = con->request_content_queue;
1158 buffer *payload = hctx->frame.payload;
1159 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx",
1160 "recv data from client ( fd =", con->fd,
1161 "), size =", chunkqueue_length(cq));
1162 for (chunk *c = cq->first; c; c = c->next) {
1163 char *frame = c->mem->ptr+c->offset;
1164 /*(chunk_remaining_length() on MEM_CHUNK)*/
1165 size_t flen = (size_t)(buffer_string_length(c->mem) - c->offset);
1166 /*(FILE_CHUNK not handled, but might need to add support)*/
1167 force_assert(c->type == MEM_CHUNK);
1168 for (size_t i = 0; i < flen; ) {
1169 switch (hctx->frame.state) {
1170 case MOD_WEBSOCKET_FRAME_STATE_INIT:
1171 switch (frame[i] & 0x0f) {
1172 case MOD_WEBSOCKET_OPCODE_CONT:
1173 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = continue");
1174 hctx->frame.type = hctx->frame.type_before;
1175 break;
1176 case MOD_WEBSOCKET_OPCODE_TEXT:
1177 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = text");
1178 hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
1179 hctx->frame.type_before = hctx->frame.type;
1180 break;
1181 case MOD_WEBSOCKET_OPCODE_BIN:
1182 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = binary");
1183 hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_BIN;
1184 hctx->frame.type_before = hctx->frame.type;
1185 break;
1186 case MOD_WEBSOCKET_OPCODE_PING:
1187 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = ping");
1188 hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PING;
1189 break;
1190 case MOD_WEBSOCKET_OPCODE_PONG:
1191 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = pong");
1192 hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PONG;
1193 break;
1194 case MOD_WEBSOCKET_OPCODE_CLOSE:
1195 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = close");
1196 hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_CLOSE;
1197 return -1;
1198 break;
1199 default:
1200 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "type is invalid");
1201 return -1;
1202 break;
1204 i++;
1205 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH;
1206 break;
1207 case MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH:
1208 if ((frame[i] & 0x80) != 0x80) {
1209 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s",
1210 "payload was not masked");
1211 return -1;
1213 hctx->frame.ctl.mask_cnt = 0;
1214 hctx->frame.ctl.siz = (uint64_t)(frame[i] & 0x7f);
1215 if (hctx->frame.ctl.siz == 0) {
1216 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1217 "specified payload size =", hctx->frame.ctl.siz);
1218 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK;
1220 else if (hctx->frame.ctl.siz == MOD_WEBSOCKET_FRAME_LEN16) {
1221 hctx->frame.ctl.siz = 0;
1222 hctx->frame.ctl.siz_cnt = MOD_WEBSOCKET_FRAME_LEN16_CNT;
1223 hctx->frame.state =
1224 MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH;
1226 else if (hctx->frame.ctl.siz == MOD_WEBSOCKET_FRAME_LEN63) {
1227 hctx->frame.ctl.siz = 0;
1228 hctx->frame.ctl.siz_cnt = MOD_WEBSOCKET_FRAME_LEN63_CNT;
1229 hctx->frame.state =
1230 MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH;
1232 else {
1233 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1234 "specified payload size =", hctx->frame.ctl.siz);
1235 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK;
1237 i++;
1238 break;
1239 case MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH:
1240 hctx->frame.ctl.siz =
1241 (hctx->frame.ctl.siz << 8) + (frame[i] & 0xff);
1242 hctx->frame.ctl.siz_cnt--;
1243 if (hctx->frame.ctl.siz_cnt <= 0) {
1244 if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_PING &&
1245 hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
1246 DEBUG_LOG(MOD_WEBSOCKET_LOG_WARN, "sx",
1247 "frame size has been exceeded:",
1248 MOD_WEBSOCKET_BUFMAX);
1249 return -1;
1251 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1252 "specified payload size =", hctx->frame.ctl.siz);
1253 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK;
1255 i++;
1256 break;
1257 case MOD_WEBSOCKET_FRAME_STATE_READ_MASK:
1258 hctx->frame.ctl.mask[hctx->frame.ctl.mask_cnt] = frame[i];
1259 hctx->frame.ctl.mask_cnt++;
1260 if (hctx->frame.ctl.mask_cnt >= MOD_WEBSOCKET_MASK_CNT) {
1261 hctx->frame.ctl.mask_cnt = 0;
1262 if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_PING &&
1263 hctx->frame.ctl.siz == 0) {
1264 mod_wstunnel_frame_send(hctx,
1265 MOD_WEBSOCKET_FRAME_TYPE_PONG,
1266 NULL, 0);
1268 if (hctx->frame.ctl.siz == 0) {
1269 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
1271 else {
1272 hctx->frame.state =
1273 MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD;
1276 i++;
1277 break;
1278 case MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD:
1279 /* hctx->frame.ctl.siz <= SIZE_MAX */
1280 if (hctx->frame.ctl.siz <= flen - i) {
1281 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1282 "read payload, size =", hctx->frame.ctl.siz);
1283 buffer_append_string_len(payload, frame+i, (size_t)
1284 (hctx->frame.ctl.siz & SIZE_MAX));
1285 i += (size_t)(hctx->frame.ctl.siz & SIZE_MAX);
1286 hctx->frame.ctl.siz = 0;
1287 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
1288 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1289 "rest of frame size =", flen - i);
1290 /* SIZE_MAX < hctx->frame.ctl.siz */
1292 else {
1293 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1294 "read payload, size =", flen - i);
1295 buffer_append_string_len(payload, frame+i, flen - i);
1296 hctx->frame.ctl.siz -= flen - i;
1297 i += flen - i;
1298 DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx",
1299 "rest of payload size =", hctx->frame.ctl.siz);
1301 switch (hctx->frame.type) {
1302 case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
1303 case MOD_WEBSOCKET_FRAME_TYPE_BIN:
1305 unmask_payload(hctx);
1306 chunkqueue_append_buffer(hctx->gw.wb, payload);
1307 buffer_clear(payload);
1308 break;
1310 case MOD_WEBSOCKET_FRAME_TYPE_PING:
1311 if (hctx->frame.ctl.siz == 0) {
1312 unmask_payload(hctx);
1313 mod_wstunnel_frame_send(hctx,
1314 MOD_WEBSOCKET_FRAME_TYPE_PONG,
1315 payload->ptr, buffer_string_length(payload));
1316 buffer_clear(payload);
1318 break;
1319 case MOD_WEBSOCKET_FRAME_TYPE_PONG:
1320 buffer_clear(payload);
1321 break;
1322 case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
1323 default:
1324 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s",
1325 "BUG: invalid frame type");
1326 return -1;
1328 break;
1329 default:
1330 DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "BUG: invalid state");
1331 return -1;
1335 /* XXX: should add ability to handle and preserve partial frames above */
1336 /*(not chunkqueue_reset(); do not reset cq->bytes_in, cq->bytes_out)*/
1337 chunkqueue_mark_written(cq, chunkqueue_length(cq));
1338 return 0;
1341 #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
1344 int mod_wstunnel_frame_send(handler_ctx *hctx, mod_wstunnel_frame_type_t type,
1345 const char *payload, size_t siz) {
1346 #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
1347 if (hctx->hybivers >= 8) return send_rfc_6455(hctx, type, payload, siz);
1348 #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
1349 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
1350 if (0 == hctx->hybivers) return send_ietf_00(hctx, type, payload, siz);
1351 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
1352 return -1;
1355 int mod_wstunnel_frame_recv(handler_ctx *hctx) {
1356 #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
1357 if (hctx->hybivers >= 8) return recv_rfc_6455(hctx);
1358 #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
1359 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
1360 if (0 == hctx->hybivers) return recv_ietf_00(hctx);
1361 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
1362 return -1;