summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGlenn Strauss <gstrauss@gluelogic.com>2017-07-25 02:08:15 -0400
committerGlenn Strauss <gstrauss@gluelogic.com>2017-07-25 02:29:23 -0400
commit477534084adc987a5dbe13eabe40e2411a022ede (patch)
tree566f421a480bfc6b824ccc887f004bea4c8b67ee
parent889db409dcdd26de432b51e08fa7999ea902ffe4 (diff)
downloadlighttpd1.4-477534084adc987a5dbe13eabe40e2411a022ede.tar.gz
lighttpd1.4-477534084adc987a5dbe13eabe40e2411a022ede.zip
[mod_wstunnel] websocket tunnel to other protocol
*experimental* decodes websockets and passes body back and forth from backend (body could be known protocol such as JSON, or any custom protocol) originally based off https://github.com/nori0428/mod_websocket
-rw-r--r--configure.ac2
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/Makefile.am5
-rw-r--r--src/SConscript1
-rw-r--r--src/mod_wstunnel.c1351
5 files changed, 1360 insertions, 1 deletions
diff --git a/configure.ac b/configure.ac
index 8c37810f..195d6d69 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1014,7 +1014,7 @@ AC_OUTPUT
do_build="mod_cgi mod_fastcgi mod_extforward mod_proxy mod_evhost mod_simple_vhost mod_access mod_alias mod_setenv mod_usertrack mod_auth mod_authn_file mod_status mod_accesslog"
do_build="$do_build mod_rrdtool mod_secdownload mod_expire mod_compress mod_dirlisting mod_indexfile mod_userdir mod_webdav mod_staticfile mod_scgi mod_flv_streaming mod_ssi mod_deflate"
-do_build="$do_build mod_vhostdb"
+do_build="$do_build mod_vhostdb mod_wstunnel"
plugins="mod_rewrite mod_redirect"
features="regex-conditionals"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 173c564b..e6ec3e0e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -635,6 +635,7 @@ add_and_install_library(mod_userdir mod_userdir.c)
add_and_install_library(mod_usertrack mod_usertrack.c)
add_and_install_library(mod_vhostdb mod_vhostdb.c)
add_and_install_library(mod_webdav mod_webdav.c)
+add_and_install_library(mod_wstunnel mod_wstunnel.c)
add_executable(test_buffer
test_buffer.c
@@ -822,6 +823,7 @@ if(HAVE_LIBSSL AND HAVE_LIBCRYPTO)
set(L_MOD_AUTHN_FILE ${L_MOD_AUTHN_FILE} crypto)
target_link_libraries(mod_authn_file ${L_MOD_AUTHN_FILE})
target_link_libraries(mod_secdownload crypto)
+ target_link_libraries(mod_wstunnel crypto)
endif()
if(WITH_LIBEV)
diff --git a/src/Makefile.am b/src/Makefile.am
index dc12aee9..c8582029 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -370,6 +370,11 @@ mod_uploadprogress_la_SOURCES = mod_uploadprogress.c
mod_uploadprogress_la_LDFLAGS = $(common_module_ldflags)
mod_uploadprogress_la_LIBADD = $(common_libadd)
+lib_LTLIBRARIES += mod_wstunnel.la
+mod_wstunnel_la_SOURCES = mod_wstunnel.c
+mod_wstunnel_la_LDFLAGS = $(common_module_ldflags)
+mod_wstunnel_la_LIBADD = $(common_libadd) $(CRYPTO_LIB)
+
hdr = server.h base64.h buffer.h network.h log.h keyvalue.h \
response.h request.h fastcgi.h chunk.h \
diff --git a/src/SConscript b/src/SConscript
index 546f7083..3665e45d 100644
--- a/src/SConscript
+++ b/src/SConscript
@@ -124,6 +124,7 @@ modules = {
'mod_ssi' : { 'src' : [ 'mod_ssi_exprparser.c', 'mod_ssi_expr.c', 'mod_ssi.c' ] },
'mod_flv_streaming' : { 'src' : [ 'mod_flv_streaming.c' ] },
'mod_vhostdb' : { 'src' : [ 'mod_vhostdb.c' ] },
+ 'mod_wstunnel' : { 'src' : [ 'mod_wstunnel.c' ], 'lib' : [ env['LIBCRYPTO'] ] },
}
if env['with_geoip']:
diff --git a/src/mod_wstunnel.c b/src/mod_wstunnel.c
new file mode 100644
index 00000000..b817af31
--- /dev/null
+++ b/src/mod_wstunnel.c
@@ -0,0 +1,1351 @@
+/*
+ * 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" => "..." )
+ *
+ * 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 <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"
+#include "plugin.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;
+
+ 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 */
+ if (s->gw.exts_auth->used) { /*(check after gw_set_defaults_backend())*/
+ 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);
+ /*PATCH_GW(exts_auth);*//*(wstunnel can not act as authorizer)*/
+ 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;
+ 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 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();
+
+ if (!buffer_is_empty(hctx->conf.frame_type)) { /*("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
+ && 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->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;
+ array * const hdrs = con->response.headers;
+ data_string *ds;
+
+ /* "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
+
+ ds = (data_string *)array_get_unused_element(hdrs, TYPE_STRING);
+ if (NULL == ds) ds = data_string_init();
+ buffer_copy_string_len(ds->key, CONST_STR_LEN("Sec-WebSocket-Location"));
+ if (buffer_is_equal_string(con->uri.scheme, CONST_STR_LEN("https")))
+ buffer_copy_string_len(ds->value, CONST_STR_LEN("wss://"));
+ else
+ buffer_copy_string_len(ds->value, CONST_STR_LEN("ws://"));
+ buffer_append_string_buffer(ds->value, con->request.http_host);
+ buffer_append_string_buffer(ds->value, con->uri.path);
+ array_insert_unique(hdrs, (data_unset *)ds);
+
+ 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;
+ 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
+
+ ds = (data_string *)array_get_unused_element(hdrs, TYPE_STRING);
+ if (NULL == ds) ds = data_string_init();
+ buffer_copy_string_len(ds->key, CONST_STR_LEN("Sec-WebSocket-Accept"));
+ buffer_append_base64_encode(ds->value, sha_digest, SHA_DIGEST_LENGTH, BASE64_STANDARD);
+ array_insert_unique(hdrs, (data_unset *)ds);
+
+ /*(admin can set "Sec-WebSocket-Protocol" response hdr using mod_setenv)*/
+
+ 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;
+ }
+ http_chunk_append_mem(srv, con, "", 1); /* needs '\0' to send */
+ DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx",
+ "send data to client ( fd =", con->fd, "), frame size =", len+1);
+ 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)+1;
+ chunkqueue_get_memory(hctx->gw.wb, &mem, &len, len, 0);
+ len = buffer_string_length(payload)+1;
+ memcpy(mem, payload->ptr, len);
+ mem[len] = '\0'; /* needs '\0' char to send */
+ chunkqueue_use_memory(hctx->gw.wb, len+1);
+ 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;
+ }
+ /* needs '\0' char to send */
+ buffer_append_string_len(b, CONST_STR_LEN(""));
+ 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;
+ }
+ }
+ }
+ chunkqueue_reset(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 {
+ memset(mem, 0, sizeof(mem));
+ 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);
+ http_chunk_append_mem(srv, con, "", 1); /* needs '\0' to send */
+ DEBUG_LOG(MOD_WEBSOCKET_LOG_DEBUG, "sdsx",
+ "send data to client ( fd =",con->fd,"), frame size =",len+siz+1);
+ 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)+1;
+ chunkqueue_get_memory(hctx->gw.wb, &mem, &len, len, 0);
+ len = buffer_string_length(payload)+1;
+ memcpy(mem, payload->ptr, len);
+ mem[len] = '\0'; /* needs '\0' char to send */
+ chunkqueue_use_memory(hctx->gw.wb, len+1);
+ 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)+1);
+ 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;
+ }
+ }
+ }
+ chunkqueue_reset(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;
+}