1326 lines
50 KiB
C
1326 lines
50 KiB
C
/*
|
|
* 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 "http_header.h"
|
|
#include "log.h"
|
|
#include "connections.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_ERR(format, ...) \
|
|
if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_ERR) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
|
|
|
|
#define DEBUG_LOG_WARN(format, ...) \
|
|
if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_WARN) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
|
|
|
|
#define DEBUG_LOG_INFO(format, ...) \
|
|
if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_INFO) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
|
|
|
|
#define DEBUG_LOG_DEBUG(format, ...) \
|
|
if (hctx->gw.conf.debug >= MOD_WEBSOCKET_LOG_DEBUG) { log_error(hctx->errh, __FILE__, __LINE__, (format), __VA_ARGS__); }
|
|
|
|
typedef struct {
|
|
gw_plugin_config gw; /* start must match layout of gw_plugin_config */
|
|
const array *origins;
|
|
unsigned int frame_type;
|
|
unsigned short int ping_interval;
|
|
} plugin_config;
|
|
|
|
typedef struct plugin_data {
|
|
PLUGIN_DATA;
|
|
pid_t srv_pid; /* must match layout of gw_plugin_data through conf member */
|
|
plugin_config conf;
|
|
plugin_config defaults;
|
|
} 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;
|
|
|
|
log_error_st *errh; /*(for mod_wstunnel module-specific DEBUG_*() macros)*/
|
|
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));
|
|
}
|
|
|
|
static void mod_wstunnel_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
|
|
switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
|
|
case 0: /* wstunnel.server */
|
|
if (cpv->vtype == T_CONFIG_LOCAL) {
|
|
gw_plugin_config * const gw = cpv->v.v;
|
|
pconf->gw.exts = gw->exts;
|
|
pconf->gw.exts_auth = gw->exts_auth;
|
|
pconf->gw.exts_resp = gw->exts_resp;
|
|
}
|
|
break;
|
|
case 1: /* wstunnel.balance */
|
|
/*if (cpv->vtype == T_CONFIG_LOCAL)*//*always true here for this param*/
|
|
pconf->gw.balance = (int)cpv->v.u;
|
|
break;
|
|
case 2: /* wstunnel.debug */
|
|
pconf->gw.debug = (int)cpv->v.u;
|
|
break;
|
|
case 3: /* wstunnel.map-extensions */
|
|
pconf->gw.ext_mapping = cpv->v.a;
|
|
break;
|
|
case 4: /* wstunnel.frame-type */
|
|
pconf->frame_type = cpv->v.u;
|
|
break;
|
|
case 5: /* wstunnel.origins */
|
|
pconf->origins = cpv->v.a;
|
|
break;
|
|
case 6: /* wstunnel.ping-interval */
|
|
pconf->ping_interval = cpv->v.shrt;
|
|
break;
|
|
default:/* should not happen */
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void mod_wstunnel_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
|
|
do {
|
|
mod_wstunnel_merge_config_cpv(pconf, cpv);
|
|
} while ((++cpv)->k_id != -1);
|
|
}
|
|
|
|
static void mod_wstunnel_patch_config(request_st * const r, plugin_data * const p) {
|
|
memcpy(&p->conf, &p->defaults, sizeof(plugin_config));
|
|
for (int i = 1, used = p->nconfig; i < used; ++i) {
|
|
if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
|
|
mod_wstunnel_merge_config(&p->conf, p->cvlist+p->cvlist[i].v.u2[0]);
|
|
}
|
|
}
|
|
|
|
SETDEFAULTS_FUNC(mod_wstunnel_set_defaults) {
|
|
static const config_plugin_keys_t cpk[] = {
|
|
{ CONST_STR_LEN("wstunnel.server"),
|
|
T_CONFIG_ARRAY_KVARRAY,
|
|
T_CONFIG_SCOPE_CONNECTION }
|
|
,{ CONST_STR_LEN("wstunnel.balance"),
|
|
T_CONFIG_STRING,
|
|
T_CONFIG_SCOPE_CONNECTION }
|
|
,{ CONST_STR_LEN("wstunnel.debug"),
|
|
T_CONFIG_INT,
|
|
T_CONFIG_SCOPE_CONNECTION }
|
|
,{ CONST_STR_LEN("wstunnel.map-extensions"),
|
|
T_CONFIG_ARRAY_KVSTRING,
|
|
T_CONFIG_SCOPE_CONNECTION }
|
|
,{ CONST_STR_LEN("wstunnel.frame-type"),
|
|
T_CONFIG_STRING,
|
|
T_CONFIG_SCOPE_CONNECTION }
|
|
,{ CONST_STR_LEN("wstunnel.origins"),
|
|
T_CONFIG_ARRAY_VLIST,
|
|
T_CONFIG_SCOPE_CONNECTION }
|
|
,{ CONST_STR_LEN("wstunnel.ping-interval"),
|
|
T_CONFIG_SHORT,
|
|
T_CONFIG_SCOPE_CONNECTION }
|
|
,{ NULL, 0,
|
|
T_CONFIG_UNSET,
|
|
T_CONFIG_SCOPE_UNSET }
|
|
};
|
|
|
|
plugin_data * const p = p_d;
|
|
if (!config_plugin_values_init(srv, p, cpk, "mod_wstunnel"))
|
|
return HANDLER_ERROR;
|
|
|
|
/* process and validate config directives
|
|
* (init i to 0 if global context; to 1 to skip empty global context) */
|
|
for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
|
|
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
|
|
gw_plugin_config *gw = NULL;
|
|
for (; -1 != cpv->k_id; ++cpv) {
|
|
switch (cpv->k_id) {
|
|
case 0: /* wstunnel.server */
|
|
gw = calloc(1, sizeof(gw_plugin_config));
|
|
force_assert(gw);
|
|
if (!gw_set_defaults_backend(srv, (gw_plugin_data *)p, cpv->v.a,
|
|
gw, 0, cpk[cpv->k_id].k)) {
|
|
gw_plugin_config_free(gw);
|
|
return HANDLER_ERROR;
|
|
}
|
|
/* error if "mode" = "authorizer";
|
|
* wstunnel can not act as authorizer */
|
|
/*(check after gw_set_defaults_backend())*/
|
|
if (gw->exts_auth && gw->exts_auth->used) {
|
|
log_error(srv->errh, __FILE__, __LINE__,
|
|
"%s must not define any hosts with "
|
|
"attribute \"mode\" = \"authorizer\"", cpk[cpv->k_id].k);
|
|
gw_plugin_config_free(gw);
|
|
return HANDLER_ERROR;
|
|
}
|
|
cpv->v.v = gw;
|
|
cpv->vtype = T_CONFIG_LOCAL;
|
|
break;
|
|
case 1: /* wstunnel.balance */
|
|
cpv->v.u = (unsigned int)gw_get_defaults_balance(srv, cpv->v.b);
|
|
break;
|
|
case 2: /* wstunnel.debug */
|
|
case 3: /* wstunnel.map-extensions */
|
|
break;
|
|
case 4: /* wstunnel.frame-type */
|
|
/*(default frame-type to "text" unless "binary" is specified)*/
|
|
cpv->v.u =
|
|
buffer_eq_icase_slen(cpv->v.b, CONST_STR_LEN("binary"));
|
|
break;
|
|
case 5: /* wstunnel.origins */
|
|
for (uint32_t j = 0; j < cpv->v.a->used; ++j) {
|
|
buffer *origin = &((data_string *)cpv->v.a->data[j])->value;
|
|
if (buffer_string_is_empty(origin)) {
|
|
log_error(srv->errh, __FILE__, __LINE__,
|
|
"unexpected empty string in %s", cpk[cpv->k_id].k);
|
|
return HANDLER_ERROR;
|
|
}
|
|
}
|
|
break;
|
|
case 6: /* wstunnel.ping-interval */
|
|
break;
|
|
default:/* should not happen */
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* disable check-local for all exts (default enabled) */
|
|
if (gw && gw->exts) { /*(check after gw_set_defaults_backend())*/
|
|
gw_exts_clear_check_local(gw->exts);
|
|
}
|
|
}
|
|
|
|
/* default is 0 */
|
|
/*p->defaults.balance = (unsigned int)gw_get_defaults_balance(srv, NULL);*/
|
|
p->defaults.ping_interval = 0; /* do not send ping */
|
|
|
|
/* initialize p->defaults from global config context */
|
|
if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
|
|
const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
|
|
if (-1 != cpv->k_id)
|
|
mod_wstunnel_merge_config(&p->defaults, cpv);
|
|
}
|
|
|
|
return HANDLER_GO_ON;
|
|
}
|
|
|
|
static handler_t wstunnel_create_env(gw_handler_ctx *gwhctx) {
|
|
handler_ctx *hctx = (handler_ctx *)gwhctx;
|
|
request_st * const r = hctx->gw.r;
|
|
handler_t rc;
|
|
if (0 == r->reqbody_length) {
|
|
http_response_upgrade_read_body_unknown(r);
|
|
chunkqueue_append_chunkqueue(r->reqbody_queue, r->read_queue);
|
|
}
|
|
rc = mod_wstunnel_handshake_create_response(hctx);
|
|
if (rc != HANDLER_GO_ON) return rc;
|
|
|
|
r->http_status = 101; /* Switching Protocols */
|
|
r->resp_body_started = 1;
|
|
|
|
hctx->ping_ts = log_epoch_secs;
|
|
gw_set_transparent(&hctx->gw);
|
|
|
|
return HANDLER_GO_ON;
|
|
}
|
|
|
|
static handler_t wstunnel_stdin_append(gw_handler_ctx *gwhctx) {
|
|
/* prepare websocket frames to backend */
|
|
/* (caller should verify r->reqbody_queue) */
|
|
/*assert(!chunkqueue_is_empty(r->reqbody_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 */
|
|
request_st * const r = hctx->gw.r;
|
|
DEBUG_LOG_INFO("disconnected from client (fd=%d)", r->con->fd);
|
|
DEBUG_LOG_DEBUG("send close response to client (fd=%d)", r->con->fd);
|
|
mod_wstunnel_frame_send(hctx, MOD_WEBSOCKET_FRAME_TYPE_CLOSE, CONST_STR_LEN("1000")); /* 1000 Normal Closure */
|
|
gw_connection_reset(r, hctx->gw.plugin_data);
|
|
return HANDLER_FINISHED;
|
|
}
|
|
}
|
|
|
|
static handler_t wstunnel_recv_parse(request_st * const r, http_response_opts * const opts, buffer * const b, size_t n) {
|
|
handler_ctx *hctx = (handler_ctx *)opts->pdata;
|
|
DEBUG_LOG_DEBUG("recv data from backend (fd=%d), size=%zx", hctx->gw.fd, n);
|
|
if (0 == n) return HANDLER_FINISHED;
|
|
if (mod_wstunnel_frame_send(hctx,hctx->frame.type_backend,b->ptr,n) < 0) {
|
|
DEBUG_LOG_ERR("%s", "fail to send data to client");
|
|
return HANDLER_ERROR;
|
|
}
|
|
buffer_clear(b);
|
|
UNUSED(r);
|
|
return HANDLER_GO_ON;
|
|
}
|
|
|
|
static int wstunnel_is_allowed_origin(request_st * const r, handler_ctx * const 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;
|
|
const buffer *origin = NULL;
|
|
size_t olen;
|
|
|
|
if (NULL == allowed_origins || 0 == allowed_origins->used) {
|
|
DEBUG_LOG_INFO("%s", "allowed origins not specified");
|
|
return 1;
|
|
}
|
|
|
|
/* "Origin" header is preferred
|
|
* ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */
|
|
origin = http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Origin"));
|
|
if (NULL == origin) {
|
|
origin =
|
|
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Origin"));
|
|
}
|
|
olen = buffer_string_length(origin);
|
|
if (0 == olen) {
|
|
DEBUG_LOG_ERR("%s", "Origin header is invalid");
|
|
r->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_INFO("%s matches allowed origin: %s",origin->ptr,b->ptr);
|
|
return 1;
|
|
}
|
|
}
|
|
DEBUG_LOG_INFO("%s does not match any allowed origins", origin->ptr);
|
|
r->http_status = 403; /* Forbidden */
|
|
return 0;
|
|
}
|
|
|
|
static int wstunnel_check_request(request_st * const r, handler_ctx * const hctx) {
|
|
const buffer * const vers =
|
|
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Version"));
|
|
const long hybivers = (NULL != vers)
|
|
? light_isdigit(*vers->ptr) ? strtol(vers->ptr, NULL, 10) : -1
|
|
: 0;
|
|
if (hybivers < 0 || hybivers > INT_MAX) {
|
|
DEBUG_LOG_ERR("%s", "invalid Sec-WebSocket-Version");
|
|
r->http_status = 400; /* Bad Request */
|
|
return -1;
|
|
}
|
|
|
|
/*(redundant since HTTP/1.1 required in mod_wstunnel_check_extension())*/
|
|
if (buffer_is_empty(r->http_host)) {
|
|
DEBUG_LOG_ERR("%s", "Host header does not exist");
|
|
r->http_status = 400; /* Bad Request */
|
|
return -1;
|
|
}
|
|
|
|
if (!wstunnel_is_allowed_origin(r, 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;
|
|
chunk_buffer_release(hctx->frame.payload);
|
|
}
|
|
|
|
static handler_t wstunnel_handler_setup (request_st * const r, plugin_data * const p) {
|
|
handler_ctx *hctx = r->plugin_ctx[p->id];
|
|
int hybivers;
|
|
hctx->errh = r->conf.errh;/*(for mod_wstunnel-specific DEBUG_* macros)*/
|
|
hctx->conf = p->conf; /*(copies struct)*/
|
|
hybivers = wstunnel_check_request(r, hctx);
|
|
if (hybivers < 0) return HANDLER_FINISHED;
|
|
hctx->hybivers = hybivers;
|
|
if (0 == hybivers) {
|
|
DEBUG_LOG_INFO("WebSocket Version = %s", "hybi-00");
|
|
}
|
|
else {
|
|
DEBUG_LOG_INFO("WebSocket Version = %d", 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 = chunk_buffer_acquire();
|
|
|
|
hctx->frame.state = MOD_WEBSOCKET_FRAME_STATE_INIT;
|
|
hctx->frame.ctl.siz = 0;
|
|
hctx->frame.payload = chunk_buffer_acquire();
|
|
|
|
unsigned int binary = hctx->conf.frame_type; /*(0 = "text"; 1 = "binary")*/
|
|
if (!binary) {
|
|
const buffer *vb =
|
|
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Protocol"));
|
|
if (NULL != vb) {
|
|
for (const char *s = vb->ptr; *s; ++s) {
|
|
while (*s==' '||*s=='\t'||*s=='\r'||*s=='\n') ++s;
|
|
if (buffer_eq_icase_ssn(s, CONST_STR_LEN("binary"))) {
|
|
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 (buffer_eq_icase_ssn(s, CONST_STR_LEN("base64"))) {
|
|
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_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_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(request_st * const r, void *p_d) {
|
|
plugin_data *p = p_d;
|
|
const buffer *vb;
|
|
handler_t rc;
|
|
|
|
if (NULL != r->handler_module)
|
|
return HANDLER_GO_ON;
|
|
if (r->http_method != HTTP_METHOD_GET)
|
|
return HANDLER_GO_ON;
|
|
if (r->http_version != HTTP_VERSION_1_1)
|
|
return HANDLER_GO_ON;
|
|
|
|
/*
|
|
* Connection: upgrade, keep-alive, ...
|
|
* Upgrade: WebSocket, ...
|
|
*/
|
|
vb = http_header_request_get(r, HTTP_HEADER_UPGRADE, CONST_STR_LEN("Upgrade"));
|
|
if (NULL == vb
|
|
|| !http_header_str_contains_token(CONST_BUF_LEN(vb), CONST_STR_LEN("websocket")))
|
|
return HANDLER_GO_ON;
|
|
vb = http_header_request_get(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"));
|
|
if (NULL == vb
|
|
|| !http_header_str_contains_token(CONST_BUF_LEN(vb), CONST_STR_LEN("upgrade")))
|
|
return HANDLER_GO_ON;
|
|
|
|
mod_wstunnel_patch_config(r, p);
|
|
if (NULL == p->conf.gw.exts) return HANDLER_GO_ON;
|
|
|
|
rc = gw_check_extension(r, (gw_plugin_data *)p, 1, sizeof(handler_ctx));
|
|
return (HANDLER_GO_ON == rc && r->handler_module == p->self)
|
|
? wstunnel_handler_setup(r, p)
|
|
: rc;
|
|
}
|
|
|
|
TRIGGER_FUNC(mod_wstunnel_handle_trigger) {
|
|
const plugin_data * const p = p_d;
|
|
const time_t cur_ts = log_epoch_secs + 1;
|
|
|
|
gw_handle_trigger(srv, p_d);
|
|
|
|
for (uint32_t i = 0; i < srv->conns.used; ++i) {
|
|
connection *con = srv->conns.ptr[i];
|
|
request_st * const r = &con->request;
|
|
handler_ctx *hctx = r->plugin_ctx[p->id];
|
|
if (NULL == hctx || r->handler_module != p->self)
|
|
continue;
|
|
|
|
if (hctx->gw.state != GW_STATE_WRITE && hctx->gw.state != GW_STATE_READ)
|
|
continue;
|
|
|
|
if (cur_ts - con->read_idle_ts > r->conf.max_read_idle) {
|
|
DEBUG_LOG_INFO("timeout client (fd=%d)", con->fd);
|
|
mod_wstunnel_frame_send(hctx,MOD_WEBSOCKET_FRAME_TYPE_CLOSE,NULL,0);
|
|
gw_connection_reset(r, p_d);
|
|
joblist_append(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(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 = "wstunnel";
|
|
p->init = mod_wstunnel_init;
|
|
p->cleanup = gw_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;
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
* modified from Norio Kobota mod_websocket_handshake.c
|
|
*/
|
|
|
|
#ifdef _MOD_WEBSOCKET_SPEC_IETF_00_
|
|
|
|
#include "sys-crypto-md.h" /* lighttpd */
|
|
#include "sys-endian.h" /* lighttpd */
|
|
|
|
static int get_key3(request_st * const r, char *buf) {
|
|
/* 8 bytes should have been sent with request
|
|
* for draft-ietf-hybi-thewebsocketprotocol-00 */
|
|
chunkqueue *cq = r->reqbody_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 = buffer_string_length(b); 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 || !light_isdigit(*tmp)) return -1;
|
|
*ret = (uint32_t)n / sp;
|
|
return 0;
|
|
}
|
|
|
|
static int create_MD5_sum(request_st * const r) {
|
|
uint32_t buf[4]; /* MD5 binary hash len */
|
|
li_MD5_CTX ctx;
|
|
|
|
const buffer *key1 =
|
|
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key1"));
|
|
const buffer *key2 =
|
|
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key2"));
|
|
|
|
if (NULL == key1 || get_key_number(buf+0, key1) < 0 ||
|
|
NULL == key2 || get_key_number(buf+1, key2) < 0 ||
|
|
get_key3(r, (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(r->write_queue, (char *)buf, sizeof(buf));
|
|
return 0;
|
|
}
|
|
|
|
static int create_response_ietf_00(handler_ctx *hctx) {
|
|
request_st * const r = hctx->gw.r;
|
|
buffer *value = r->tmp_buf;
|
|
|
|
/* "Origin" header is preferred
|
|
* ("Sec-WebSocket-Origin" is from older drafts of websocket spec) */
|
|
const buffer *origin = http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Origin"));
|
|
if (NULL == origin) {
|
|
origin =
|
|
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Origin"));
|
|
}
|
|
if (NULL == origin) {
|
|
DEBUG_LOG_ERR("%s", "Origin header is invalid");
|
|
return -1;
|
|
}
|
|
if (buffer_is_empty(r->http_host)) {
|
|
DEBUG_LOG_ERR("%s", "Host header does not exist");
|
|
return -1;
|
|
}
|
|
|
|
/* calc MD5 sum from keys */
|
|
if (create_MD5_sum(r) < 0) {
|
|
DEBUG_LOG_ERR("%s", "Sec-WebSocket-Key is invalid");
|
|
return -1;
|
|
}
|
|
|
|
http_header_response_set(r, HTTP_HEADER_UPGRADE,
|
|
CONST_STR_LEN("Upgrade"),
|
|
CONST_STR_LEN("websocket"));
|
|
#if 0 /*(added later in http_response_write_header())*/
|
|
http_header_response_append(r, HTTP_HEADER_CONNECTION,
|
|
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()) */
|
|
http_header_response_set(r, HTTP_HEADER_OTHER,
|
|
CONST_STR_LEN("Sec-WebSocket-Origin"),
|
|
CONST_BUF_LEN(origin));
|
|
#endif
|
|
|
|
if (buffer_is_equal_string(&r->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, r->http_host);
|
|
buffer_append_string_buffer(value, &r->uri.path);
|
|
http_header_response_set(r, HTTP_HEADER_OTHER,
|
|
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 "sys-crypto-md.h" /* lighttpd */
|
|
#include "base64.h" /* lighttpd */
|
|
|
|
static int create_response_rfc_6455(handler_ctx *hctx) {
|
|
request_st * const r = hctx->gw.r;
|
|
SHA_CTX sha;
|
|
unsigned char sha_digest[SHA_DIGEST_LENGTH];
|
|
|
|
const buffer *value_wskey =
|
|
http_header_request_get(r, HTTP_HEADER_OTHER, CONST_STR_LEN("Sec-WebSocket-Key"));
|
|
if (NULL == value_wskey) {
|
|
DEBUG_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(value_wskey));
|
|
SHA1_Update(&sha, (const unsigned char *)CONST_STR_LEN("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
|
|
SHA1_Final(sha_digest, &sha);
|
|
|
|
http_header_response_set(r, HTTP_HEADER_UPGRADE,
|
|
CONST_STR_LEN("Upgrade"),
|
|
CONST_STR_LEN("websocket"));
|
|
#if 0 /*(added later in http_response_write_header())*/
|
|
http_header_response_append(r, HTTP_HEADER_CONNECTION,
|
|
CONST_STR_LEN("Connection"),
|
|
CONST_STR_LEN("upgrade"));
|
|
#endif
|
|
|
|
buffer *value = r->tmp_buf;
|
|
buffer_clear(value);
|
|
buffer_append_base64_encode(value, sha_digest, SHA_DIGEST_LENGTH, BASE64_STANDARD);
|
|
http_header_response_set(r, HTTP_HEADER_OTHER,
|
|
CONST_STR_LEN("Sec-WebSocket-Accept"),
|
|
CONST_BUF_LEN(value));
|
|
|
|
if (hctx->frame.type == MOD_WEBSOCKET_FRAME_TYPE_BIN)
|
|
http_header_response_set(r, HTTP_HEADER_OTHER,
|
|
CONST_STR_LEN("Sec-WebSocket-Protocol"),
|
|
CONST_STR_LEN("binary"));
|
|
else if (-1 == hctx->subproto)
|
|
http_header_response_set(r, HTTP_HEADER_OTHER,
|
|
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) {
|
|
request_st * const r = hctx->gw.r;
|
|
#ifdef _MOD_WEBSOCKET_SPEC_RFC_6455_
|
|
if (hctx->hybivers >= 8) {
|
|
DEBUG_LOG_DEBUG("%s", "send handshake response");
|
|
if (0 != create_response_rfc_6455(hctx)) {
|
|
r->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 = r->reqbody_queue;
|
|
if (chunkqueue_length(cq) < 8)
|
|
return HANDLER_WAIT_FOR_EVENT;
|
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
|
|
|
|
DEBUG_LOG_DEBUG("%s", "send handshake response");
|
|
if (0 != create_response_ietf_00(hctx)) {
|
|
r->http_status = 400; /* Bad Request */
|
|
return HANDLER_ERROR;
|
|
}
|
|
return HANDLER_GO_ON;
|
|
}
|
|
#endif /* _MOD_WEBSOCKET_SPEC_IETF_00_ */
|
|
|
|
DEBUG_LOG_ERR("%s", "not supported WebSocket Version");
|
|
r->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 */
|
|
request_st * const r = hctx->gw.r;
|
|
char *mem;
|
|
size_t len;
|
|
|
|
switch (type) {
|
|
case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
|
|
if (0 == siz) return 0;
|
|
http_chunk_append_mem(r, &head, 1);
|
|
http_chunk_append_mem(r, payload, siz);
|
|
http_chunk_append_mem(r, &tail, 1);
|
|
len = siz+2;
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_BIN:
|
|
if (0 == siz) return 0;
|
|
http_chunk_append_mem(r, &head, 1);
|
|
len = 4*(siz/3)+4+1;
|
|
/* avoid accumulating too much data in memory; send to tmpfile */
|
|
mem = malloc(len);
|
|
force_assert(mem);
|
|
len=li_to_base64(mem,len,(unsigned char *)payload,siz,BASE64_STANDARD);
|
|
http_chunk_append_mem(r, mem, len);
|
|
free(mem);
|
|
http_chunk_append_mem(r, &tail, 1);
|
|
len += 2;
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
|
|
http_chunk_append_mem(r, &tail, 1);
|
|
http_chunk_append_mem(r, &head, 1);
|
|
len = 2;
|
|
break;
|
|
default:
|
|
DEBUG_LOG_ERR("%s", "invalid frame type");
|
|
return -1;
|
|
}
|
|
DEBUG_LOG_DEBUG("send data to client (fd=%d), frame size=%zx",
|
|
r->con->fd, len);
|
|
return 0;
|
|
}
|
|
|
|
static int recv_ietf_00(handler_ctx *hctx) {
|
|
request_st * const r = hctx->gw.r;
|
|
chunkqueue *cq = r->reqbody_queue;
|
|
buffer *payload = hctx->frame.payload;
|
|
char *mem;
|
|
DEBUG_LOG_DEBUG("recv data from client (fd=%d), size=%llx",
|
|
r->con->fd, (long long)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_DEBUG("%s", "recv close frame");
|
|
return -1;
|
|
}
|
|
else {
|
|
DEBUG_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_DEBUG("got continuous payload, size=%zx", flen-i);
|
|
hctx->frame.ctl.siz += flen - i;
|
|
if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
|
|
DEBUG_LOG_WARN("frame size has been exceeded: %x",
|
|
MOD_WEBSOCKET_BUFMAX);
|
|
return -1;
|
|
}
|
|
buffer_append_string_len(payload, frame+i, flen - i);
|
|
i += flen - i;
|
|
}
|
|
else {
|
|
DEBUG_LOG_DEBUG("got final payload, size=%zx",
|
|
mem - (frame+i));
|
|
hctx->frame.ctl.siz += (mem - (frame+i));
|
|
if (hctx->frame.ctl.siz > MOD_WEBSOCKET_BUFMAX) {
|
|
DEBUG_LOG_WARN("frame size has been exceeded: %x",
|
|
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)) {
|
|
hctx->frame.ctl.siz = 0;
|
|
chunkqueue_append_buffer(hctx->gw.wb, payload);
|
|
buffer_clear(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, &len);
|
|
b = hctx->gw.wb->last->mem;
|
|
len = buffer_string_length(b);
|
|
DEBUG_LOG_DEBUG("try to base64 decode: %s",
|
|
payload->ptr);
|
|
if (NULL == buffer_append_base64_decode(b, CONST_BUF_LEN(payload), BASE64_STANDARD)) {
|
|
DEBUG_LOG_ERR("%s", "fail to base64-decode");
|
|
return -1;
|
|
}
|
|
buffer_clear(payload);
|
|
/*chunkqueue_use_memory()*/
|
|
hctx->gw.wb->bytes_in += buffer_string_length(b)-len;
|
|
}
|
|
}
|
|
break;
|
|
default: /* never reach */
|
|
DEBUG_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) {
|
|
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_DEBUG("%s", "type = text");
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_BIN:
|
|
mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_BIN);
|
|
DEBUG_LOG_DEBUG("%s", "type = binary");
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_PING:
|
|
mem[0] = (char) (0x80 | MOD_WEBSOCKET_OPCODE_PING);
|
|
DEBUG_LOG_DEBUG("%s", "type = ping");
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_PONG:
|
|
mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_PONG);
|
|
DEBUG_LOG_DEBUG("%s", "type = pong");
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
|
|
default:
|
|
mem[0] = (char)(0x80 | MOD_WEBSOCKET_OPCODE_CLOSE);
|
|
DEBUG_LOG_DEBUG("%s", "type = close");
|
|
break;
|
|
}
|
|
|
|
DEBUG_LOG_DEBUG("payload size=%zx", 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;
|
|
}
|
|
request_st * const r = hctx->gw.r;
|
|
http_chunk_append_mem(r, mem, len);
|
|
if (siz) http_chunk_append_mem(r, payload, siz);
|
|
DEBUG_LOG_DEBUG("send data to client (fd=%d), frame size=%zx",
|
|
r->con->fd, 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) {
|
|
request_st * const r = hctx->gw.r;
|
|
chunkqueue *cq = r->reqbody_queue;
|
|
buffer *payload = hctx->frame.payload;
|
|
DEBUG_LOG_DEBUG("recv data from client (fd=%d), size=%llx",
|
|
r->con->fd, (long long)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_DEBUG("%s", "type = continue");
|
|
hctx->frame.type = hctx->frame.type_before;
|
|
break;
|
|
case MOD_WEBSOCKET_OPCODE_TEXT:
|
|
DEBUG_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_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_DEBUG("%s", "type = ping");
|
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PING;
|
|
break;
|
|
case MOD_WEBSOCKET_OPCODE_PONG:
|
|
DEBUG_LOG_DEBUG("%s", "type = pong");
|
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_PONG;
|
|
break;
|
|
case MOD_WEBSOCKET_OPCODE_CLOSE:
|
|
DEBUG_LOG_DEBUG("%s", "type = close");
|
|
hctx->frame.type = MOD_WEBSOCKET_FRAME_TYPE_CLOSE;
|
|
return -1;
|
|
break;
|
|
default:
|
|
DEBUG_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_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_DEBUG("specified payload size=%llx",
|
|
(unsigned long long)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_DEBUG("specified payload size=%llx",
|
|
(unsigned long long)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_WARN("frame size has been exceeded: %x",
|
|
MOD_WEBSOCKET_BUFMAX);
|
|
return -1;
|
|
}
|
|
DEBUG_LOG_DEBUG("specified payload size=%llx",
|
|
(unsigned long long)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_DEBUG("read payload, size=%llx",
|
|
(unsigned long long)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_DEBUG("rest of frame size=%zx", flen - i);
|
|
/* SIZE_MAX < hctx->frame.ctl.siz */
|
|
}
|
|
else {
|
|
DEBUG_LOG_DEBUG("read payload, size=%zx", flen - i);
|
|
buffer_append_string_len(payload, frame+i, flen - i);
|
|
hctx->frame.ctl.siz -= flen - i;
|
|
i += flen - i;
|
|
DEBUG_LOG_DEBUG("rest of payload size=%llx",
|
|
(unsigned long long)hctx->frame.ctl.siz);
|
|
}
|
|
switch (hctx->frame.type) {
|
|
case MOD_WEBSOCKET_FRAME_TYPE_TEXT:
|
|
case MOD_WEBSOCKET_FRAME_TYPE_BIN:
|
|
{
|
|
unmask_payload(hctx);
|
|
chunkqueue_append_buffer(hctx->gw.wb, payload);
|
|
buffer_clear(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_clear(payload);
|
|
}
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_PONG:
|
|
buffer_clear(payload);
|
|
break;
|
|
case MOD_WEBSOCKET_FRAME_TYPE_CLOSE:
|
|
default:
|
|
DEBUG_LOG_ERR("%s", "BUG: invalid frame type");
|
|
return -1;
|
|
}
|
|
break;
|
|
default:
|
|
DEBUG_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;
|
|
}
|