xref: /lighttpd1.4/src/mod_wstunnel.c (revision f2e4e83a)
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  */
6 
7 /*
8  * Copyright(c) 2010, Norio Kobota, All rights reserved.
9  *
10  * Redistribution and use in source and binary forms, with or without
11  * modification, are permitted provided that the following conditions are met:
12  *
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.
21  *
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.
33  */
34 
35 /* NOTES:
36  *
37  * mod_wstunnel has been largely rewritten from Norio Kobota mod_websocket.
38  *
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
66  *
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
71  *
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
76  */
77 #include "first.h"
78 
79 #include <sys/types.h>
80 #include <limits.h>
81 #include <stdlib.h>
82 #include <string.h>
83 
84 #include "gw_backend.h"
85 
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 "log.h"
93 #include "connections.h"
94 
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
100 
101 #define DEBUG_LOG_ERR(format, ...) \
102   if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_ERR) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
103 
104 #define DEBUG_LOG_WARN(format, ...) \
105   if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_WARN) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
106 
107 #define DEBUG_LOG_INFO(format, ...) \
108   if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_INFO) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
109 
110 #define DEBUG_LOG_DEBUG(format, ...) \
111   if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_DEBUG) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
112 
113 typedef struct {
114     gw_plugin_config gw; /* start must match layout of gw_plugin_config */
115     const array *origins;
116     unsigned int frame_type;
117     unsigned short int ping_interval;
118 } plugin_config;
119 
120 typedef struct plugin_data {
121     PLUGIN_DATA;
122     pid_t srv_pid; /* must match layout of gw_plugin_data through conf member */
123     plugin_config conf;
124     plugin_config defaults;
125 } plugin_data;
126 
127 typedef enum {
128     MOD_WEBSOCKET_FRAME_STATE_INIT,
129 
130     /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
131     MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH,
132     MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH,
133     MOD_WEBSOCKET_FRAME_STATE_READ_MASK,
134     /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
135 
136     MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD
137 } mod_wstunnel_frame_state_t;
138 
139 typedef enum {
140     MOD_WEBSOCKET_FRAME_TYPE_TEXT,
141     MOD_WEBSOCKET_FRAME_TYPE_BIN,
142     MOD_WEBSOCKET_FRAME_TYPE_CLOSE,
143 
144     /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
145     MOD_WEBSOCKET_FRAME_TYPE_PING,
146     MOD_WEBSOCKET_FRAME_TYPE_PONG
147     /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
148 
149 } mod_wstunnel_frame_type_t;
150 
151 typedef struct {
152     uint64_t siz;
153 
154     /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
155     int siz_cnt;
156     int mask_cnt;
157     #define MOD_WEBSOCKET_MASK_CNT 4
158     unsigned char mask[MOD_WEBSOCKET_MASK_CNT];
159     /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
160 
161 } mod_wstunnel_frame_control_t;
162 
163 typedef struct {
164     mod_wstunnel_frame_state_t state;
165     mod_wstunnel_frame_control_t ctl;
166     mod_wstunnel_frame_type_t type, type_before, type_backend;
167     buffer *payload;
168 } mod_wstunnel_frame_t;
169 
170 typedef struct {
171     gw_handler_ctx gw;
172     mod_wstunnel_frame_t frame;
173 
174     int hybivers;
175     time_t ping_ts;
176     int subproto;
177 
178     log_error_st *errh; /*(for mod_wstunnel module-specific DEBUG_*() macros)*/
179     plugin_config conf;
180 } handler_ctx;
181 
182 /* prototypes */
183 static handler_t mod_wstunnel_handshake_create_response(handler_ctx *);
184 static int mod_wstunnel_frame_send(handler_ctx *, mod_wstunnel_frame_type_t, const char *, size_t);
185 static int mod_wstunnel_frame_recv(handler_ctx *);
186 #define _MOD_WEBSOCKET_SPEC_IETF_00_
187 #define _MOD_WEBSOCKET_SPEC_RFC_6455_
188 
189 INIT_FUNC(mod_wstunnel_init) {
190     return calloc(1, sizeof(plugin_data));
191 }
192 
193 static void mod_wstunnel_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
194     switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
195       case 0: /* wstunnel.server */
196         if (cpv->vtype == T_CONFIG_LOCAL) {
197             gw_plugin_config * const gw = cpv->v.v;
198             pconf->gw.exts      = gw->exts;
199             pconf->gw.exts_auth = gw->exts_auth;
200             pconf->gw.exts_resp = gw->exts_resp;
201         }
202         break;
203       case 1: /* wstunnel.balance */
204         /*if (cpv->vtype == T_CONFIG_LOCAL)*//*always true here for this param*/
205             pconf->gw.balance = (int)cpv->v.u;
206         break;
207       case 2: /* wstunnel.debug */
208         pconf->gw.debug = (int)cpv->v.u;
209         break;
210       case 3: /* wstunnel.map-extensions */
211         pconf->gw.ext_mapping = cpv->v.a;
212         break;
213       case 4: /* wstunnel.frame-type */
214         pconf->frame_type = cpv->v.u;
215         break;
216       case 5: /* wstunnel.origins */
217         pconf->origins = cpv->v.a;
218         break;
219       case 6: /* wstunnel.ping-interval */
220         pconf->ping_interval = cpv->v.shrt;
221         break;
222       default:/* should not happen */
223         return;
224     }
225 }
226 
227 static void mod_wstunnel_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
228     do {
229         mod_wstunnel_merge_config_cpv(pconf, cpv);
230     } while ((++cpv)->k_id != -1);
231 }
232 
233 static void mod_wstunnel_patch_config(request_st * const r, plugin_data * const p) {
234     memcpy(&p->conf, &p->defaults, sizeof(plugin_config));
235     for (int i = 1, used = p->nconfig; i < used; ++i) {
236         if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
237             mod_wstunnel_merge_config(&p->conf, p->cvlist+p->cvlist[i].v.u2[0]);
238     }
239 }
240 
241 SETDEFAULTS_FUNC(mod_wstunnel_set_defaults) {
242     static const config_plugin_keys_t cpk[] = {
243       { CONST_STR_LEN("wstunnel.server"),
244         T_CONFIG_ARRAY_KVARRAY,
245         T_CONFIG_SCOPE_CONNECTION }
246      ,{ CONST_STR_LEN("wstunnel.balance"),
247         T_CONFIG_STRING,
248         T_CONFIG_SCOPE_CONNECTION }
249      ,{ CONST_STR_LEN("wstunnel.debug"),
250         T_CONFIG_INT,
251         T_CONFIG_SCOPE_CONNECTION }
252      ,{ CONST_STR_LEN("wstunnel.map-extensions"),
253         T_CONFIG_ARRAY_KVSTRING,
254         T_CONFIG_SCOPE_CONNECTION }
255      ,{ CONST_STR_LEN("wstunnel.frame-type"),
256         T_CONFIG_STRING,
257         T_CONFIG_SCOPE_CONNECTION }
258      ,{ CONST_STR_LEN("wstunnel.origins"),
259         T_CONFIG_ARRAY_VLIST,
260         T_CONFIG_SCOPE_CONNECTION }
261      ,{ CONST_STR_LEN("wstunnel.ping-interval"),
262         T_CONFIG_SHORT,
263         T_CONFIG_SCOPE_CONNECTION }
264      ,{ NULL, 0,
265         T_CONFIG_UNSET,
266         T_CONFIG_SCOPE_UNSET }
267     };
268 
269     plugin_data * const p = p_d;
270     if (!config_plugin_values_init(srv, p, cpk, "mod_wstunnel"))
271         return HANDLER_ERROR;
272 
273     /* process and validate config directives
274      * (init i to 0 if global context; to 1 to skip empty global context) */
275     for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
276         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
277         gw_plugin_config *gw = NULL;
278         for (; -1 != cpv->k_id; ++cpv) {
279             switch (cpv->k_id) {
280               case 0: /* wstunnel.server */
281                 gw = calloc(1, sizeof(gw_plugin_config));
282                 force_assert(gw);
283                 if (!gw_set_defaults_backend(srv, (gw_plugin_data *)p, cpv->v.a,
284                                              gw, 0, cpk[cpv->k_id].k)) {
285                     gw_plugin_config_free(gw);
286                     return HANDLER_ERROR;
287                 }
288                 /* error if "mode" = "authorizer";
289                  * wstunnel can not act as authorizer */
290                 /*(check after gw_set_defaults_backend())*/
291                 if (gw->exts_auth && gw->exts_auth->used) {
292                     log_error(srv->errh, __FILE__, __LINE__,
293                       "%s must not define any hosts with "
294                       "attribute \"mode\" = \"authorizer\"", cpk[cpv->k_id].k);
295                     gw_plugin_config_free(gw);
296                     return HANDLER_ERROR;
297                 }
298                 cpv->v.v = gw;
299                 cpv->vtype = T_CONFIG_LOCAL;
300                 break;
301               case 1: /* wstunnel.balance */
302                 cpv->v.u = (unsigned int)gw_get_defaults_balance(srv, cpv->v.b);
303                 break;
304               case 2: /* wstunnel.debug */
305               case 3: /* wstunnel.map-extensions */
306                 break;
307               case 4: /* wstunnel.frame-type */
308                 /*(default frame-type to "text" unless "binary" is specified)*/
309                 cpv->v.u =
310                   buffer_eq_icase_slen(cpv->v.b, CONST_STR_LEN("binary"));
311                 break;
312               case 5: /* wstunnel.origins */
313                 for (uint32_t j = 0; j < cpv->v.a->used; ++j) {
314                     buffer *origin = &((data_string *)cpv->v.a->data[j])->value;
315                     if (buffer_string_is_empty(origin)) {
316                         log_error(srv->errh, __FILE__, __LINE__,
317                           "unexpected empty string in %s", cpk[cpv->k_id].k);
318                         return HANDLER_ERROR;
319                     }
320                 }
321                 break;
322               case 6: /* wstunnel.ping-interval */
323                 break;
324               default:/* should not happen */
325                 break;
326             }
327         }
328 
329         /* disable check-local for all exts (default enabled) */
330         if (gw && gw->exts) { /*(check after gw_set_defaults_backend())*/
331             gw_exts_clear_check_local(gw->exts);
332         }
333     }
334 
335     /* default is 0 */
336     /*p->defaults.balance = (unsigned int)gw_get_defaults_balance(srv, NULL);*/
337     p->defaults.ping_interval = 0; /* do not send ping */
338 
339     /* initialize p->defaults from global config context */
340     if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
341         const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
342         if (-1 != cpv->k_id)
343             mod_wstunnel_merge_config(&p->defaults, cpv);
344     }
345 
346     return HANDLER_GO_ON;
347 }
348 
349 static handler_t wstunnel_create_env(gw_handler_ctx *gwhctx) {
350     handler_ctx *hctx = (handler_ctx *)gwhctx;
351     request_st * const r = hctx->gw.r;
352     handler_t rc;
353     if (0 == r->reqbody_length) {
354         http_response_upgrade_read_body_unknown(r);
355         chunkqueue_append_chunkqueue(r->reqbody_queue, r->read_queue);
356     }
357     rc = mod_wstunnel_handshake_create_response(hctx);
358     if (rc != HANDLER_GO_ON) return rc;
359 
360     r->http_status = 101; /* Switching Protocols */
361     r->resp_body_started = 1;
362 
363     hctx->ping_ts = log_epoch_secs;
364     gw_set_transparent(&hctx->gw);
365 
366     return HANDLER_GO_ON;
367 }
368 
369 static handler_t wstunnel_stdin_append(gw_handler_ctx *gwhctx) {
370     /* prepare websocket frames to backend */
371     /* (caller should verify r->reqbody_queue) */
372     /*assert(!chunkqueue_is_empty(r->reqbody_queue));*/
373     handler_ctx *hctx = (handler_ctx *)gwhctx;
374     if (0 == mod_wstunnel_frame_recv(hctx))
375         return HANDLER_GO_ON;
376     else {
377         /*(error)*/
378         /* future: might differentiate client close request from client error,
379          *         and then send 1000 or 1001 */
380         request_st * const r = hctx->gw.r;
381         DEBUG_LOG_INFO("disconnected from client (fd=%d)", r->con->fd);
382         DEBUG_LOG_DEBUG("send close response to client (fd=%d)", r->con->fd);
383         mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, CONST_STR_LEN("1000")); /* 1000 Normal Closure */
384         gw_handle_request_reset(r, hctx->gw.plugin_data);
385         return HANDLER_FINISHED;
386     }
387 }
388 
389 static handler_t wstunnel_recv_parse(request_st * const r, http_response_opts * const opts, buffer * const b, size_t n) {
390     handler_ctx *hctx = (handler_ctx *)opts->pdata;
391     DEBUG_LOG_DEBUG("recv data from backend (fd=%d), size=%zx", hctx->gw.fd, n);
392     if (0 == n) return HANDLER_FINISHED;
393     if (mod_wstunnel_frame_send(hctx,hctx->frame.type_backend,b->ptr,n) < 0) {
394         DEBUG_LOG_ERR("%s", "fail to send data to client");
395         return HANDLER_ERROR;
396     }
397     buffer_clear(b);
398     UNUSED(r);
399     return HANDLER_GO_ON;
400 }
401 
402 static int wstunnel_is_allowed_origin(request_st * const r, handler_ctx * const hctx) {
403     /* If allowed origins is set (and not empty list), fail closed if no match.
404      * Note that origin provided in request header has not been normalized, so
405      * change in case or other non-normal forms might not match allowed list */
406     const array * const allowed_origins = hctx->conf.origins;
407     const buffer *origin = NULL;
408     size_t olen;
409 
410     if (NULL == allowed_origins || 0 == allowed_origins->used) {
411         DEBUG_LOG_INFO("%s", "allowed origins not specified");
412         return 1;
413     }
414 
415     /* "Origin" header is preferred
416      * ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */
417     origin = http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Origin"));
418     if (NULL == origin) {
419         origin =
420           http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Origin"));
421     }
422     olen = buffer_string_length(origin);
423     if (0 == olen) {
424         DEBUG_LOG_ERR("%s", "Origin header is invalid");
425         r->http_status = 400; /* Bad Request */
426         return 0;
427     }
428 
429     for (size_t i = 0; i < allowed_origins->used; ++i) {
430         buffer *b = &((data_string *)allowed_origins->data[i])->value;
431         size_t blen = buffer_string_length(b);
432         if ((olen > blen ? origin->ptr[olen-blen-1] == '.' : olen == blen)
433             && buffer_is_equal_right_len(origin, b, blen)) {
434             DEBUG_LOG_INFO("%s matches allowed origin: %s",origin->ptr,b->ptr);
435             return 1;
436         }
437     }
438     DEBUG_LOG_INFO("%s does not match any allowed origins", origin->ptr);
439     r->http_status = 403; /* Forbidden */
440     return 0;
441 }
442 
443 static int wstunnel_check_request(request_st * const r, handler_ctx * const hctx) {
444     const buffer * const vers =
445       http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Version"));
446     const long hybivers = (NULL != vers)
447       ? light_isdigit(*vers->ptr) ? strtol(vers->ptr, NULL, 10) : -1
448       : 0;
449     if (hybivers < 0 || hybivers > INT_MAX) {
450         DEBUG_LOG_ERR("%s", "invalid Sec-WebSocket-Version");
451         r->http_status = 400; /* Bad Request */
452         return -1;
453     }
454 
455     /*(redundant since HTTP/1.1 required in mod_wstunnel_check_extension())*/
456     if (buffer_is_empty(r->http_host)) {
457         DEBUG_LOG_ERR("%s", "Host header does not exist");
458         r->http_status = 400; /* Bad Request */
459         return -1;
460     }
461 
462     if (!wstunnel_is_allowed_origin(r, hctx)) {
463         return -1;
464     }
465 
466     return (int)hybivers;
467 }
468 
469 static void wstunnel_backend_error(gw_handler_ctx *gwhctx) {
470     handler_ctx *hctx = (handler_ctx *)gwhctx;
471     if (hctx->gw.state == GW_STATE_WRITE || hctx->gw.state == GW_STATE_READ) {
472         mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, CONST_STR_LEN("1001")); /* 1001 Going Away */
473     }
474 }
475 
476 static void wstunnel_handler_ctx_free(void *gwhctx) {
477     handler_ctx *hctx = (handler_ctx *)gwhctx;
478     chunk_buffer_release(hctx->frame.payload);
479 }
480 
481 static handler_t wstunnel_handler_setup (request_st * const r, plugin_data * const p) {
482     handler_ctx *hctx = r->plugin_ctx[p->id];
483     int hybivers;
484     hctx->errh = r->conf.errh;/*(for mod_wstunnel-specific DEBUG_* macros)*/
485     hctx->conf = p->conf; /*(copies struct)*/
486     hybivers = wstunnel_check_request(r, hctx);
487     if (hybivers < 0) return HANDLER_FINISHED;
488     hctx->hybivers = hybivers;
489     if (0 == hybivers) {
490         DEBUG_LOG_INFO("WebSocket Version = %s", "hybi-00");
491     }
492     else {
493         DEBUG_LOG_INFO("WebSocket Version = %d", hybivers);
494     }
495 
496     hctx->gw.opts.backend     = BACKEND_PROXY; /*(act proxy-like; not used)*/
497     hctx->gw.opts.pdata       = hctx;
498     hctx->gw.opts.parse       = wstunnel_recv_parse;
499     hctx->gw.stdin_append     = wstunnel_stdin_append;
500     hctx->gw.create_env       = wstunnel_create_env;
501     hctx->gw.handler_ctx_free = wstunnel_handler_ctx_free;
502     hctx->gw.backend_error    = wstunnel_backend_error;
503     hctx->gw.response         = chunk_buffer_acquire();
504 
505     hctx->frame.state         = MOD_WEBSOCKET_FRAME_STATE_INIT;
506     hctx->frame.ctl.siz       = 0;
507     hctx->frame.payload       = chunk_buffer_acquire();
508 
509     unsigned int binary = hctx->conf.frame_type; /*(0 = "text"; 1 = "binary")*/
510     if (!binary) {
511         const buffer *vb =
512           http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Protocol"));
513         if (NULL != vb) {
514             for (const char *s = vb->ptr; *s; ++s) {
515                 while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s;
516                 if (buffer_eq_icase_ssn(s, CONST_STR_LEN("binary"))) {
517                     s += sizeof("binary")-1;
518                     while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s;
519                     if (*s==','||*s=='\0') {
520                         hctx->subproto = 1;
521                         binary = 1;
522                         break;
523                     }
524                 }
525                 else if (buffer_eq_icase_ssn(s, CONST_STR_LEN("base64"))) {
526                     s += sizeof("base64")-1;
527                     while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s;
528                     if (*s==','||*s=='\0') {
529                         hctx->subproto = -1;
530                         break;
531                     }
532                 }
533                 s = strchr(s, ',');
534                 if (NULL == s) break;
535             }
536         }
537     }
538 
539     if (binary) {
540         DEBUG_LOG_INFO("%s", "will recv binary data from backend");
541         hctx->frame.type         = MOD_WEBSOCKET_FRAME_TYPE_BIN;
542         hctx->frame.type_before  = MOD_WEBSOCKET_FRAME_TYPE_BIN;
543         hctx->frame.type_backend = MOD_WEBSOCKET_FRAME_TYPE_BIN;
544     }
545     else {
546         DEBUG_LOG_INFO("%s", "will recv text data from backend");
547         hctx->frame.type         = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
548         hctx->frame.type_before  = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
549         hctx->frame.type_backend = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
550     }
551 
552     return HANDLER_GO_ON;
553 }
554 
555 static handler_t mod_wstunnel_check_extension(request_st * const r, void *p_d) {
556     plugin_data *p = p_d;
557     const buffer *vb;
558     handler_t rc;
559 
560     if (NULL != r->handler_module)
561         return HANDLER_GO_ON;
562     if (r->http_method != HTTP_METHOD_GET)
563         return HANDLER_GO_ON;
564     if (r->http_version != HTTP_VERSION_1_1)
565         return HANDLER_GO_ON;
566 
567     /*
568      * Connection: upgrade, keep-alive, ...
569      * Upgrade: WebSocket, ...
570      */
571     vb = http_header_request_get(r, HTTP_HEADER_UPGRADE, CONST_STR_LEN("Upgrade"));
572     if (NULL == vb
573         || !http_header_str_contains_token(CONST_BUF_LEN(vb), CONST_STR_LEN("websocket")))
574         return HANDLER_GO_ON;
575     vb = http_header_request_get(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"));
576     if (NULL == vb
577         || !http_header_str_contains_token(CONST_BUF_LEN(vb), CONST_STR_LEN("upgrade")))
578         return HANDLER_GO_ON;
579 
580     mod_wstunnel_patch_config(r, p);
581     if (NULL == p->conf.gw.exts) return HANDLER_GO_ON;
582 
583     rc = gw_check_extension(r, (gw_plugin_data *)p, 1, sizeof(handler_ctx));
584     return (HANDLER_GO_ON == rc && r->handler_module == p->self)
585       ? wstunnel_handler_setup(r, p)
586       : rc;
587 }
588 
589 TRIGGER_FUNC(mod_wstunnel_handle_trigger) {
590     const plugin_data * const p = p_d;
591     const time_t cur_ts = log_epoch_secs + 1;
592 
593     gw_handle_trigger(srv, p_d);
594 
595     for (uint32_t i = 0; i < srv->conns.used; ++i) {
596         connection *con = srv->conns.ptr[i];
597         request_st * const r = &con->request;
598         handler_ctx *hctx = r->plugin_ctx[p->id];
599         if (NULL == hctx || r->handler_module != p->self)
600             continue;
601 
602         if (hctx->gw.state != GW_STATE_WRITE && hctx->gw.state != GW_STATE_READ)
603             continue;
604 
605         if (cur_ts - con->read_idle_ts > r->conf.max_read_idle) {
606             DEBUG_LOG_INFO("timeout client (fd=%d)", con->fd);
607             mod_wstunnel_frame_send(hctx,MOD_WEBSOCKET_FRAME_TYPE_CLOSE,NULL,0);
608             gw_handle_request_reset(r, p_d);
609             joblist_append(con);
610             /* avoid server.c closing connection with error due to max_read_idle
611              * (might instead run joblist after plugins_call_handle_trigger())*/
612             con->read_idle_ts = cur_ts;
613             continue;
614         }
615 
616         if (0 != hctx->hybivers
617             && hctx->conf.ping_interval > 0
618             && (time_t)hctx->conf.ping_interval + hctx->ping_ts < cur_ts) {
619             hctx->ping_ts = cur_ts;
620             mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_PING, CONST_STR_LEN("ping"));
621             joblist_append(con);
622             continue;
623         }
624     }
625 
626     return HANDLER_GO_ON;
627 }
628 
629 int mod_wstunnel_plugin_init(plugin *p);
630 int mod_wstunnel_plugin_init(plugin *p) {
631     p->version           = LIGHTTPD_VERSION_ID;
632     p->name              = "wstunnel";
633     p->init              = mod_wstunnel_init;
634     p->cleanup           = gw_free;
635     p->set_defaults      = mod_wstunnel_set_defaults;
636     p->handle_request_reset = gw_handle_request_reset;
637     p->handle_uri_clean  = mod_wstunnel_check_extension;
638     p->handle_subrequest = gw_handle_subrequest;
639     p->handle_trigger    = mod_wstunnel_handle_trigger;
640     p->handle_waitpid    = gw_handle_waitpid_cb;
641     return 0;
642 }
643 
644 
645 
646 
647 /*
648  * modified from Norio Kobota mod_websocket_handshake.c
649  */
650 
651 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
652 
653 #include "sys-crypto-md.h"  /* lighttpd */
654 #include "sys-endian.h"     /* lighttpd */
655 
656 static int get_key3(request_st * const r, char *buf) {
657     /* 8 bytes should have been sent with request
658      * for draft-ietf-hybi-thewebsocketprotocol-00 */
659     chunkqueue *cq = r->reqbody_queue;
660     size_t bytes = 8;
661     /*(caller should ensure bytes available prior to calling this routine)*/
662     /*assert(chunkqueue_length(cq) >= 8);*/
663     for (chunk *c = cq->first; NULL != c; c = c->next) {
664         /*(chunk_remaining_length() on MEM_CHUNK)*/
665         size_t n = (size_t)(buffer_string_length(c->mem) - c->offset);
666         /*(expecting 8 bytes to be in memory directly after headers)*/
667         if (c->type != MEM_CHUNK) break; /* FILE_CHUNK not handled here */
668         if (n > bytes) n = bytes;
669         memcpy(buf, c->mem->ptr+c->offset, n);
670         buf += n;
671         if (0 == (bytes -= n)) break;
672     }
673     if (0 != bytes) return -1;
674     chunkqueue_mark_written(cq, 8);
675     return 0;
676 }
677 
678 static int get_key_number(uint32_t *ret, const buffer *b) {
679     const char * const s = b->ptr;
680     size_t j = 0;
681     unsigned long n;
682     uint32_t sp = 0;
683     char tmp[10 + 1]; /* #define UINT32_MAX_STRLEN 10 */
684 
685     for (size_t i = 0, used = buffer_string_length(b); i < used; ++i) {
686         if (light_isdigit(s[i])) {
687             tmp[j] = s[i];
688             if (++j >= sizeof(tmp)) return -1;
689         }
690         else if (s[i] == ' ') ++sp; /* count num spaces */
691     }
692     tmp[j] = '\0';
693     n = strtoul(tmp, NULL, 10);
694     if (n > UINT32_MAX || 0 == sp || !light_isdigit(*tmp)) return -1;
695     *ret = (uint32_t)n / sp;
696     return 0;
697 }
698 
699 static int create_MD5_sum(request_st * const r) {
700     uint32_t buf[4]; /* MD5 binary hash len */
701     li_MD5_CTX ctx;
702 
703     const buffer *key1 =
704       http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key1"));
705     const buffer *key2 =
706       http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key2"));
707 
708     if (NULL == key1 || get_key_number(buf+0, key1) < 0 ||
709         NULL == key2 || get_key_number(buf+1, key2) < 0 ||
710         get_key3(r, (char *)(buf+2)) < 0) {
711         return -1;
712     }
713   #ifdef __BIG_ENDIAN__
714   #define ws_htole32(s,u)\
715     (s)[0]=((u)>>24);    \
716     (s)[1]=((u)>>16);    \
717     (s)[2]=((u)>>8);     \
718     (s)[3]=((u))
719     ws_htole32((unsigned char *)(buf+0), buf[0]);
720     ws_htole32((unsigned char *)(buf+1), buf[1]);
721   #endif
722     li_MD5_Init(&ctx);
723     li_MD5_Update(&ctx, buf, sizeof(buf));
724     li_MD5_Final((unsigned char *)buf, &ctx); /*(overwrite buf[] with result)*/
725     chunkqueue_append_mem(r->write_queue, (char *)buf, sizeof(buf));
726     return 0;
727 }
728 
729 static int create_response_ietf_00(handler_ctx *hctx) {
730     request_st * const r = hctx->gw.r;
731     buffer *value = r->tmp_buf;
732 
733     /* "Origin" header is preferred
734      * ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */
735     const buffer *origin = http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Origin"));
736     if (NULL == origin) {
737         origin =
738           http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Origin"));
739     }
740     if (NULL == origin) {
741         DEBUG_LOG_ERR("%s", "Origin header is invalid");
742         return -1;
743     }
744     if (buffer_is_empty(r->http_host)) {
745         DEBUG_LOG_ERR("%s", "Host header does not exist");
746         return -1;
747     }
748 
749     /* calc MD5 sum from keys */
750     if (create_MD5_sum(r) < 0) {
751         DEBUG_LOG_ERR("%s", "Sec-WebSocket-Key is invalid");
752         return -1;
753     }
754 
755     http_header_response_set(r, HTTP_HEADER_UPGRADE,
756                              CONST_STR_LEN("Upgrade"),
757                              CONST_STR_LEN("websocket"));
758   #if 0 /*(added later in http_response_write_header())*/
759     http_header_response_append(r, HTTP_HEADER_CONNECTION,
760                                 CONST_STR_LEN("Connection"),
761                                 CONST_STR_LEN("upgrade"));
762   #endif
763   #if 0 /*(Sec-WebSocket-Origin header is not required for hybi-00)*/
764     /* Note: it is insecure to simply reflect back origin provided by client
765      * (if admin did not configure restricted list of valid origins)
766      * (see wstunnel_check_request()) */
767     http_header_response_set(r, HTTP_HEADER_OTHER,
768                              CONST_STR_LEN("Sec-WebSocket-Origin"),
769                              CONST_BUF_LEN(origin));
770   #endif
771 
772     if (buffer_is_equal_string(&r->uri.scheme, CONST_STR_LEN("https")))
773         buffer_copy_string_len(value, CONST_STR_LEN("wss://"));
774     else
775         buffer_copy_string_len(value, CONST_STR_LEN("ws://"));
776     buffer_append_string_buffer(value, r->http_host);
777     buffer_append_string_buffer(value, &r->uri.path);
778     http_header_response_set(r, HTTP_HEADER_OTHER,
779                              CONST_STR_LEN("Sec-WebSocket-Location"),
780                              CONST_BUF_LEN(value));
781 
782     return 0;
783 }
784 
785 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
786 
787 
788 #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
789 
790 #include "sys-crypto-md.h"  /* lighttpd */
791 #include "base64.h"         /* lighttpd */
792 
793 static int create_response_rfc_6455(handler_ctx *hctx) {
794     request_st * const r = hctx->gw.r;
795     SHA_CTX sha;
796     unsigned char sha_digest[SHA_DIGEST_LENGTH];
797 
798     const buffer *value_wskey =
799       http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key"));
800     if (NULL == value_wskey) {
801         DEBUG_LOG_ERR("%s", "Sec-WebSocket-Key is invalid");
802         return -1;
803     }
804 
805     /* get SHA1 hash of key */
806     /* refer: RFC-6455 Sec.1.3 Opening Handshake */
807     SHA1_Init(&sha);
808     SHA1_Update(&sha, (const unsigned char *)CONST_BUF_LEN(value_wskey));
809     SHA1_Update(&sha, (const unsigned char *)CONST_STR_LEN("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
810     SHA1_Final(sha_digest, &sha);
811 
812     http_header_response_set(r, HTTP_HEADER_UPGRADE,
813                              CONST_STR_LEN("Upgrade"),
814                              CONST_STR_LEN("websocket"));
815   #if 0 /*(added later in http_response_write_header())*/
816     http_header_response_append(r, HTTP_HEADER_CONNECTION,
817                                 CONST_STR_LEN("Connection"),
818                                 CONST_STR_LEN("upgrade"));
819   #endif
820 
821     buffer *value = r->tmp_buf;
822     buffer_clear(value);
823     buffer_append_base64_encode(value, sha_digest, SHA_DIGEST_LENGTH, BASE64_STANDARD);
824     http_header_response_set(r, HTTP_HEADER_OTHER,
825                              CONST_STR_LEN("Sec-WebSocket-Accept"),
826                              CONST_BUF_LEN(value));
827 
828     if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_BIN)
829         http_header_response_set(r, HTTP_HEADER_OTHER,
830                                  CONST_STR_LEN("Sec-WebSocket-Protocol"),
831                                  CONST_STR_LEN("binary"));
832     else if (-1 == hctx->subproto)
833         http_header_response_set(r, HTTP_HEADER_OTHER,
834                                  CONST_STR_LEN("Sec-WebSocket-Protocol"),
835                                  CONST_STR_LEN("base64"));
836 
837     return 0;
838 }
839 
840 #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
841 
842 
843 handler_t mod_wstunnel_handshake_create_response(handler_ctx *hctx) {
844     request_st * const r = hctx->gw.r;
845   #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
846     if (hctx->hybivers >= 8) {
847         DEBUG_LOG_DEBUG("%s", "send handshake response");
848         if (0 != create_response_rfc_6455(hctx)) {
849             r->http_status = 400; /* Bad Request */
850             return HANDLER_ERROR;
851         }
852         return HANDLER_GO_ON;
853     }
854   #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
855 
856   #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
857     if (hctx->hybivers == 0) {
858       #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
859         /* 8 bytes should have been sent with request
860          * for draft-ietf-hybi-thewebsocketprotocol-00 */
861         chunkqueue *cq = r->reqbody_queue;
862         if (chunkqueue_length(cq) < 8)
863             return HANDLER_WAIT_FOR_EVENT;
864       #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
865 
866         DEBUG_LOG_DEBUG("%s", "send handshake response");
867         if (0 != create_response_ietf_00(hctx)) {
868             r->http_status = 400; /* Bad Request */
869             return HANDLER_ERROR;
870         }
871         return HANDLER_GO_ON;
872     }
873   #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
874 
875     DEBUG_LOG_ERR("%s", "not supported WebSocket Version");
876     r->http_status = 503; /* Service Unavailable */
877     return HANDLER_ERROR;
878 }
879 
880 
881 
882 
883 /*
884  * modified from Norio Kobota mod_websocket_frame.c
885  */
886 
887 #include "base64.h"     /* lighttpd */
888 #include "http_chunk.h" /* lighttpd */
889 
890 #define MOD_WEBSOCKET_BUFMAX (0x0fffff)
891 
892 #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
893 
894 #include <stdlib.h>
895 static int send_ietf_00(handler_ctx *hctx, mod_wstunnel_frame_type_t type, const char *payload, size_t siz) {
896     static const char head =  0; /* 0x00 */
897     static const char tail = ~0; /* 0xff */
898     request_st * const r = hctx->gw.r;
899     char *mem;
900     size_t len;
901 
902     switch (type) {
903     case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
904         if (0 == siz) return 0;
905         http_chunk_append_mem(r, &head, 1);
906         http_chunk_append_mem(r, payload, siz);
907         http_chunk_append_mem(r, &tail, 1);
908         len = siz+2;
909         break;
910     case MOD_WEBSOCKET_FRAME_TYPE_BIN:
911         if (0 == siz) return 0;
912         http_chunk_append_mem(r, &head, 1);
913         len = 4*(siz/3)+4+1;
914         /* avoid accumulating too much data in memory; send to tmpfile */
915         mem = malloc(len);
916         force_assert(mem);
917         len=li_to_base64(mem,len,(unsigned char *)payload,siz,BASE64_STANDARD);
918         http_chunk_append_mem(r, mem, len);
919         free(mem);
920         http_chunk_append_mem(r, &tail, 1);
921         len += 2;
922         break;
923     case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
924         http_chunk_append_mem(r, &tail, 1);
925         http_chunk_append_mem(r, &head, 1);
926         len = 2;
927         break;
928     default:
929         DEBUG_LOG_ERR("%s", "invalid frame type");
930         return -1;
931     }
932     DEBUG_LOG_DEBUG("send data to client (fd=%d), frame size=%zx",
933                     r->con->fd, len);
934     return 0;
935 }
936 
937 static int recv_ietf_00(handler_ctx *hctx) {
938     request_st * const r = hctx->gw.r;
939     chunkqueue *cq = r->reqbody_queue;
940     buffer *payload = hctx->frame.payload;
941     char *mem;
942     DEBUG_LOG_DEBUG("recv data from client (fd=%d), size=%llx",
943                     r->con->fd, (long long)chunkqueue_length(cq));
944     for (chunk *c = cq->first; c; c = c->next) {
945         char *frame = c->mem->ptr+c->offset;
946         /*(chunk_remaining_length() on MEM_CHUNK)*/
947         size_t flen = (size_t)(buffer_string_length(c->mem) - c->offset);
948         /*(FILE_CHUNK not handled, but might need to add support)*/
949         force_assert(c->type == MEM_CHUNK);
950         for (size_t i = 0; i < flen; ) {
951             switch (hctx->frame.state) {
952             case MOD_WEBSOCKET_FRAME_STATE_INIT:
953                 hctx->frame.ctl.siz = 0;
954                 if (frame[i] == 0x00) {
955                     hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD;
956                     i++;
957                 }
958                 else if (((unsigned char *)frame)[i] == 0xff) {
959                     DEBUG_LOG_DEBUG("%s", "recv close frame");
960                     return -1;
961                 }
962                 else {
963                     DEBUG_LOG_DEBUG("%s", "recv invalid frame");
964                     return -1;
965                 }
966                 break;
967             case MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD:
968                 mem = (char *)memchr(frame+i, 0xff, flen - i);
969                 if (mem == NULL) {
970                     DEBUG_LOG_DEBUG("got continuous payload, size=%zx", flen-i);
971                     hctx->frame.ctl.siz += flen - i;
972                     if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
973                         DEBUG_LOG_WARN("frame size has been exceeded: %x",
974                                        MOD_WEBSOCKET_BUFMAX);
975                         return -1;
976                     }
977                     buffer_append_string_len(payload, frame+i, flen - i);
978                     i += flen - i;
979                 }
980                 else {
981                     DEBUG_LOG_DEBUG("got final payload, size=%zx",
982                                     mem - (frame+i));
983                     hctx->frame.ctl.siz += (mem - (frame+i));
984                     if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
985                         DEBUG_LOG_WARN("frame size has been exceeded: %x",
986                                        MOD_WEBSOCKET_BUFMAX);
987                         return -1;
988                     }
989                     buffer_append_string_len(payload, frame+i, mem - (frame+i));
990                     i += (mem - (frame+i));
991                     hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
992                 }
993                 i++;
994                 if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_TEXT
995                     && !buffer_is_empty(payload)) {
996                     hctx->frame.ctl.siz = 0;
997                     chunkqueue_append_buffer(hctx->gw.wb, payload);
998                     buffer_clear(payload);
999                 }
1000                 else {
1001                     if (hctx->frame.state == MOD_WEBSOCKET_FRAME_STATE_INIT
1002                         && !buffer_is_empty(payload)) {
1003                         buffer *b;
1004                         size_t len = buffer_string_length(payload);
1005                         len = (len+3)/4*3+1;
1006                         chunkqueue_get_memory(hctx->gw.wb, &len);
1007                         b = hctx->gw.wb->last->mem;
1008                         len = buffer_string_length(b);
1009                         DEBUG_LOG_DEBUG("try to base64 decode: %s",
1010                                         payload->ptr);
1011                         if (NULL == buffer_append_base64_decode(b, CONST_BUF_LEN(payload), BASE64_STANDARD)) {
1012                             DEBUG_LOG_ERR("%s", "fail to base64-decode");
1013                             return -1;
1014                         }
1015                         buffer_clear(payload);
1016                         /*chunkqueue_use_memory()*/
1017                         hctx->gw.wb->bytes_in += buffer_string_length(b)-len;
1018                     }
1019                 }
1020                 break;
1021             default: /* never reach */
1022                 DEBUG_LOG_ERR("%s", "BUG: unknown state");
1023                 return -1;
1024             }
1025         }
1026     }
1027     /* XXX: should add ability to handle and preserve partial frames above */
1028     /*(not chunkqueue_reset(); do not reset cq->bytes_in, cq->bytes_out)*/
1029     chunkqueue_mark_written(cq, chunkqueue_length(cq));
1030     return 0;
1031 }
1032 
1033 #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
1034 
1035 
1036 #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
1037 
1038 #define MOD_WEBSOCKET_OPCODE_CONT   0x00
1039 #define MOD_WEBSOCKET_OPCODE_TEXT   0x01
1040 #define MOD_WEBSOCKET_OPCODE_BIN    0x02
1041 #define MOD_WEBSOCKET_OPCODE_CLOSE  0x08
1042 #define MOD_WEBSOCKET_OPCODE_PING   0x09
1043 #define MOD_WEBSOCKET_OPCODE_PONG   0x0A
1044 
1045 #define MOD_WEBSOCKET_FRAME_LEN16   0x7E
1046 #define MOD_WEBSOCKET_FRAME_LEN63   0x7F
1047 #define MOD_WEBSOCKET_FRAME_LEN16_CNT  2
1048 #define MOD_WEBSOCKET_FRAME_LEN63_CNT  8
1049 
1050 static int send_rfc_6455(handler_ctx *hctx, mod_wstunnel_frame_type_t type, const char *payload, size_t siz) {
1051     char mem[10];
1052     size_t len;
1053 
1054     /* allowed null payload for ping, pong, close frame */
1055     if (payload == NULL && (   type == MOD_WEBSOCKET_FRAME_TYPE_TEXT
1056                             || type == MOD_WEBSOCKET_FRAME_TYPE_BIN   )) {
1057         return -1;
1058     }
1059 
1060     switch (type) {
1061     case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
1062         mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_TEXT);
1063         DEBUG_LOG_DEBUG("%s", "type = text");
1064         break;
1065     case MOD_WEBSOCKET_FRAME_TYPE_BIN:
1066         mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_BIN);
1067         DEBUG_LOG_DEBUG("%s", "type = binary");
1068         break;
1069     case MOD_WEBSOCKET_FRAME_TYPE_PING:
1070         mem[0] = (char) (0x80 | MOD_WEBSOCKET_OPCODE_PING);
1071         DEBUG_LOG_DEBUG("%s", "type = ping");
1072         break;
1073     case MOD_WEBSOCKET_FRAME_TYPE_PONG:
1074         mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_PONG);
1075         DEBUG_LOG_DEBUG("%s", "type = pong");
1076         break;
1077     case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
1078     default:
1079         mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_CLOSE);
1080         DEBUG_LOG_DEBUG("%s", "type = close");
1081         break;
1082     }
1083 
1084     DEBUG_LOG_DEBUG("payload size=%zx", siz);
1085     if (siz < MOD_WEBSOCKET_FRAME_LEN16) {
1086         mem[1] = siz;
1087         len = 2;
1088     }
1089     else if (siz <= UINT16_MAX) {
1090         mem[1] = MOD_WEBSOCKET_FRAME_LEN16;
1091         mem[2] = (siz >> 8) & 0xff;
1092         mem[3] = siz & 0xff;
1093         len = 1+MOD_WEBSOCKET_FRAME_LEN16_CNT+1;
1094     }
1095     else {
1096         mem[1] = MOD_WEBSOCKET_FRAME_LEN63;
1097         mem[2] = 0;
1098         mem[3] = 0;
1099         mem[4] = 0;
1100         mem[5] = 0;
1101         mem[6] = (siz >> 24) & 0xff;
1102         mem[7] = (siz >> 16) & 0xff;
1103         mem[8] = (siz >> 8) & 0xff;
1104         mem[9] = siz & 0xff;
1105         len = 1+MOD_WEBSOCKET_FRAME_LEN63_CNT+1;
1106     }
1107     request_st * const r = hctx->gw.r;
1108     http_chunk_append_mem(r, mem, len);
1109     if (siz) http_chunk_append_mem(r, payload, siz);
1110     DEBUG_LOG_DEBUG("send data to client (fd=%d), frame size=%zx",
1111                     r->con->fd, len+siz);
1112     return 0;
1113 }
1114 
1115 static void unmask_payload(handler_ctx *hctx) {
1116     buffer * const b = hctx->frame.payload;
1117     for (size_t i = 0, used = buffer_string_length(b); i < used; ++i) {
1118         b->ptr[i] ^= hctx->frame.ctl.mask[hctx->frame.ctl.mask_cnt];
1119         hctx->frame.ctl.mask_cnt = (hctx->frame.ctl.mask_cnt + 1) % 4;
1120     }
1121 }
1122 
1123 static int recv_rfc_6455(handler_ctx *hctx) {
1124     request_st * const r = hctx->gw.r;
1125     chunkqueue *cq = r->reqbody_queue;
1126     buffer *payload = hctx->frame.payload;
1127     DEBUG_LOG_DEBUG("recv data from client (fd=%d), size=%llx",
1128                     r->con->fd, (long long)chunkqueue_length(cq));
1129     for (chunk *c = cq->first; c; c = c->next) {
1130         char *frame = c->mem->ptr+c->offset;
1131         /*(chunk_remaining_length() on MEM_CHUNK)*/
1132         size_t flen = (size_t)(buffer_string_length(c->mem) - c->offset);
1133         /*(FILE_CHUNK not handled, but might need to add support)*/
1134         force_assert(c->type == MEM_CHUNK);
1135         for (size_t i = 0; i < flen; ) {
1136             switch (hctx->frame.state) {
1137             case MOD_WEBSOCKET_FRAME_STATE_INIT:
1138                 switch (frame[i] & 0x0f) {
1139                 case MOD_WEBSOCKET_OPCODE_CONT:
1140                     DEBUG_LOG_DEBUG("%s", "type = continue");
1141                     hctx->frame.type = hctx->frame.type_before;
1142                     break;
1143                 case MOD_WEBSOCKET_OPCODE_TEXT:
1144                     DEBUG_LOG_DEBUG("%s", "type = text");
1145                     hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_TEXT;
1146                     hctx->frame.type_before = hctx->frame.type;
1147                     break;
1148                 case MOD_WEBSOCKET_OPCODE_BIN:
1149                     DEBUG_LOG_DEBUG("%s", "type = binary");
1150                     hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_BIN;
1151                     hctx->frame.type_before = hctx->frame.type;
1152                     break;
1153                 case MOD_WEBSOCKET_OPCODE_PING:
1154                     DEBUG_LOG_DEBUG("%s", "type = ping");
1155                     hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PING;
1156                     break;
1157                 case MOD_WEBSOCKET_OPCODE_PONG:
1158                     DEBUG_LOG_DEBUG("%s", "type = pong");
1159                     hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PONG;
1160                     break;
1161                 case MOD_WEBSOCKET_OPCODE_CLOSE:
1162                     DEBUG_LOG_DEBUG("%s", "type = close");
1163                     hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_CLOSE;
1164                     return -1;
1165                     break;
1166                 default:
1167                     DEBUG_LOG_ERR("%s", "type is invalid");
1168                     return -1;
1169                     break;
1170                 }
1171                 i++;
1172                 hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH;
1173                 break;
1174             case MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH:
1175                 if ((frame[i] & 0x80) != 0x80) {
1176                     DEBUG_LOG_ERR("%s", "payload was not masked");
1177                     return -1;
1178                 }
1179                 hctx->frame.ctl.mask_cnt = 0;
1180                 hctx->frame.ctl.siz = (uint64_t)(frame[i] & 0x7f);
1181                 if (hctx->frame.ctl.siz == 0) {
1182                     DEBUG_LOG_DEBUG("specified payload size=%llx",
1183                                     (unsigned long long)hctx->frame.ctl.siz);
1184                     hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK;
1185                 }
1186                 else if (hctx->frame.ctl.siz == MOD_WEBSOCKET_FRAME_LEN16) {
1187                     hctx->frame.ctl.siz = 0;
1188                     hctx->frame.ctl.siz_cnt = MOD_WEBSOCKET_FRAME_LEN16_CNT;
1189                     hctx->frame.state =
1190                         MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH;
1191                 }
1192                 else if (hctx->frame.ctl.siz == MOD_WEBSOCKET_FRAME_LEN63) {
1193                     hctx->frame.ctl.siz = 0;
1194                     hctx->frame.ctl.siz_cnt = MOD_WEBSOCKET_FRAME_LEN63_CNT;
1195                     hctx->frame.state =
1196                         MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH;
1197                 }
1198                 else {
1199                     DEBUG_LOG_DEBUG("specified payload size=%llx",
1200                                     (unsigned long long)hctx->frame.ctl.siz);
1201                     hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK;
1202                 }
1203                 i++;
1204                 break;
1205             case MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH:
1206                 hctx->frame.ctl.siz =
1207                     (hctx->frame.ctl.siz << 8) + (frame[i] & 0xff);
1208                 hctx->frame.ctl.siz_cnt--;
1209                 if (hctx->frame.ctl.siz_cnt <= 0) {
1210                     if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_PING &&
1211                         hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
1212                         DEBUG_LOG_WARN("frame size has been exceeded: %x",
1213                                        MOD_WEBSOCKET_BUFMAX);
1214                         return -1;
1215                     }
1216                     DEBUG_LOG_DEBUG("specified payload size=%llx",
1217                                     (unsigned long long)hctx->frame.ctl.siz);
1218                     hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK;
1219                 }
1220                 i++;
1221                 break;
1222             case MOD_WEBSOCKET_FRAME_STATE_READ_MASK:
1223                 hctx->frame.ctl.mask[hctx->frame.ctl.mask_cnt] = frame[i];
1224                 hctx->frame.ctl.mask_cnt++;
1225                 if (hctx->frame.ctl.mask_cnt >= MOD_WEBSOCKET_MASK_CNT) {
1226                     hctx->frame.ctl.mask_cnt = 0;
1227                     if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_PING &&
1228                         hctx->frame.ctl.siz == 0) {
1229                         mod_wstunnel_frame_send(hctx,
1230                                                 MOD_WEBSOCKET_FRAME_TYPE_PONG,
1231                                                 NULL, 0);
1232                     }
1233                     if (hctx->frame.ctl.siz == 0) {
1234                         hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
1235                     }
1236                     else {
1237                         hctx->frame.state =
1238                             MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD;
1239                     }
1240                 }
1241                 i++;
1242                 break;
1243             case MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD:
1244                 /* hctx->frame.ctl.siz <= SIZE_MAX */
1245                 if (hctx->frame.ctl.siz <= flen - i) {
1246                     DEBUG_LOG_DEBUG("read payload, size=%llx",
1247                                     (unsigned long long)hctx->frame.ctl.siz);
1248                     buffer_append_string_len(payload, frame+i, (size_t)
1249                                              (hctx->frame.ctl.siz & SIZE_MAX));
1250                     i += (size_t)(hctx->frame.ctl.siz & SIZE_MAX);
1251                     hctx->frame.ctl.siz = 0;
1252                     hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
1253                     DEBUG_LOG_DEBUG("rest of frame size=%zx", flen - i);
1254                 /* SIZE_MAX < hctx->frame.ctl.siz */
1255                 }
1256                 else {
1257                     DEBUG_LOG_DEBUG("read payload, size=%zx", flen - i);
1258                     buffer_append_string_len(payload, frame+i, flen - i);
1259                     hctx->frame.ctl.siz -= flen - i;
1260                     i += flen - i;
1261                     DEBUG_LOG_DEBUG("rest of payload size=%llx",
1262                                     (unsigned long long)hctx->frame.ctl.siz);
1263                 }
1264                 switch (hctx->frame.type) {
1265                 case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
1266                 case MOD_WEBSOCKET_FRAME_TYPE_BIN:
1267                   {
1268                     unmask_payload(hctx);
1269                     chunkqueue_append_buffer(hctx->gw.wb, payload);
1270                     buffer_clear(payload);
1271                     break;
1272                   }
1273                 case MOD_WEBSOCKET_FRAME_TYPE_PING:
1274                     if (hctx->frame.ctl.siz == 0) {
1275                         unmask_payload(hctx);
1276                         mod_wstunnel_frame_send(hctx,
1277                           MOD_WEBSOCKET_FRAME_TYPE_PONG,
1278                           payload->ptr, buffer_string_length(payload));
1279                         buffer_clear(payload);
1280                     }
1281                     break;
1282                 case MOD_WEBSOCKET_FRAME_TYPE_PONG:
1283                     buffer_clear(payload);
1284                     break;
1285                 case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
1286                 default:
1287                     DEBUG_LOG_ERR("%s", "BUG: invalid frame type");
1288                     return -1;
1289                 }
1290                 break;
1291             default:
1292                 DEBUG_LOG_ERR("%s", "BUG: invalid state");
1293                 return -1;
1294             }
1295         }
1296     }
1297     /* XXX: should add ability to handle and preserve partial frames above */
1298     /*(not chunkqueue_reset(); do not reset cq->bytes_in, cq->bytes_out)*/
1299     chunkqueue_mark_written(cq, chunkqueue_length(cq));
1300     return 0;
1301 }
1302 
1303 #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
1304 
1305 
1306 int mod_wstunnel_frame_send(handler_ctx *hctx, mod_wstunnel_frame_type_t type,
1307                              const char *payload, size_t siz) {
1308   #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
1309     if (hctx->hybivers >= 8) return send_rfc_6455(hctx, type, payload, siz);
1310   #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
1311   #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
1312     if (0 == hctx->hybivers) return send_ietf_00(hctx, type, payload, siz);
1313   #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
1314     return -1;
1315 }
1316 
1317 int mod_wstunnel_frame_recv(handler_ctx *hctx) {
1318   #ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
1319     if (hctx->hybivers >= 8) return recv_rfc_6455(hctx);
1320   #endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */
1321   #ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
1322     if (0 == hctx->hybivers) return recv_ietf_00(hctx);
1323   #endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
1324     return -1;
1325 }
1326