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