lighttpd 1.4.x
https://www.lighttpd.net/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1385 lines
54 KiB
1385 lines
54 KiB
/* |
|
* mod_wstunnel originally based off https://github.com/nori0428/mod_websocket |
|
* Portions of this module Copyright(c) 2017, Glenn Strauss, All rights reserved |
|
* Portions of this module Copyright(c) 2010, Norio Kobota, All rights reserved. |
|
*/ |
|
|
|
/* |
|
* Copyright(c) 2010, Norio Kobota, All rights reserved. |
|
* |
|
* Redistribution and use in source and binary forms, with or without |
|
* modification, are permitted provided that the following conditions are met: |
|
* |
|
* - Redistributions of source code must retain the above copyright notice, |
|
* this list of conditions and the following disclaimer. |
|
* - Redistributions in binary form must reproduce the above copyright notice, |
|
* this list of conditions and the following disclaimer in the documentation |
|
* and/or other materials provided with the distribution. |
|
* - Neither the name of the 'incremental' nor the names of its contributors |
|
* may be used to endorse or promote products derived from this software |
|
* without specific prior written permission. |
|
* |
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
|
* THE POSSIBILITY OF SUCH DAMAGE. |
|
*/ |
|
|
|
/* NOTES: |
|
* |
|
* mod_wstunnel has been largely rewritten from Norio Kobota mod_websocket. |
|
* |
|
* highlighted differences from Norio Kobota mod_websocket |
|
* - re-coded to use lighttpd 1.4.46 buffer, chunkqueue, and gw_backend APIs |
|
* - websocket.server "ext" value is no longer regex; |
|
* operates similar to mod_proxy for either path prefix or extension match |
|
* - validation of "origins" value is no longer regex; operates as suffix match |
|
* (admin could use lighttpd.conf regex on "Origin" or "Sec-WebSocket-Origin" |
|
* and reject non-matches with mod_access if such regex validation required) |
|
* - websocket transparent proxy mode removed; functionality is now in mod_proxy |
|
* Backend server which responds to Connection: upgrade and Upgrade: websocket |
|
* should check "Origin" and/or "Sec-WebSocket-Origin". lighttpd.conf could |
|
* additionally be configured to check |
|
* $REQUEST_HEADER["Sec-WebSocket-Origin"] !~ "..." |
|
* with regex, and mod_access used to reject non-matches, if desired. |
|
* - connections to backend no longer block, but only first address returned |
|
* by getaddrinfo() is used; lighttpd does not cycle through all addresses |
|
* returned by DNS resolution. Note: DNS resolution occurs once at startup. |
|
* - directives renamed from websocket.* to wstunnel.* |
|
* - directive websocket.ping_interval replaced with wstunnel.ping-interval |
|
* (note the '_' changed to '-') |
|
* - directive websocket.timeout should be replaced with server.max-read-idle |
|
* - attribute "type" is an independent directive wstunnel.frame-type |
|
* (default is "text" unless "binary" is specified) |
|
* - attribute "origins" is an independent directive wstunnel.origins |
|
* - attribute "proto" removed; mod_proxy can proxy to backend websocket server |
|
* - attribute "subproto" should be replaced with mod_setenv directive |
|
* setenv.set-response-header = ( "Sec-WebSocket-Protocol" => "..." ) |
|
* if header is required |
|
* |
|
* not reviewed: |
|
* - websocket protocol compliance has not been reviewed |
|
* e.g. when to send 1000 Normal Closure and when to send 1001 Going Away |
|
* - websocket protocol sanity checking has not been reviewed |
|
* |
|
* References: |
|
* https://en.wikipedia.org/wiki/WebSocket |
|
* https://tools.ietf.org/html/rfc6455 |
|
* https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 |
|
*/ |
|
#include "first.h" |
|
|
|
#include <sys/types.h> |
|
#include <limits.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
|
|
#include "gw_backend.h" |
|
|
|
#include "base.h" |
|
#include "array.h" |
|
#include "buffer.h" |
|
#include "chunk.h" |
|
#include "fdevent.h" |
|
#include "joblist.h" |
|
#include "log.h" |
|
|
|
#define MOD_WEBSOCKET_LOG_NONE 0 |
|
#define MOD_WEBSOCKET_LOG_ERR 1 |
|
#define MOD_WEBSOCKET_LOG_WARN 2 |
|
#define MOD_WEBSOCKET_LOG_INFO 3 |
|
#define MOD_WEBSOCKET_LOG_DEBUG 4 |
|
|
|
#define DEBUG_LOG(level, format, ...) \ |
|
if (hctx->gw.conf.debug >= (level)) { \ |
|
log_error_write(hctx->srv, __FILE__, __LINE__, (format), __VA_ARGS__); \ |
|
} |
|
|
|
typedef struct { |
|
gw_plugin_config gw; |
|
buffer *frame_type; |
|
array *origins; |
|
unsigned int ping_interval; |
|
} plugin_config; |
|
|
|
typedef struct plugin_data { |
|
PLUGIN_DATA; |
|
plugin_config **config_storage; |
|
plugin_config conf; |
|
} plugin_data; |
|
|
|
typedef enum { |
|
MOD_WEBSOCKET_FRAME_STATE_INIT, |
|
|
|
/* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH, |
|
MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH, |
|
MOD_WEBSOCKET_FRAME_STATE_READ_MASK, |
|
/* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
|
|
MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD |
|
} mod_wstunnel_frame_state_t; |
|
|
|
typedef enum { |
|
MOD_WEBSOCKET_FRAME_TYPE_TEXT, |
|
MOD_WEBSOCKET_FRAME_TYPE_BIN, |
|
MOD_WEBSOCKET_FRAME_TYPE_CLOSE, |
|
|
|
/* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
MOD_WEBSOCKET_FRAME_TYPE_PING, |
|
MOD_WEBSOCKET_FRAME_TYPE_PONG |
|
/* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
|
|
} mod_wstunnel_frame_type_t; |
|
|
|
typedef struct { |
|
uint64_t siz; |
|
|
|
/* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
int siz_cnt; |
|
int mask_cnt; |
|
#define MOD_WEBSOCKET_MASK_CNT 4 |
|
unsigned char mask[MOD_WEBSOCKET_MASK_CNT]; |
|
/* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
|
|
} mod_wstunnel_frame_control_t; |
|
|
|
typedef struct { |
|
mod_wstunnel_frame_state_t state; |
|
mod_wstunnel_frame_control_t ctl; |
|
mod_wstunnel_frame_type_t type, type_before, type_backend; |
|
buffer *payload; |
|
} mod_wstunnel_frame_t; |
|
|
|
typedef struct { |
|
gw_handler_ctx gw; |
|
mod_wstunnel_frame_t frame; |
|
|
|
int hybivers; |
|
time_t ping_ts; |
|
int subproto; |
|
|
|
server *srv; /*(for mod_wstunnel module-specific DEBUG_LOG() macro)*/ |
|
plugin_config conf; |
|
} handler_ctx; |
|
|
|
/* prototypes */ |
|
static handler_t mod_wstunnel_handshake_create_response(handler_ctx *); |
|
static int mod_wstunnel_frame_send(handler_ctx *, mod_wstunnel_frame_type_t, const char *, size_t); |
|
static int mod_wstunnel_frame_recv(handler_ctx *); |
|
#define _MOD_WEBSOCKET_SPEC_IETF_00_ |
|
#define _MOD_WEBSOCKET_SPEC_RFC_6455_ |
|
|
|
INIT_FUNC(mod_wstunnel_init) { |
|
return calloc(1, sizeof(plugin_data)); |
|
} |
|
|
|
FREE_FUNC(mod_wstunnel_free) { |
|
plugin_data *p = p_d; |
|
if (p->config_storage) { |
|
for (size_t i = 0; i < srv->config_context->used; ++i) { |
|
plugin_config *s = p->config_storage[i]; |
|
if (NULL == s) continue; |
|
buffer_free(s->frame_type); |
|
array_free(s->origins); |
|
/*assert(0 == offsetof(s->gw));*/ |
|
gw_plugin_config_free(&s->gw); |
|
/*free(s);*//*free'd by gw_plugin_config_free()*/ |
|
} |
|
free(p->config_storage); |
|
} |
|
free(p); |
|
return HANDLER_GO_ON; |
|
} |
|
|
|
SETDEFAULTS_FUNC(mod_wstunnel_set_defaults) { |
|
plugin_data *p = p_d; |
|
data_unset *du; |
|
config_values_t cv[] = { |
|
{ "wstunnel.server", NULL, T_CONFIG_LOCAL, T_CONFIG_SCOPE_CONNECTION }, |
|
{ "wstunnel.debug", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, |
|
{ "wstunnel.balance", NULL, T_CONFIG_LOCAL, T_CONFIG_SCOPE_CONNECTION }, |
|
{ "wstunnel.map-extensions",NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, |
|
{ "wstunnel.frame-type", NULL, T_CONFIG_STRING,T_CONFIG_SCOPE_CONNECTION }, |
|
{ "wstunnel.origins", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, |
|
{ "wstunnel.ping-interval", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, |
|
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } |
|
}; |
|
|
|
p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *)); |
|
force_assert(p->config_storage); |
|
for (size_t i = 0; i < srv->config_context->used; ++i) { |
|
array *ca = ((data_config *)(srv->config_context->data[i]))->value; |
|
plugin_config *s = calloc(1, sizeof(plugin_config)); |
|
force_assert(s); |
|
|
|
s->gw.debug = 0; /* MOD_WEBSOCKET_LOG_NONE */ |
|
s->gw.ext_mapping = array_init(); |
|
s->frame_type = buffer_init(); |
|
s->origins = array_init(); |
|
s->ping_interval = 0; /* do not send ping */ |
|
|
|
cv[0].destination = NULL; /* T_CONFIG_LOCAL */ |
|
cv[1].destination = &(s->gw.debug); |
|
cv[2].destination = NULL; /* T_CONFIG_LOCAL */ |
|
cv[3].destination = s->gw.ext_mapping; |
|
cv[4].destination = s->frame_type; |
|
cv[5].destination = s->origins; |
|
cv[6].destination = &(s->ping_interval); |
|
|
|
p->config_storage[i] = s; |
|
|
|
if (0 != config_insert_values_global(srv, ca, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) { |
|
return HANDLER_ERROR; |
|
} |
|
|
|
du = array_get_element(ca, "wstunnel.server"); |
|
if (!gw_set_defaults_backend(srv, (gw_plugin_data *)p, du, i, 0)) { |
|
return HANDLER_ERROR; |
|
} |
|
|
|
du = array_get_element(ca, "wstunnel.balance"); |
|
if (!gw_set_defaults_balance(srv, &s->gw, du)) { |
|
return HANDLER_ERROR; |
|
} |
|
|
|
/* disable check-local for all exts (default enabled) */ |
|
if (s->gw.exts) { /*(check after gw_set_defaults_backend())*/ |
|
for (size_t j = 0; j < s->gw.exts->used; ++j) { |
|
gw_extension *ex = s->gw.exts->exts[j]; |
|
for (size_t n = 0; n < ex->used; ++n) { |
|
ex->hosts[n]->check_local = 0; |
|
} |
|
} |
|
} |
|
|
|
/* error if "mode" = "authorizer"; wstunnel can not act as authorizer */ |
|
/*(check after gw_set_defaults_backend())*/ |
|
if (s->gw.exts_auth && s->gw.exts_auth->used) { |
|
log_error_write(srv, __FILE__, __LINE__, "s", |
|
"wstunnel.server must not define any hosts " |
|
"with attribute \"mode\" = \"authorizer\""); |
|
return HANDLER_ERROR; |
|
} |
|
|
|
/*(default frame-type to "text" unless "binary" is specified)*/ |
|
if (!buffer_is_empty(s->frame_type) |
|
&& !buffer_is_equal_caseless_string(s->frame_type, |
|
CONST_STR_LEN("binary"))) { |
|
buffer_reset(s->frame_type); |
|
} |
|
|
|
if (!array_is_vlist(s->origins)) { |
|
log_error_write(srv, __FILE__, __LINE__, "s", |
|
"unexpected value for wstunnel.origins; expected wstunnel.origins = ( \"...\", \"...\" )"); |
|
return HANDLER_ERROR; |
|
} |
|
for (size_t j = 0; j < s->origins->used; ++j) { |
|
if (buffer_string_is_empty(((data_string *)s->origins->data[j])->value)) { |
|
log_error_write(srv, __FILE__, __LINE__, "s", |
|
"unexpected empty string in wstunnel.origins"); |
|
return HANDLER_ERROR; |
|
} |
|
} |
|
} |
|
|
|
/*assert(0 == offsetof(s->gw));*/ |
|
return HANDLER_GO_ON; |
|
} |
|
|
|
static handler_t wstunnel_create_env(server *srv, gw_handler_ctx *gwhctx) { |
|
handler_ctx *hctx = (handler_ctx *)gwhctx; |
|
connection *con = hctx->gw.remote_conn; |
|
handler_t rc; |
|
if (0 == con->request.content_length) { |
|
http_response_upgrade_read_body_unknown(srv, con); |
|
chunkqueue_append_chunkqueue(con->request_content_queue, |
|
con->read_queue); |
|
} |
|
rc = mod_wstunnel_handshake_create_response(hctx); |
|
if (rc != HANDLER_GO_ON) return rc; |
|
|
|
con->http_status = 101; /* Switching Protocols */ |
|
con->file_started = 1; |
|
|
|
hctx->ping_ts = srv->cur_ts; |
|
gw_set_transparent(srv, &hctx->gw); |
|
|
|
return HANDLER_GO_ON; |
|
} |
|
|
|
static handler_t wstunnel_stdin_append(server *srv, gw_handler_ctx *gwhctx) { |
|
/* prepare websocket frames to backend */ |
|
/* (caller should verify con->request_content_queue) */ |
|
/*assert(!chunkqueue_is_empty(con->request_content_queue));*/ |
|
handler_ctx *hctx = (handler_ctx *)gwhctx; |
|
if (0 == mod_wstunnel_frame_recv(hctx)) |
|
return HANDLER_GO_ON; |
|
else { |
|
/*(error)*/ |
|
/* future: might differentiate client close request from client error, |
|
* and then send 1000 or 1001 */ |
|
connection *con = hctx->gw.remote_conn; |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "sds", |
|
"disconnected from client ( fd =", con->fd, ")"); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sds", |
|
"send close response to client ( fd =", con->fd, ")"); |
|
mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, CONST_STR_LEN("1000")); /* 1000 Normal Closure */ |
|
gw_connection_reset(srv, con, hctx->gw.plugin_data); |
|
return HANDLER_FINISHED; |
|
} |
|
} |
|
|
|
static handler_t wstunnel_recv_parse(server *srv, connection *con, http_response_opts *opts, buffer *b, size_t n) { |
|
handler_ctx *hctx = (handler_ctx *)opts->pdata; |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx", |
|
"recv data from backend ( fd =", hctx->gw.fd, "), size =", n); |
|
if (0 == n) return HANDLER_FINISHED; |
|
if (mod_wstunnel_frame_send(hctx,hctx->frame.type_backend,b->ptr,n) < 0) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "fail to send data to client"); |
|
return HANDLER_ERROR; |
|
} |
|
buffer_string_set_length(b, 0); |
|
UNUSED(srv); |
|
UNUSED(con); |
|
return HANDLER_GO_ON; |
|
} |
|
|
|
#define PATCH(x) p->conf.x = s->x |
|
#define PATCH_GW(x) p->conf.gw.x = s->gw.x |
|
static void mod_wstunnel_patch_connection(server *srv, connection *con, plugin_data *p) { |
|
size_t i, j; |
|
plugin_config *s = p->config_storage[0]; |
|
|
|
PATCH_GW(exts); |
|
PATCH_GW(exts_auth); |
|
PATCH_GW(exts_resp); |
|
PATCH_GW(debug); |
|
PATCH_GW(balance); |
|
PATCH_GW(ext_mapping); |
|
PATCH(frame_type); |
|
PATCH(origins); |
|
PATCH(ping_interval); |
|
|
|
/* skip the first, the global context */ |
|
for (i = 1; i < srv->config_context->used; i++) { |
|
data_config *dc = (data_config *)srv->config_context->data[i]; |
|
s = p->config_storage[i]; |
|
|
|
/* condition didn't match */ |
|
if (!config_check_cond(srv, con, dc)) { |
|
continue; |
|
} |
|
/* merge config */ |
|
for (j = 0; j < dc->value->used; j++) { |
|
data_unset *du = dc->value->data[j]; |
|
|
|
if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.server"))) { |
|
PATCH_GW(exts); |
|
/*(wstunnel can not act as authorizer, |
|
* but p->conf.exts_auth must not be NULL)*/ |
|
PATCH_GW(exts_auth); |
|
PATCH_GW(exts_resp); |
|
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.debug"))) { |
|
PATCH_GW(debug); |
|
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.balance"))) { |
|
PATCH_GW(balance); |
|
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.map-extensions"))) { |
|
PATCH_GW(ext_mapping); |
|
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.frame-type"))) { |
|
PATCH(frame_type); |
|
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.origins"))) { |
|
PATCH(origins); |
|
} else if (buffer_is_equal_string(du->key, CONST_STR_LEN("wstunnel.ping-interval"))) { |
|
PATCH(ping_interval); |
|
} |
|
} |
|
} |
|
} |
|
#undef PATCH_GW |
|
#undef PATCH |
|
|
|
static int header_contains_token (buffer *b, const char *m, size_t mlen) |
|
{ |
|
for (char *s = b->ptr; s; s = strchr(s, ',')) { |
|
while (*s == ' ' || *s == '\t' || *s == ',') ++s; |
|
if (0 == strncasecmp(s, m, mlen)) { |
|
s += mlen; |
|
if (*s == '\0' || *s == ' ' || *s == '\t' || *s == ',' || *s == ';') |
|
return 1; |
|
} |
|
} |
|
return 0; |
|
} |
|
|
|
static int wstunnel_is_allowed_origin(connection *con, handler_ctx *hctx) { |
|
/* If allowed origins is set (and not empty list), fail closed if no match. |
|
* Note that origin provided in request header has not been normalized, so |
|
* change in case or other non-normal forms might not match allowed list */ |
|
const array * const allowed_origins = hctx->conf.origins; |
|
buffer *origin = NULL; |
|
size_t olen; |
|
data_string *dsorigin; |
|
|
|
if (0 == allowed_origins->used) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "s", "allowed origins not specified"); |
|
return 1; |
|
} |
|
|
|
/* "Origin" header is preferred |
|
* ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */ |
|
dsorigin = (data_string *)array_get_element(con->request.headers, "Origin"); |
|
if (NULL == dsorigin) { |
|
dsorigin = (data_string *) |
|
array_get_element(con->request.headers, "Sec-WebSocket-Origin"); |
|
} |
|
olen = buffer_string_length(dsorigin ? (origin = dsorigin->value) : NULL); |
|
if (0 == olen) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Origin header is invalid"); |
|
con->http_status = 400; /* Bad Request */ |
|
return 0; |
|
} |
|
|
|
for (size_t i = 0; i < allowed_origins->used; ++i) { |
|
buffer *b = ((data_string *)allowed_origins->data[i])->value; |
|
size_t blen = buffer_string_length(b); |
|
if ((olen > blen ? origin->ptr[olen-blen-1] == '.' : olen == blen) |
|
&& buffer_is_equal_right_len(origin, b, blen)) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "bsb", |
|
origin, "matches allowed origin:", b); |
|
return 1; |
|
} |
|
} |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "bs", |
|
origin, "does not match any allowed origins"); |
|
con->http_status = 403; /* Forbidden */ |
|
return 0; |
|
} |
|
|
|
static int wstunnel_check_request(connection *con, handler_ctx *hctx) { |
|
const data_string * const vers = (data_string *) |
|
array_get_element(con->request.headers, "Sec-WebSocket-Version"); |
|
const long hybivers = (NULL != vers && !buffer_is_empty(vers->value)) |
|
? strtol(vers->value->ptr, NULL, 10) |
|
: 0; |
|
if (hybivers < 0 || hybivers > INT_MAX) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "invalid Sec-WebSocket-Version"); |
|
con->http_status = 400; /* Bad Request */ |
|
return -1; |
|
} |
|
|
|
/*(redundant since HTTP/1.1 required in mod_wstunnel_check_extension())*/ |
|
if (buffer_is_empty(con->request.http_host)) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Host header does not exist"); |
|
con->http_status = 400; /* Bad Request */ |
|
return -1; |
|
} |
|
|
|
if (!wstunnel_is_allowed_origin(con, hctx)) { |
|
return -1; |
|
} |
|
|
|
return (int)hybivers; |
|
} |
|
|
|
static void wstunnel_backend_error(gw_handler_ctx *gwhctx) { |
|
handler_ctx *hctx = (handler_ctx *)gwhctx; |
|
if (hctx->gw.state == GW_STATE_WRITE || hctx->gw.state == GW_STATE_READ) { |
|
mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, CONST_STR_LEN("1001")); /* 1001 Going Away */ |
|
} |
|
} |
|
|
|
static void wstunnel_handler_ctx_free(void *gwhctx) { |
|
handler_ctx *hctx = (handler_ctx *)gwhctx; |
|
buffer_free(hctx->frame.payload); |
|
} |
|
|
|
static handler_t wstunnel_handler_setup (server *srv, connection *con, plugin_data *p) { |
|
handler_ctx *hctx = con->plugin_ctx[p->id]; |
|
int binary; |
|
int hybivers; |
|
hctx->srv = srv; /*(for mod_wstunnel module-specific DEBUG_LOG() macro)*/ |
|
hctx->conf.frame_type = p->conf.frame_type; |
|
hctx->conf.origins = p->conf.origins; |
|
hctx->conf.ping_interval = p->conf.ping_interval; |
|
hybivers = wstunnel_check_request(con, hctx); |
|
if (hybivers < 0) return HANDLER_FINISHED; |
|
hctx->hybivers = hybivers; |
|
if (0 == hybivers) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO,"s","WebSocket Version = hybi-00"); |
|
} |
|
else { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO,"sd","WebSocket Version =",hybivers); |
|
} |
|
|
|
hctx->gw.opts.backend = BACKEND_PROXY; /*(act proxy-like; not used)*/ |
|
hctx->gw.opts.pdata = hctx; |
|
hctx->gw.opts.parse = wstunnel_recv_parse; |
|
hctx->gw.stdin_append = wstunnel_stdin_append; |
|
hctx->gw.create_env = wstunnel_create_env; |
|
hctx->gw.handler_ctx_free = wstunnel_handler_ctx_free; |
|
hctx->gw.backend_error = wstunnel_backend_error; |
|
hctx->gw.response = buffer_init(); |
|
|
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT; |
|
hctx->frame.ctl.siz = 0; |
|
hctx->frame.payload = buffer_init(); |
|
|
|
binary = !buffer_is_empty(hctx->conf.frame_type); /*("binary")*/ |
|
if (!binary) { |
|
data_string *ds = (data_string *) |
|
array_get_element(con->request.headers, "Sec-WebSocket-Protocol"); |
|
if (NULL != ds) { |
|
for (const char *s = ds->value->ptr; *s; ++s) { |
|
while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s; |
|
if (0 == strncasecmp(s, "binary", sizeof("binary")-1)) { |
|
s += sizeof("binary")-1; |
|
while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s; |
|
if (*s==','||*s=='\0') { |
|
hctx->subproto = 1; |
|
binary = 1; |
|
break; |
|
} |
|
} |
|
else if (0 == strncasecmp(s, "base64", sizeof("base64")-1)) { |
|
s += sizeof("base64")-1; |
|
while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s; |
|
if (*s==','||*s=='\0') { |
|
hctx->subproto = -1; |
|
break; |
|
} |
|
} |
|
s = strchr(s, ','); |
|
if (NULL == s) break; |
|
} |
|
} |
|
} |
|
|
|
if (binary) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "s", |
|
"will recv binary data from backend"); |
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_BIN; |
|
hctx->frame.type_before = MOD_WEBSOCKET_FRAME_TYPE_BIN; |
|
hctx->frame.type_backend = MOD_WEBSOCKET_FRAME_TYPE_BIN; |
|
} |
|
else { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "s", |
|
"will recv text data from backend"); |
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_TEXT; |
|
hctx->frame.type_before = MOD_WEBSOCKET_FRAME_TYPE_TEXT; |
|
hctx->frame.type_backend = MOD_WEBSOCKET_FRAME_TYPE_TEXT; |
|
} |
|
|
|
return HANDLER_GO_ON; |
|
} |
|
|
|
static handler_t mod_wstunnel_check_extension(server *srv, connection *con, void *p_d) { |
|
plugin_data *p = p_d; |
|
array *hdrs = con->request.headers; |
|
data_string *dsconnection, *dsupgrade; |
|
handler_t rc; |
|
|
|
if (con->mode != DIRECT) |
|
return HANDLER_GO_ON; |
|
if (con->request.http_method != HTTP_METHOD_GET) |
|
return HANDLER_GO_ON; |
|
if (con->request.http_version != HTTP_VERSION_1_1) |
|
return HANDLER_GO_ON; |
|
|
|
/* |
|
* Connection: upgrade, keep-alive, ... |
|
* Upgrade: WebSocket, ... |
|
*/ |
|
dsupgrade = (data_string *)array_get_element(hdrs, "Upgrade"); |
|
if (NULL == dsupgrade |
|
|| !header_contains_token(dsupgrade->value, CONST_STR_LEN("websocket"))) |
|
return HANDLER_GO_ON; |
|
dsconnection = (data_string *)array_get_element(hdrs, "Connection"); |
|
if (NULL == dsconnection |
|
|| !header_contains_token(dsconnection->value,CONST_STR_LEN("upgrade"))) |
|
return HANDLER_GO_ON; |
|
|
|
mod_wstunnel_patch_connection(srv, con, p); |
|
if (NULL == p->conf.gw.exts) return HANDLER_GO_ON; |
|
|
|
rc = gw_check_extension(srv,con,(gw_plugin_data *)p,1,sizeof(handler_ctx)); |
|
return (HANDLER_GO_ON == rc && con->mode == p->id) |
|
? wstunnel_handler_setup(srv, con, p) |
|
: rc; |
|
} |
|
|
|
TRIGGER_FUNC(mod_wstunnel_handle_trigger) { |
|
const plugin_data * const p = p_d; |
|
const time_t cur_ts = srv->cur_ts + 1; |
|
|
|
gw_handle_trigger(srv, p_d); |
|
|
|
for (size_t i = 0; i < srv->conns->used; ++i) { |
|
connection *con = srv->conns->ptr[i]; |
|
handler_ctx *hctx = con->plugin_ctx[p->id]; |
|
if (NULL == hctx || con->mode != p->id) |
|
continue; |
|
|
|
if (hctx->gw.state != GW_STATE_WRITE && hctx->gw.state != GW_STATE_READ) |
|
continue; |
|
|
|
if (cur_ts - con->read_idle_ts > con->conf.max_read_idle) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_INFO, "sds", |
|
"timeout client ( fd =", con->fd, ")"); |
|
mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, NULL, 0); |
|
gw_connection_reset(srv, con, p_d); |
|
joblist_append(srv, con); |
|
/* avoid server.c closing connection with error due to max_read_idle |
|
* (might instead run joblist after plugins_call_handle_trigger())*/ |
|
con->read_idle_ts = cur_ts; |
|
continue; |
|
} |
|
|
|
if (0 != hctx->hybivers |
|
&& hctx->conf.ping_interval > 0 |
|
&& (time_t)hctx->conf.ping_interval + hctx->ping_ts < cur_ts) { |
|
hctx->ping_ts = cur_ts; |
|
mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_PING, CONST_STR_LEN("ping")); |
|
joblist_append(srv, con); |
|
continue; |
|
} |
|
} |
|
|
|
return HANDLER_GO_ON; |
|
} |
|
|
|
int mod_wstunnel_plugin_init(plugin *p); |
|
int mod_wstunnel_plugin_init(plugin *p) { |
|
p->version = LIGHTTPD_VERSION_ID; |
|
p->name = buffer_init_string("wstunnel"); |
|
p->init = mod_wstunnel_init; |
|
p->cleanup = mod_wstunnel_free; |
|
p->set_defaults = mod_wstunnel_set_defaults; |
|
p->connection_reset = gw_connection_reset; |
|
p->handle_uri_clean = mod_wstunnel_check_extension; |
|
p->handle_subrequest = gw_handle_subrequest; |
|
p->handle_trigger = mod_wstunnel_handle_trigger; |
|
p->handle_waitpid = gw_handle_waitpid_cb; |
|
p->data = NULL; |
|
return 0; |
|
} |
|
|
|
|
|
|
|
|
|
/* |
|
* modified from Norio Kobota mod_websocket_handshake.c |
|
*/ |
|
|
|
#ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ |
|
|
|
#include "sys-endian.h" /* lighttpd */ |
|
#include "md5.h" /* lighttpd */ |
|
|
|
static int get_key3(connection *con, char *buf) { |
|
/* 8 bytes should have been sent with request |
|
* for draft-ietf-hybi-thewebsocketprotocol-00 */ |
|
chunkqueue *cq = con->request_content_queue; |
|
size_t bytes = 8; |
|
/*(caller should ensure bytes available prior to calling this routine)*/ |
|
/*assert(chunkqueue_length(cq) >= 8);*/ |
|
for (chunk *c = cq->first; NULL != c; c = c->next) { |
|
/*(chunk_remaining_length() on MEM_CHUNK)*/ |
|
size_t n = (size_t)(buffer_string_length(c->mem) - c->offset); |
|
/*(expecting 8 bytes to be in memory directly after headers)*/ |
|
if (c->type != MEM_CHUNK) break; /* FILE_CHUNK not handled here */ |
|
if (n > bytes) n = bytes; |
|
memcpy(buf, c->mem->ptr+c->offset, n); |
|
buf += n; |
|
if (0 == (bytes -= n)) break; |
|
} |
|
if (0 != bytes) return -1; |
|
chunkqueue_mark_written(cq, 8); |
|
return 0; |
|
} |
|
|
|
static int get_key_number(uint32_t *ret, const buffer *b) { |
|
const char * const s = b->ptr; |
|
size_t j = 0; |
|
unsigned long n; |
|
uint32_t sp = 0; |
|
char tmp[10 + 1]; /* #define UINT32_MAX_STRLEN 10 */ |
|
|
|
for (size_t i = 0, used = b->used; i < used; ++i) { |
|
if (light_isdigit(s[i])) { |
|
tmp[j] = s[i]; |
|
if (++j >= sizeof(tmp)) return -1; |
|
} |
|
else if (s[i] == ' ') ++sp; /* count num spaces */ |
|
} |
|
tmp[j] = '\0'; |
|
n = strtoul(tmp, NULL, 10); |
|
if (n > UINT32_MAX || 0 == sp) return -1; |
|
*ret = (uint32_t)n / sp; |
|
return 0; |
|
} |
|
|
|
static int create_MD5_sum(connection *con) { |
|
uint32_t buf[4]; /* MD5 binary hash len */ |
|
data_string *dskey1, *dskey2; |
|
li_MD5_CTX ctx; |
|
|
|
dskey1 = (data_string *) |
|
array_get_element(con->request.headers, "Sec-WebSocket-Key1"); |
|
dskey2 = (data_string *) |
|
array_get_element(con->request.headers, "Sec-WebSocket-Key2"); |
|
|
|
if (NULL == dskey1 || get_key_number(buf+0, dskey1->value) < 0 || |
|
NULL == dskey2 || get_key_number(buf+1, dskey2->value) < 0 || |
|
get_key3(con, (char *)(buf+2)) < 0) { |
|
return -1; |
|
} |
|
#ifdef __BIG_ENDIAN__ |
|
#define ws_htole32(s,u)\ |
|
(s)[0]=((u)>>24); \ |
|
(s)[1]=((u)>>16); \ |
|
(s)[2]=((u)>>8); \ |
|
(s)[3]=((u)) |
|
ws_htole32((unsigned char *)(buf+0), buf[0]); |
|
ws_htole32((unsigned char *)(buf+1), buf[1]); |
|
#endif |
|
li_MD5_Init(&ctx); |
|
li_MD5_Update(&ctx, buf, sizeof(buf)); |
|
li_MD5_Final((unsigned char *)buf, &ctx); /*(overwrite buf[] with result)*/ |
|
chunkqueue_append_mem(con->write_queue, (char *)buf, sizeof(buf)); |
|
return 0; |
|
} |
|
|
|
static int create_response_ietf_00(handler_ctx *hctx) { |
|
connection *con = hctx->gw.remote_conn; |
|
buffer *value = hctx->srv->tmp_buf; |
|
|
|
/* "Origin" header is preferred |
|
* ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */ |
|
data_string *origin = (data_string *) |
|
array_get_element(con->request.headers, "Origin"); |
|
if (NULL == origin) { |
|
origin = (data_string *) |
|
array_get_element(con->request.headers, "Sec-WebSocket-Origin"); |
|
} |
|
if (NULL == origin || buffer_is_empty(origin->value)) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Origin header is invalid"); |
|
return -1; |
|
} |
|
if (buffer_is_empty(con->request.http_host)) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Host header does not exist"); |
|
return -1; |
|
} |
|
|
|
/* calc MD5 sum from keys */ |
|
if (create_MD5_sum(con) < 0) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Sec-WebSocket-Key is invalid"); |
|
return -1; |
|
} |
|
|
|
con->parsed_response |= HTTP_UPGRADE; |
|
response_header_overwrite(hctx->srv, con, |
|
CONST_STR_LEN("Upgrade"), |
|
CONST_STR_LEN("websocket")); |
|
#if 0 /*(added later in http_response_write_header())*/ |
|
response_header_append(hctx->srv, con, |
|
CONST_STR_LEN("Connection"), |
|
CONST_STR_LEN("upgrade")); |
|
#endif |
|
#if 0 /*(Sec-WebSocket-Origin header is not required for hybi-00)*/ |
|
/* Note: it is insecure to simply reflect back origin provided by client |
|
* (if admin did not configure restricted list of valid origins) |
|
* (see wstunnel_check_request()) */ |
|
response_header_overwrite(hctx->srv, con, |
|
CONST_STR_LEN("Sec-WebSocket-Origin"), |
|
CONST_BUF_LEN(origin->value)); |
|
#endif |
|
|
|
if (buffer_is_equal_string(con->uri.scheme, CONST_STR_LEN("https"))) |
|
buffer_copy_string_len(value, CONST_STR_LEN("wss://")); |
|
else |
|
buffer_copy_string_len(value, CONST_STR_LEN("ws://")); |
|
buffer_append_string_buffer(value, con->request.http_host); |
|
buffer_append_string_buffer(value, con->uri.path); |
|
response_header_insert(hctx->srv, con, CONST_STR_LEN("Sec-WebSocket-Location"), CONST_BUF_LEN(value)); |
|
|
|
return 0; |
|
} |
|
|
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */ |
|
|
|
|
|
#ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_ |
|
|
|
#include "algo_sha1.h" /* lighttpd */ |
|
#include "base64.h" /* lighttpd */ |
|
|
|
static int create_response_rfc_6455(handler_ctx *hctx) { |
|
connection *con = hctx->gw.remote_conn; |
|
buffer *value = hctx->srv->tmp_buf; |
|
array * const hdrs = con->response.headers; |
|
data_string *ds; |
|
SHA_CTX sha; |
|
unsigned char sha_digest[SHA_DIGEST_LENGTH]; |
|
|
|
ds = (data_string *) |
|
array_get_element(con->request.headers, "Sec-WebSocket-Key"); |
|
if (NULL == ds || buffer_is_empty(ds->value)) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "Sec-WebSocket-Key is invalid"); |
|
return -1; |
|
} |
|
|
|
/* get SHA1 hash of key */ |
|
/* refer: RFC-6455 Sec.1.3 Opening Handshake */ |
|
SHA1_Init(&sha); |
|
SHA1_Update(&sha, (const unsigned char *)CONST_BUF_LEN(ds->value)); |
|
SHA1_Update(&sha, (const unsigned char *)CONST_STR_LEN("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); |
|
SHA1_Final(sha_digest, &sha); |
|
|
|
con->parsed_response |= HTTP_UPGRADE; |
|
response_header_overwrite(hctx->srv, con, |
|
CONST_STR_LEN("Upgrade"), |
|
CONST_STR_LEN("websocket")); |
|
#if 0 /*(added later in http_response_write_header())*/ |
|
response_header_append(hctx->srv, con, |
|
CONST_STR_LEN("Connection"), |
|
CONST_STR_LEN("upgrade")); |
|
#endif |
|
|
|
buffer_string_set_length(value, 0); |
|
buffer_append_base64_encode(value, sha_digest, SHA_DIGEST_LENGTH, BASE64_STANDARD); |
|
response_header_insert(hctx->srv, con, CONST_STR_LEN("Sec-WebSocket-Accept"), CONST_BUF_LEN(value)); |
|
|
|
if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_BIN) |
|
array_set_key_value(hdrs, CONST_STR_LEN("Sec-WebSocket-Protocol"), |
|
CONST_STR_LEN("binary")); |
|
else if (-1 == hctx->subproto) |
|
array_set_key_value(hdrs, CONST_STR_LEN("Sec-WebSocket-Protocol"), |
|
CONST_STR_LEN("base64")); |
|
|
|
return 0; |
|
} |
|
|
|
#endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
|
|
|
|
handler_t mod_wstunnel_handshake_create_response(handler_ctx *hctx) { |
|
connection *con = hctx->gw.remote_conn; |
|
#ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_ |
|
if (hctx->hybivers >= 8) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "send handshake response"); |
|
if (0 != create_response_rfc_6455(hctx)) { |
|
con->http_status = 400; /* Bad Request */ |
|
return HANDLER_ERROR; |
|
} |
|
return HANDLER_GO_ON; |
|
} |
|
#endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
|
|
#ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ |
|
if (hctx->hybivers == 0) { |
|
#ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ |
|
/* 8 bytes should have been sent with request |
|
* for draft-ietf-hybi-thewebsocketprotocol-00 */ |
|
chunkqueue *cq = con->request_content_queue; |
|
if (0 == hctx->hybivers && chunkqueue_length(cq) < 8) |
|
return HANDLER_WAIT_FOR_EVENT; |
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */ |
|
|
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "send handshake response"); |
|
if (0 != create_response_ietf_00(hctx)) { |
|
con->http_status = 400; /* Bad Request */ |
|
return HANDLER_ERROR; |
|
} |
|
return HANDLER_GO_ON; |
|
} |
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */ |
|
|
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "not supported WebSocket Version"); |
|
con->http_status = 503; /* Service Unavailable */ |
|
return HANDLER_ERROR; |
|
} |
|
|
|
|
|
|
|
|
|
/* |
|
* modified from Norio Kobota mod_websocket_frame.c |
|
*/ |
|
|
|
#include "base64.h" /* lighttpd */ |
|
#include "http_chunk.h" /* lighttpd */ |
|
|
|
#define MOD_WEBSOCKET_BUFMAX (0x0fffff) |
|
|
|
#ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ |
|
|
|
#include <stdlib.h> |
|
static int send_ietf_00(handler_ctx *hctx, mod_wstunnel_frame_type_t type, const char *payload, size_t siz) { |
|
static const char head = 0; /* 0x00 */ |
|
static const char tail = ~0; /* 0xff */ |
|
server *srv = hctx->srv; |
|
connection *con = hctx->gw.remote_conn; |
|
char *mem; |
|
size_t len; |
|
|
|
switch (type) { |
|
case MOD_WEBSOCKET_FRAME_TYPE_TEXT: |
|
if (0 == siz) return 0; |
|
http_chunk_append_mem(srv, con, &head, 1); |
|
http_chunk_append_mem(srv, con, payload, siz); |
|
http_chunk_append_mem(srv, con, &tail, 1); |
|
len = siz+2; |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_BIN: |
|
if (0 == siz) return 0; |
|
http_chunk_append_mem(srv, con, &head, 1); |
|
len = 4*(siz/3)+4+1; |
|
/* avoid accumulating too much data in memory; send to tmpfile */ |
|
#if 0 |
|
chunkqueue_get_memory(con->write_queue, &mem, &len, len, 0); |
|
len=li_to_base64(mem,len,(unsigned char *)payload,siz,BASE64_STANDARD); |
|
chunkqueue_use_memory(con->write_queue, len); |
|
#else |
|
mem = malloc(len); |
|
force_assert(mem); |
|
len=li_to_base64(mem,len,(unsigned char *)payload,siz,BASE64_STANDARD); |
|
http_chunk_append_mem(srv, con, mem, len); |
|
free(mem); |
|
#endif |
|
http_chunk_append_mem(srv, con, &tail, 1); |
|
len += 2; |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_CLOSE: |
|
http_chunk_append_mem(srv, con, &tail, 1); |
|
http_chunk_append_mem(srv, con, &head, 1); |
|
len = 2; |
|
break; |
|
default: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "invalid frame type"); |
|
return -1; |
|
} |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx", |
|
"send data to client ( fd =", con->fd, "), frame size =", len); |
|
return 0; |
|
} |
|
|
|
static int recv_ietf_00(handler_ctx *hctx) { |
|
connection *con = hctx->gw.remote_conn; |
|
chunkqueue *cq = con->request_content_queue; |
|
buffer *payload = hctx->frame.payload; |
|
char *mem; |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx", |
|
"recv data from client ( fd =", con->fd, |
|
"), size =", chunkqueue_length(cq)); |
|
for (chunk *c = cq->first; c; c = c->next) { |
|
char *frame = c->mem->ptr+c->offset; |
|
/*(chunk_remaining_length() on MEM_CHUNK)*/ |
|
size_t flen = (size_t)(buffer_string_length(c->mem) - c->offset); |
|
/*(FILE_CHUNK not handled, but might need to add support)*/ |
|
force_assert(c->type == MEM_CHUNK); |
|
for (size_t i = 0; i < flen; ) { |
|
switch (hctx->frame.state) { |
|
case MOD_WEBSOCKET_FRAME_STATE_INIT: |
|
hctx->frame.ctl.siz = 0; |
|
if (frame[i] == 0x00) { |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD; |
|
i++; |
|
} |
|
else if (((unsigned char *)frame)[i] == 0xff) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG,"s","recv close frame"); |
|
return -1; |
|
} |
|
else { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG,"s","recv invalid frame"); |
|
return -1; |
|
} |
|
break; |
|
case MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD: |
|
mem = (char *)memchr(frame+i, 0xff, flen - i); |
|
if (mem == NULL) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"got continuous payload, size =", flen - i); |
|
hctx->frame.ctl.siz += flen - i; |
|
if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_WARN, "sx", |
|
"frame size has been exceeded:", |
|
MOD_WEBSOCKET_BUFMAX); |
|
return -1; |
|
} |
|
buffer_append_string_len(payload, frame+i, flen - i); |
|
i += flen - i; |
|
} |
|
else { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"got final payload, size =", (mem - frame+i)); |
|
hctx->frame.ctl.siz += (mem - frame+i); |
|
if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_WARN, "sx", |
|
"frame size has been exceeded:", |
|
MOD_WEBSOCKET_BUFMAX); |
|
return -1; |
|
} |
|
buffer_append_string_len(payload, frame+i, mem - frame+i); |
|
i += (mem - frame+i); |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT; |
|
} |
|
i++; |
|
if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_TEXT |
|
&& !buffer_is_empty(payload)) { |
|
size_t len; |
|
hctx->frame.ctl.siz = 0; |
|
len = buffer_string_length(payload); |
|
chunkqueue_get_memory(hctx->gw.wb, &mem, &len, len, 0); |
|
len = buffer_string_length(payload); |
|
memcpy(mem, payload->ptr, len); |
|
chunkqueue_use_memory(hctx->gw.wb, len); |
|
buffer_reset(payload); |
|
} |
|
else { |
|
if (hctx->frame.state == MOD_WEBSOCKET_FRAME_STATE_INIT |
|
&& !buffer_is_empty(payload)) { |
|
buffer *b; |
|
size_t len = buffer_string_length(payload); |
|
len = (len+3)/4*3+1; |
|
chunkqueue_get_memory(hctx->gw.wb, &mem, &len, len, 0); |
|
b = hctx->gw.wb->last->mem; |
|
len = buffer_string_length(b); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "ss", |
|
"try to base64 decode:", payload->ptr); |
|
if (NULL == buffer_append_base64_decode(b, CONST_BUF_LEN(payload), BASE64_STANDARD)) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", |
|
"fail to base64-decode"); |
|
return -1; |
|
} |
|
buffer_reset(payload); |
|
/*chunkqueue_use_memory()*/ |
|
hctx->gw.wb->bytes_in += buffer_string_length(b)-len; |
|
} |
|
} |
|
break; |
|
default: /* never reach */ |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR,"s", "BUG: unknown state"); |
|
return -1; |
|
} |
|
} |
|
} |
|
/* XXX: should add ability to handle and preserve partial frames above */ |
|
/*(not chunkqueue_reset(); do not reset cq->bytes_in, cq->bytes_out)*/ |
|
chunkqueue_mark_written(cq, chunkqueue_length(cq)); |
|
return 0; |
|
} |
|
|
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */ |
|
|
|
|
|
#ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_ |
|
|
|
#define MOD_WEBSOCKET_OPCODE_CONT 0x00 |
|
#define MOD_WEBSOCKET_OPCODE_TEXT 0x01 |
|
#define MOD_WEBSOCKET_OPCODE_BIN 0x02 |
|
#define MOD_WEBSOCKET_OPCODE_CLOSE 0x08 |
|
#define MOD_WEBSOCKET_OPCODE_PING 0x09 |
|
#define MOD_WEBSOCKET_OPCODE_PONG 0x0A |
|
|
|
#define MOD_WEBSOCKET_FRAME_LEN16 0x7E |
|
#define MOD_WEBSOCKET_FRAME_LEN63 0x7F |
|
#define MOD_WEBSOCKET_FRAME_LEN16_CNT 2 |
|
#define MOD_WEBSOCKET_FRAME_LEN63_CNT 8 |
|
|
|
static int send_rfc_6455(handler_ctx *hctx, mod_wstunnel_frame_type_t type, const char *payload, size_t siz) { |
|
server *srv = hctx->srv; |
|
connection *con = hctx->gw.remote_conn; |
|
char mem[10]; |
|
size_t len; |
|
|
|
/* allowed null payload for ping, pong, close frame */ |
|
if (payload == NULL && ( type == MOD_WEBSOCKET_FRAME_TYPE_TEXT |
|
|| type == MOD_WEBSOCKET_FRAME_TYPE_BIN )) { |
|
return -1; |
|
} |
|
|
|
switch (type) { |
|
case MOD_WEBSOCKET_FRAME_TYPE_TEXT: |
|
mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_TEXT); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = text"); |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_BIN: |
|
mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_BIN); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = binary"); |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_PING: |
|
mem[0] = (char) (0x80 | MOD_WEBSOCKET_OPCODE_PING); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = ping"); |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_PONG: |
|
mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_PONG); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = pong"); |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_CLOSE: |
|
default: |
|
mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_CLOSE); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = close"); |
|
break; |
|
} |
|
|
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", "payload size =", siz); |
|
if (siz < MOD_WEBSOCKET_FRAME_LEN16) { |
|
mem[1] = siz; |
|
len = 2; |
|
} |
|
else if (siz <= UINT16_MAX) { |
|
mem[1] = MOD_WEBSOCKET_FRAME_LEN16; |
|
mem[2] = (siz >> 8) & 0xff; |
|
mem[3] = siz & 0xff; |
|
len = 1+MOD_WEBSOCKET_FRAME_LEN16_CNT+1; |
|
} |
|
else { |
|
mem[1] = MOD_WEBSOCKET_FRAME_LEN63; |
|
mem[2] = 0; |
|
mem[3] = 0; |
|
mem[4] = 0; |
|
mem[5] = 0; |
|
mem[6] = (siz >> 24) & 0xff; |
|
mem[7] = (siz >> 16) & 0xff; |
|
mem[8] = (siz >> 8) & 0xff; |
|
mem[9] = siz & 0xff; |
|
len = 1+MOD_WEBSOCKET_FRAME_LEN63_CNT+1; |
|
} |
|
http_chunk_append_mem(srv, con, mem, len); |
|
if (siz) http_chunk_append_mem(srv, con, payload, siz); |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx", |
|
"send data to client ( fd =",con->fd,"), frame size =",len+siz); |
|
return 0; |
|
} |
|
|
|
static void unmask_payload(handler_ctx *hctx) { |
|
buffer * const b = hctx->frame.payload; |
|
for (size_t i = 0, used = buffer_string_length(b); i < used; ++i) { |
|
b->ptr[i] ^= hctx->frame.ctl.mask[hctx->frame.ctl.mask_cnt]; |
|
hctx->frame.ctl.mask_cnt = (hctx->frame.ctl.mask_cnt + 1) % 4; |
|
} |
|
} |
|
|
|
static int recv_rfc_6455(handler_ctx *hctx) { |
|
connection *con = hctx->gw.remote_conn; |
|
chunkqueue *cq = con->request_content_queue; |
|
buffer *payload = hctx->frame.payload; |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx", |
|
"recv data from client ( fd =", con->fd, |
|
"), size =", chunkqueue_length(cq)); |
|
for (chunk *c = cq->first; c; c = c->next) { |
|
char *frame = c->mem->ptr+c->offset; |
|
/*(chunk_remaining_length() on MEM_CHUNK)*/ |
|
size_t flen = (size_t)(buffer_string_length(c->mem) - c->offset); |
|
/*(FILE_CHUNK not handled, but might need to add support)*/ |
|
force_assert(c->type == MEM_CHUNK); |
|
for (size_t i = 0; i < flen; ) { |
|
switch (hctx->frame.state) { |
|
case MOD_WEBSOCKET_FRAME_STATE_INIT: |
|
switch (frame[i] & 0x0f) { |
|
case MOD_WEBSOCKET_OPCODE_CONT: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = continue"); |
|
hctx->frame.type = hctx->frame.type_before; |
|
break; |
|
case MOD_WEBSOCKET_OPCODE_TEXT: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = text"); |
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_TEXT; |
|
hctx->frame.type_before = hctx->frame.type; |
|
break; |
|
case MOD_WEBSOCKET_OPCODE_BIN: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = binary"); |
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_BIN; |
|
hctx->frame.type_before = hctx->frame.type; |
|
break; |
|
case MOD_WEBSOCKET_OPCODE_PING: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = ping"); |
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PING; |
|
break; |
|
case MOD_WEBSOCKET_OPCODE_PONG: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = pong"); |
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PONG; |
|
break; |
|
case MOD_WEBSOCKET_OPCODE_CLOSE: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "s", "type = close"); |
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_CLOSE; |
|
return -1; |
|
break; |
|
default: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "type is invalid"); |
|
return -1; |
|
break; |
|
} |
|
i++; |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH; |
|
break; |
|
case MOD_WEBSOCKET_FRAME_STATE_READ_LENGTH: |
|
if ((frame[i] & 0x80) != 0x80) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", |
|
"payload was not masked"); |
|
return -1; |
|
} |
|
hctx->frame.ctl.mask_cnt = 0; |
|
hctx->frame.ctl.siz = (uint64_t)(frame[i] & 0x7f); |
|
if (hctx->frame.ctl.siz == 0) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"specified payload size =", hctx->frame.ctl.siz); |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK; |
|
} |
|
else if (hctx->frame.ctl.siz == MOD_WEBSOCKET_FRAME_LEN16) { |
|
hctx->frame.ctl.siz = 0; |
|
hctx->frame.ctl.siz_cnt = MOD_WEBSOCKET_FRAME_LEN16_CNT; |
|
hctx->frame.state = |
|
MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH; |
|
} |
|
else if (hctx->frame.ctl.siz == MOD_WEBSOCKET_FRAME_LEN63) { |
|
hctx->frame.ctl.siz = 0; |
|
hctx->frame.ctl.siz_cnt = MOD_WEBSOCKET_FRAME_LEN63_CNT; |
|
hctx->frame.state = |
|
MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH; |
|
} |
|
else { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"specified payload size =", hctx->frame.ctl.siz); |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK; |
|
} |
|
i++; |
|
break; |
|
case MOD_WEBSOCKET_FRAME_STATE_READ_EX_LENGTH: |
|
hctx->frame.ctl.siz = |
|
(hctx->frame.ctl.siz << 8) + (frame[i] & 0xff); |
|
hctx->frame.ctl.siz_cnt--; |
|
if (hctx->frame.ctl.siz_cnt <= 0) { |
|
if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_PING && |
|
hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_WARN, "sx", |
|
"frame size has been exceeded:", |
|
MOD_WEBSOCKET_BUFMAX); |
|
return -1; |
|
} |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"specified payload size =", hctx->frame.ctl.siz); |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_READ_MASK; |
|
} |
|
i++; |
|
break; |
|
case MOD_WEBSOCKET_FRAME_STATE_READ_MASK: |
|
hctx->frame.ctl.mask[hctx->frame.ctl.mask_cnt] = frame[i]; |
|
hctx->frame.ctl.mask_cnt++; |
|
if (hctx->frame.ctl.mask_cnt >= MOD_WEBSOCKET_MASK_CNT) { |
|
hctx->frame.ctl.mask_cnt = 0; |
|
if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_PING && |
|
hctx->frame.ctl.siz == 0) { |
|
mod_wstunnel_frame_send(hctx, |
|
MOD_WEBSOCKET_FRAME_TYPE_PONG, |
|
NULL, 0); |
|
} |
|
if (hctx->frame.ctl.siz == 0) { |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT; |
|
} |
|
else { |
|
hctx->frame.state = |
|
MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD; |
|
} |
|
} |
|
i++; |
|
break; |
|
case MOD_WEBSOCKET_FRAME_STATE_READ_PAYLOAD: |
|
/* hctx->frame.ctl.siz <= SIZE_MAX */ |
|
if (hctx->frame.ctl.siz <= flen - i) { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"read payload, size =", hctx->frame.ctl.siz); |
|
buffer_append_string_len(payload, frame+i, (size_t) |
|
(hctx->frame.ctl.siz & SIZE_MAX)); |
|
i += (size_t)(hctx->frame.ctl.siz & SIZE_MAX); |
|
hctx->frame.ctl.siz = 0; |
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT; |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"rest of frame size =", flen - i); |
|
/* SIZE_MAX < hctx->frame.ctl.siz */ |
|
} |
|
else { |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"read payload, size =", flen - i); |
|
buffer_append_string_len(payload, frame+i, flen - i); |
|
hctx->frame.ctl.siz -= flen - i; |
|
i += flen - i; |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sx", |
|
"rest of payload size =", hctx->frame.ctl.siz); |
|
} |
|
switch (hctx->frame.type) { |
|
case MOD_WEBSOCKET_FRAME_TYPE_TEXT: |
|
case MOD_WEBSOCKET_FRAME_TYPE_BIN: |
|
{ |
|
char *mem; |
|
size_t len; |
|
unmask_payload(hctx); |
|
len = buffer_string_length(payload); |
|
chunkqueue_get_memory(hctx->gw.wb, &mem, &len, len, 0); |
|
len = buffer_string_length(payload); |
|
memcpy(mem, payload->ptr, len); |
|
chunkqueue_use_memory(hctx->gw.wb, len); |
|
buffer_reset(payload); |
|
break; |
|
} |
|
case MOD_WEBSOCKET_FRAME_TYPE_PING: |
|
if (hctx->frame.ctl.siz == 0) { |
|
unmask_payload(hctx); |
|
mod_wstunnel_frame_send(hctx, |
|
MOD_WEBSOCKET_FRAME_TYPE_PONG, |
|
payload->ptr, buffer_string_length(payload)); |
|
buffer_reset(payload); |
|
} |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_PONG: |
|
buffer_reset(payload); |
|
break; |
|
case MOD_WEBSOCKET_FRAME_TYPE_CLOSE: |
|
default: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", |
|
"BUG: invalid frame type"); |
|
return -1; |
|
} |
|
break; |
|
default: |
|
DEBUG_LOG(MOD_WEBSOCKET_LOG_ERR, "s", "BUG: invalid state"); |
|
return -1; |
|
} |
|
} |
|
} |
|
/* XXX: should add ability to handle and preserve partial frames above */ |
|
/*(not chunkqueue_reset(); do not reset cq->bytes_in, cq->bytes_out)*/ |
|
chunkqueue_mark_written(cq, chunkqueue_length(cq)); |
|
return 0; |
|
} |
|
|
|
#endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
|
|
|
|
int mod_wstunnel_frame_send(handler_ctx *hctx, mod_wstunnel_frame_type_t type, |
|
const char *payload, size_t siz) { |
|
#ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_ |
|
if (hctx->hybivers >= 8) return send_rfc_6455(hctx, type, payload, siz); |
|
#endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
#ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ |
|
if (0 == hctx->hybivers) return send_ietf_00(hctx, type, payload, siz); |
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */ |
|
return -1; |
|
} |
|
|
|
int mod_wstunnel_frame_recv(handler_ctx *hctx) { |
|
#ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_ |
|
if (hctx->hybivers >= 8) return recv_rfc_6455(hctx); |
|
#endif /* _MOD_WEBSOCKET_SPEC_RFC_6455_ */ |
|
#ifdef _MOD_WEBSOCKET_SPEC_IETF_00_ |
|
if (0 == hctx->hybivers) return recv_ietf_00(hctx); |
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */ |
|
return -1; |
|
}
|
|
|