From aaffffdba2fa79f06285d1afadd81316bc189014 Mon Sep 17 00:00:00 2001 From: Suru Dissanaike Date: Wed, 1 Dec 2021 16:22:58 +0100 Subject: [PATCH] quickjs-websocket: 1.1 --- quickjs-websocket/Makefile | 55 ++ quickjs-websocket/README | 23 + quickjs-websocket/src/Makefile | 38 ++ quickjs-websocket/src/lws-client.c | 958 +++++++++++++++++++++++++++++ quickjs-websocket/src/websocket.js | 415 +++++++++++++ 5 files changed, 1489 insertions(+) create mode 100644 quickjs-websocket/Makefile create mode 100755 quickjs-websocket/README create mode 100644 quickjs-websocket/src/Makefile create mode 100644 quickjs-websocket/src/lws-client.c create mode 100644 quickjs-websocket/src/websocket.js diff --git a/quickjs-websocket/Makefile b/quickjs-websocket/Makefile new file mode 100644 index 000000000..e183f91bf --- /dev/null +++ b/quickjs-websocket/Makefile @@ -0,0 +1,55 @@ +# +# Copyright (c) 2020 Genexis B.V. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=quickjs-websocket +PKG_LICENSE:=MIT +PKG_VERSION:=1 +PKG_RELEASE:=1 + +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk + +define Package/quickjs-websocket + SECTION:=libs + CATEGORY:=Libraries + TITLE:=WebSocket API for QuickJS + MAINTAINER:=Erik Karlsson + DEPENDS:=+quickjs +libwebsockets +endef + +define Package/quickjs-websocket/description + Implementation of the W3C WebSocket API in QuickJS on top of the + libwebsockets C library. +endef + +define Package/quickjs-websocket/install + $(INSTALL_DIR) $(1)/usr/lib/quickjs + $(CP) $(PKG_BUILD_DIR)/lws-client.so $(1)/usr/lib/quickjs/ + $(CP) $(PKG_BUILD_DIR)/websocket.js $(1)/usr/lib/quickjs/ +endef + +$(eval $(call BuildPackage,quickjs-websocket)) diff --git a/quickjs-websocket/README b/quickjs-websocket/README new file mode 100755 index 000000000..6dd61ffe1 --- /dev/null +++ b/quickjs-websocket/README @@ -0,0 +1,23 @@ +WebSocket API for QuickJS +=============== + +Introduction +------------ +This is an implementation of the W3C WebSocket API for the QuickJS +JavaScript engine on top of the libwebsockets C library. + +Usage +------------ +import { WebSocket } from '/usr/lib/quickjs/websocket.js' + +const w = new WebSocket('wss://example.com/', ['protocol1', 'protocol2']) + +globalThis.WebSocket = WebSocket // To make the API available globally + +Limitations +------------ +Events emitted by WebSocket objects do not implement the full DOM +event specification. Only a subset of properties is available. The +EventTarget interface, i.e. addEventListener/removeEventListener, is +unimplemented. The onopen/onerror/onclose/onmesseage handlers have to +be used instead. diff --git a/quickjs-websocket/src/Makefile b/quickjs-websocket/src/Makefile new file mode 100644 index 000000000..d243aa08f --- /dev/null +++ b/quickjs-websocket/src/Makefile @@ -0,0 +1,38 @@ +# +# Copyright (c) 2020 Genexis B.V. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, copy, +# modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +TARGETS = lws-client.so + +CFLAGS += -Os -Wall -Werror + +all: $(TARGETS) + +%.pic.o: %.c + $(CC) $(CFLAGS) -fPIC -c -o $@ $< + +lws-client.so: lws-client.pic.o + $(CC) $(LDFLAGS) -shared -o $@ $^ -lwebsockets + +clean: + rm -f *.o $(TARGETS) diff --git a/quickjs-websocket/src/lws-client.c b/quickjs-websocket/src/lws-client.c new file mode 100644 index 000000000..d187f14fd --- /dev/null +++ b/quickjs-websocket/src/lws-client.c @@ -0,0 +1,958 @@ +/* + * Copyright (c) 2020 Genexis B.V. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#define countof(x) (sizeof(x) / sizeof((x)[0])) + +#define CDEF(name) JS_PROP_INT32_DEF(#name, name, JS_PROP_CONFIGURABLE) + +#if LWS_LIBRARY_VERSION_NUMBER < 3002000 +#define MAX_WAIT 1000 +#else +#define MAX_WAIT INT32_MAX +#ifndef LWS_WITH_EXTERNAL_POLL +#error "LWS_WITH_EXTERNAL_POLL is needed for LWS versions >= 3.2.0" +#endif +#if LWS_LIBRARY_VERSION_NUMBER < 4001002 +#error "External poll is broken for 3.2.0 <= LWS version < 4.1.2" +#endif +#endif + +#define WSI_DATA_USE_OBJECT (1 << 0) +#define WSI_DATA_USE_LINKED (1 << 1) + +typedef struct js_lws_wsi_data { + struct js_lws_wsi_data *next; + struct lws *wsi; + JSValue object; + JSValue context; + JSValue user; + uint8_t in_use; +} js_lws_wsi_data_t; + +typedef struct { + struct lws_context *context; + JSContext *ctx; + JSValue callback; + js_lws_wsi_data_t *wsi_list; +} js_lws_context_data_t; + +static JSClassID js_lws_context_class_id; +static JSClassID js_lws_wsi_class_id; + +static void free_wsi_data_rt(JSRuntime *rt, js_lws_wsi_data_t *data) +{ + JS_FreeValueRT(rt, data->object); + JS_FreeValueRT(rt, data->context); + JS_FreeValueRT(rt, data->user); + js_free_rt(rt, data); +} + +static void unlink_wsi_rt(JSRuntime *rt, js_lws_context_data_t *data, + js_lws_wsi_data_t *wsi_data) +{ + js_lws_wsi_data_t **p; + for (p = &data->wsi_list; *p; p = &(*p)->next) { + if (*p == wsi_data) { + *p = (*p)->next; + break; + } + } + wsi_data->next = NULL; + wsi_data->wsi = NULL; + JS_FreeValueRT(rt, wsi_data->object); + wsi_data->object = JS_UNDEFINED; + wsi_data->in_use &= ~WSI_DATA_USE_LINKED; + if (wsi_data->in_use == 0) + free_wsi_data_rt(rt, wsi_data); +} + +static void unlink_wsi(JSContext *ctx, js_lws_context_data_t *data, + js_lws_wsi_data_t *wsi_data) +{ + unlink_wsi_rt(JS_GetRuntime(ctx), data, wsi_data); +} + +static JSValue convert_pollargs(JSContext *ctx, const struct lws_pollargs *pa) +{ + JSValue obj; + + if (pa == NULL) + return JS_NULL; + + obj = JS_NewObject(ctx); + if (JS_IsException(obj)) + return obj; + + if (JS_SetPropertyStr(ctx, obj, "fd", JS_NewInt32(ctx, pa->fd)) < 0 + || JS_SetPropertyStr(ctx, obj, "events", + JS_NewInt32(ctx, pa->events)) < 0 + || JS_SetPropertyStr(ctx, obj, "prev_events", + JS_NewInt32(ctx, pa->prev_events)) < 0) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + + return obj; +} + +static JSValue convert_close_payload(JSContext *ctx, + uint8_t *payload, size_t len) +{ + JSValue array; + uint16_t status; + JSValue reason; + + if (payload == NULL || len < 2) + return JS_NULL; + + array = JS_NewArray(ctx); + if (JS_IsException(array)) + return array; + + status = (payload[0] << 8) | payload[1]; + if (JS_SetPropertyUint32(ctx, array, 0, JS_NewInt32(ctx, status)) < 0) { + JS_FreeValue(ctx, array); + return JS_EXCEPTION; + } + + reason = JS_NewStringLen(ctx, (const char *)(payload + 2), len - 2); + if (JS_IsException(reason) + || JS_SetPropertyUint32(ctx, array, 1, reason) < 0) { + JS_FreeValue(ctx, array); + return JS_EXCEPTION; + } + + return array; +} + +static int client_callback(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + js_lws_context_data_t *data = lws_context_user(lws_get_context(wsi)); + js_lws_wsi_data_t *wsi_data = lws_wsi_user(wsi); + JSContext *ctx; + JSValue args[3]; + JSValue ret; + int32_t ret_int; + int i; + + if (data == NULL || data->ctx == NULL || JS_IsUndefined(data->callback)) + return 0; + + ctx = data->ctx; + args[0] = wsi_data ? JS_DupValue(ctx, wsi_data->object) : JS_NULL; + args[1] = JS_NewInt32(ctx, reason); + args[2] = JS_NULL; + + switch (reason) { + case LWS_CALLBACK_ADD_POLL_FD: + case LWS_CALLBACK_DEL_POLL_FD: + case LWS_CALLBACK_CHANGE_MODE_POLL_FD: + args[2] = convert_pollargs(ctx, in); + break; + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + if (in) + args[2] = JS_NewStringLen(ctx, in, len); + break; + case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: + args[2] = convert_close_payload(ctx, in, len); + break; + case LWS_CALLBACK_RECEIVE: + case LWS_CALLBACK_CLIENT_RECEIVE: + if (in) + args[2] = JS_NewArrayBufferCopy(ctx, in, len); + break; + case LWS_CALLBACK_WSI_DESTROY: + if (wsi_data) + unlink_wsi(ctx, data, wsi_data); + break; + default: + break; + } + + if (JS_IsException(args[2])) + ret = JS_EXCEPTION; + else + ret = JS_Call(ctx, data->callback, JS_UNDEFINED, countof(args), args); + + if (JS_IsException(ret) || JS_ToInt32(ctx, &ret_int, ret) < 0) { + js_std_dump_error(ctx); + ret_int = -1; + } + + JS_FreeValue(ctx, ret); + for (i = 0; i < countof(args); i++) { + JS_FreeValue(ctx, args[i]); + } + + return ret_int; +} + +static const struct lws_protocols client_protocols[] = { + { "lws-client", client_callback, 0, 0, 0, NULL, 0 }, + { NULL, NULL, 0, 0, 0, NULL, 0 } +}; + +static JSValue js_decode_utf8(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + size_t size; + uint8_t *ptr = JS_GetArrayBuffer(ctx, &size, argv[0]); + if (ptr == NULL) + return JS_EXCEPTION; + return JS_NewStringLen(ctx, (const char *)ptr, size); +} + +static JSValue js_lws_set_log_level(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int32_t level; + if (JS_ToInt32(ctx, &level, argv[0]) < 0) + return JS_EXCEPTION; + lws_set_log_level(level, NULL); + return JS_UNDEFINED; +} + +static JSValue js_lws_create_context(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int secure; + JSValue obj; + js_lws_context_data_t *data; + struct lws_context_creation_info info; + struct lws_context *context; + + if (!JS_IsFunction(ctx, argv[0])) + return JS_ThrowTypeError(ctx, "not a function"); + + secure = JS_ToBool(ctx, argv[1]); + if (secure < 0) + return JS_EXCEPTION; + + obj = JS_NewObjectClass(ctx, js_lws_context_class_id); + if (JS_IsException(obj)) + return obj; + + data = js_mallocz(ctx, sizeof(js_lws_context_data_t)); + if (data == NULL) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } + data->callback = JS_UNDEFINED; + + memset(&info, 0, sizeof(info)); + info.port = CONTEXT_PORT_NO_LISTEN; + info.protocols = client_protocols; + info.gid = -1; + info.uid = -1; + info.options = secure ? LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT : 0; + info.user = data; + context = lws_create_context(&info); + if (context == NULL) { + JS_FreeValue(ctx, obj); + js_free(ctx, data); + return JS_ThrowOutOfMemory(ctx); + } + + data->context = context; + data->ctx = JS_DupContext(ctx); + data->callback = JS_DupValue(ctx, argv[0]); + JS_SetOpaque(obj, data); + + return obj; +} + +static void js_lws_context_finalizer(JSRuntime *rt, JSValue val) +{ + js_lws_context_data_t *data = JS_GetOpaque(val, js_lws_context_class_id); + if (data) { + JS_FreeContext(data->ctx); + data->ctx = NULL; + JS_FreeValueRT(rt, data->callback); + data->callback = JS_UNDEFINED; + + lws_context_destroy(data->context); + + while (data->wsi_list) { + unlink_wsi_rt(rt, data, data->wsi_list); + } + + js_free_rt(rt, data); + } +} + +static void js_lws_context_mark(JSRuntime *rt, JSValue val, + JS_MarkFunc *mark_func) +{ + js_lws_context_data_t *data = JS_GetOpaque(val, js_lws_context_class_id); + if (data) { + js_lws_wsi_data_t *wd; + mark_func(rt, (JSGCObjectHeader *)data->ctx); + JS_MarkValue(rt, data->callback, mark_func); + for (wd = data->wsi_list; wd; wd = wd->next) { + JS_MarkValue(rt, wd->object, mark_func); + } + } +} + +static JSValue js_lws_context_get_connections(JSContext *ctx, + JSValueConst this_val) +{ + js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val, + js_lws_context_class_id); + int32_t connections = 0; + js_lws_wsi_data_t *wd; + + if (data == NULL) + return JS_EXCEPTION; + + for (wd = data->wsi_list; wd; wd = wd->next) { + connections++; + } + + return JS_NewInt32(ctx, connections); +} + +static JSValue js_lws_client_connect(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val, + js_lws_context_class_id); + const char *address = NULL; + int32_t port; + int secure; + const char *path = NULL, *host = NULL, *origin = NULL, *protocol = NULL; + JSValue obj = JS_UNDEFINED, ret = JS_EXCEPTION; + js_lws_wsi_data_t *wsi_data; + struct lws_client_connect_info info; + + if (data == NULL) + return JS_EXCEPTION; + + address = JS_ToCString(ctx, argv[0]); + if (address == NULL) + goto exception; + + if (JS_ToInt32(ctx, &port, argv[1]) < 0) + goto exception; + + if (port < 1 || port > 65535) { + JS_ThrowRangeError(ctx, "port must be between 1 and 65535"); + goto exception; + } + + secure = JS_ToBool(ctx, argv[2]); + if (secure < 0) + goto exception; + + path = JS_ToCString(ctx, argv[3]); + if (path == NULL) + goto exception; + + host = JS_ToCString(ctx, argv[4]); + if (host == NULL) + goto exception; + + if (!JS_IsUndefined(argv[5]) && !JS_IsNull(argv[5])) { + origin = JS_ToCString(ctx, argv[5]); + if (origin == NULL) + goto exception; + } + + if (!JS_IsUndefined(argv[6]) && !JS_IsNull(argv[6])) { + protocol = JS_ToCString(ctx, argv[6]); + if (protocol == NULL) + goto exception; + } + + obj = JS_NewObjectClass(ctx, js_lws_wsi_class_id); + if (JS_IsException(obj)) + goto exception; + + wsi_data = js_mallocz(ctx, sizeof(js_lws_wsi_data_t)); + if (wsi_data == NULL) + goto exception; + wsi_data->next = data->wsi_list; + wsi_data->object = JS_DupValue(ctx, obj); + wsi_data->context = JS_DupValue(ctx, this_val); + wsi_data->user = JS_DupValue(ctx, argv[7]); + wsi_data->in_use = WSI_DATA_USE_OBJECT | WSI_DATA_USE_LINKED; + data->wsi_list = wsi_data; + JS_SetOpaque(obj, wsi_data); + + memset(&info, 0, sizeof(info)); + info.context = data->context; + info.address = address; + info.port = port; + info.ssl_connection = secure ? LCCSCF_USE_SSL : 0; + info.local_protocol_name = "lws-client"; + info.path = path; + info.host = host; + info.origin = origin; + info.protocol = protocol; + info.ietf_version_or_minus_one = -1; + info.userdata = wsi_data; + info.pwsi = &wsi_data->wsi; + lws_client_connect_via_info(&info); + + if (wsi_data->wsi) { + ret = JS_DupValue(ctx, obj); + } else { + unlink_wsi(ctx, data, wsi_data); + JS_ThrowReferenceError(ctx, "cannot connect to [%s]:%d (%s)", + address, port, secure ? "wss" : "ws"); + } + +exception: + JS_FreeCString(ctx, address); + JS_FreeCString(ctx, path); + JS_FreeCString(ctx, host); + JS_FreeCString(ctx, origin); + JS_FreeCString(ctx, protocol); + JS_FreeValue(ctx, obj); + + return ret; +} + +static JSValue js_lws_service_fd(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val, + js_lws_context_class_id); + int32_t fd, events, revents; + struct lws_pollfd pfd; + + if (data == NULL) + return JS_EXCEPTION; + + if (JS_ToInt32(ctx, &fd, argv[0]) < 0) + return JS_EXCEPTION; + + if (JS_ToInt32(ctx, &events, argv[1]) < 0) + return JS_EXCEPTION; + + if (JS_ToInt32(ctx, &revents, argv[2]) < 0) + return JS_EXCEPTION; + + pfd.fd = fd; + pfd.events = events; + pfd.revents = revents; + lws_service_fd(data->context, &pfd); + + return JS_NewInt32(ctx, + lws_service_adjust_timeout(data->context, MAX_WAIT, 0)); +} + +static JSValue js_lws_service_periodic(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val, + js_lws_context_class_id); + int timeout; + + if (data == NULL) + return JS_EXCEPTION; + +#if LWS_LIBRARY_VERSION_NUMBER < 3002000 + lws_service_fd(data->context, NULL); +#endif + + timeout = lws_service_adjust_timeout(data->context, MAX_WAIT, 0); + if (timeout == 0) { + lws_service(data->context, -1); + timeout = lws_service_adjust_timeout(data->context, MAX_WAIT, 0); + } + + return JS_NewInt32(ctx, timeout); +} + +static void js_lws_wsi_finalizer(JSRuntime *rt, JSValue val) +{ + js_lws_wsi_data_t *data = JS_GetOpaque(val, js_lws_wsi_class_id); + if (data) { + JS_FreeValueRT(rt, data->context); + data->context = JS_UNDEFINED; + JS_FreeValueRT(rt, data->user); + data->user = JS_UNDEFINED; + + data->in_use &= ~WSI_DATA_USE_OBJECT; + if (data->in_use == 0) + free_wsi_data_rt(rt, data); + } +} + +static void js_lws_wsi_mark(JSRuntime *rt, JSValue val, + JS_MarkFunc *mark_func) +{ + js_lws_wsi_data_t *data = JS_GetOpaque(val, js_lws_wsi_class_id); + if (data) { + JS_MarkValue(rt, data->context, mark_func); + JS_MarkValue(rt, data->user, mark_func); + } +} + +static JSValue js_lws_wsi_get_context(JSContext *ctx, JSValueConst this_val) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + if (data == NULL) + return JS_EXCEPTION; + return JS_DupValue(ctx, data->context); +} + +static JSValue js_lws_wsi_get_user(JSContext *ctx, JSValueConst this_val) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + if (data == NULL) + return JS_EXCEPTION; + return JS_DupValue(ctx, data->user); +} + +static JSValue js_lws_wsi_get_hdr(JSContext *ctx, JSValueConst this_val, + int magic) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + int len; + char *str; + JSValue ret; + + if (data == NULL) + return JS_EXCEPTION; + + if (data->wsi == NULL) + return JS_ThrowTypeError(ctx, "defunct WSI"); + + len = lws_hdr_total_length(data->wsi, magic); + if (len < 0) + return JS_ThrowReferenceError(ctx, "HTTP headers unavailable"); + + len++; + str = js_mallocz(ctx, len); + if (str == NULL) + return JS_EXCEPTION; + + len = lws_hdr_copy(data->wsi, str, len, magic); + if (len < 0) + ret = JS_ThrowReferenceError(ctx, "HTTP headers unavailable"); + else + ret = JS_NewStringLen(ctx, str, len); + + js_free(ctx, str); + + return ret; +} + +static JSValue js_lws_is_final_fragment(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + if (data == NULL) + return JS_EXCEPTION; + if (data->wsi == NULL) + return JS_ThrowTypeError(ctx, "defunct WSI"); + return JS_NewBool(ctx, lws_is_final_fragment(data->wsi)); +} + +static JSValue js_lws_is_first_fragment(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + if (data == NULL) + return JS_EXCEPTION; + if (data->wsi == NULL) + return JS_ThrowTypeError(ctx, "defunct WSI"); + return JS_NewBool(ctx, lws_is_first_fragment(data->wsi)); +} + +static JSValue js_lws_frame_is_binary(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + if (data == NULL) + return JS_EXCEPTION; + if (data->wsi == NULL) + return JS_ThrowTypeError(ctx, "defunct WSI"); + return JS_NewBool(ctx, lws_frame_is_binary(data->wsi)); +} + +static JSValue js_lws_callback_on_writable(JSContext *ctx, + JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + if (data == NULL) + return JS_EXCEPTION; + if (data->wsi == NULL) + return JS_ThrowTypeError(ctx, "defunct WSI"); + lws_callback_on_writable(data->wsi); + return JS_UNDEFINED; +} + +static JSValue js_lws_write(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + const char *str = NULL; + const uint8_t *ptr; + uint8_t *buf; + size_t size; + enum lws_write_protocol protocol; + int ret; + + if (data == NULL) + return JS_EXCEPTION; + + if (data->wsi == NULL) + return JS_ThrowTypeError(ctx, "defunct WSI"); + + if (JS_IsString(argv[0])) { + str = JS_ToCStringLen(ctx, &size, argv[0]); + if (str == NULL) + return JS_EXCEPTION; + ptr = (const uint8_t *)str; + protocol = LWS_WRITE_TEXT; + } else { + ptr = JS_GetArrayBuffer(ctx, &size, argv[0]); + if (ptr == NULL) + return JS_EXCEPTION; + protocol = LWS_WRITE_BINARY; + } + + buf = js_malloc(ctx, LWS_PRE + size); + if (buf) + memcpy(buf + LWS_PRE, ptr, size); + if (str) + JS_FreeCString(ctx, str); + if (buf == NULL) + return JS_EXCEPTION; + ret = lws_write(data->wsi, buf + LWS_PRE, size, protocol); + js_free(ctx, buf); + + if (ret < 0) + return JS_ThrowTypeError(ctx, "WSI not writable"); + + return JS_UNDEFINED; +} + +static JSValue js_lws_close_reason(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id); + int32_t status; + const char *reason = NULL; + size_t len = 0; + + if (data == NULL) + return JS_EXCEPTION; + + if (data->wsi == NULL) + return JS_ThrowTypeError(ctx, "defunct WSI"); + + if (JS_ToInt32(ctx, &status, argv[0]) < 0) + return JS_EXCEPTION; + + if (status < 0 || status > 65535) + return JS_ThrowRangeError(ctx, "status must be between 0 and 65535"); + + if (!JS_IsUndefined(argv[1])) { + reason = JS_ToCStringLen(ctx, &len, argv[1]); + if (reason == NULL) + return JS_EXCEPTION; + if (len > 123) { + JS_FreeCString(ctx, reason); + return JS_ThrowTypeError(ctx, "reason too long (%zu > 123)", len); + } + } + + lws_close_reason(data->wsi, status, (uint8_t *)reason, len); + + if (reason) + JS_FreeCString(ctx, reason); + + return JS_UNDEFINED; +} + +static const JSCFunctionListEntry js_lws_funcs[] = { + CDEF(LLL_ERR), + CDEF(LLL_WARN), + CDEF(LLL_NOTICE), + CDEF(LLL_INFO), + CDEF(LLL_DEBUG), + CDEF(LLL_PARSER), + CDEF(LLL_HEADER), + CDEF(LLL_EXT), + CDEF(LLL_CLIENT), + CDEF(LLL_LATENCY), + CDEF(LLL_USER), + CDEF(LLL_THREAD), + CDEF(LLL_COUNT), + CDEF(LWS_CALLBACK_PROTOCOL_INIT), + CDEF(LWS_CALLBACK_PROTOCOL_DESTROY), + CDEF(LWS_CALLBACK_WSI_CREATE), + CDEF(LWS_CALLBACK_WSI_DESTROY), + CDEF(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS), + CDEF(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS), + CDEF(LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION), + CDEF(LWS_CALLBACK_OPENSSL_CONTEXT_REQUIRES_PRIVATE_KEY), + CDEF(LWS_CALLBACK_SSL_INFO), + CDEF(LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION), + CDEF(LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED), + CDEF(LWS_CALLBACK_HTTP), + CDEF(LWS_CALLBACK_HTTP_BODY), + CDEF(LWS_CALLBACK_HTTP_BODY_COMPLETION), + CDEF(LWS_CALLBACK_HTTP_FILE_COMPLETION), + CDEF(LWS_CALLBACK_HTTP_WRITEABLE), + CDEF(LWS_CALLBACK_CLOSED_HTTP), + CDEF(LWS_CALLBACK_FILTER_HTTP_CONNECTION), + CDEF(LWS_CALLBACK_ADD_HEADERS), + CDEF(LWS_CALLBACK_CHECK_ACCESS_RIGHTS), + CDEF(LWS_CALLBACK_PROCESS_HTML), + CDEF(LWS_CALLBACK_HTTP_BIND_PROTOCOL), + CDEF(LWS_CALLBACK_HTTP_DROP_PROTOCOL), + CDEF(LWS_CALLBACK_HTTP_CONFIRM_UPGRADE), + CDEF(LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP), + CDEF(LWS_CALLBACK_CLOSED_CLIENT_HTTP), + CDEF(LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ), + CDEF(LWS_CALLBACK_RECEIVE_CLIENT_HTTP), + CDEF(LWS_CALLBACK_COMPLETED_CLIENT_HTTP), + CDEF(LWS_CALLBACK_CLIENT_HTTP_WRITEABLE), + CDEF(LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL), + CDEF(LWS_CALLBACK_CLIENT_HTTP_DROP_PROTOCOL), + CDEF(LWS_CALLBACK_ESTABLISHED), + CDEF(LWS_CALLBACK_CLOSED), + CDEF(LWS_CALLBACK_SERVER_WRITEABLE), + CDEF(LWS_CALLBACK_RECEIVE), + CDEF(LWS_CALLBACK_RECEIVE_PONG), + CDEF(LWS_CALLBACK_WS_PEER_INITIATED_CLOSE), + CDEF(LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION), + CDEF(LWS_CALLBACK_CONFIRM_EXTENSION_OKAY), + CDEF(LWS_CALLBACK_WS_SERVER_BIND_PROTOCOL), + CDEF(LWS_CALLBACK_WS_SERVER_DROP_PROTOCOL), + CDEF(LWS_CALLBACK_CLIENT_CONNECTION_ERROR), + CDEF(LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH), + CDEF(LWS_CALLBACK_CLIENT_ESTABLISHED), + CDEF(LWS_CALLBACK_CLIENT_CLOSED), + CDEF(LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER), + CDEF(LWS_CALLBACK_CLIENT_RECEIVE), + CDEF(LWS_CALLBACK_CLIENT_RECEIVE_PONG), + CDEF(LWS_CALLBACK_CLIENT_WRITEABLE), + CDEF(LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED), + CDEF(LWS_CALLBACK_WS_EXT_DEFAULTS), + CDEF(LWS_CALLBACK_FILTER_NETWORK_CONNECTION), + CDEF(LWS_CALLBACK_WS_CLIENT_BIND_PROTOCOL), + CDEF(LWS_CALLBACK_WS_CLIENT_DROP_PROTOCOL), + CDEF(LWS_CALLBACK_GET_THREAD_ID), + CDEF(LWS_CALLBACK_ADD_POLL_FD), + CDEF(LWS_CALLBACK_DEL_POLL_FD), + CDEF(LWS_CALLBACK_CHANGE_MODE_POLL_FD), + CDEF(LWS_CALLBACK_LOCK_POLL), + CDEF(LWS_CALLBACK_UNLOCK_POLL), + CDEF(LWS_CALLBACK_CGI), + CDEF(LWS_CALLBACK_CGI_TERMINATED), + CDEF(LWS_CALLBACK_CGI_STDIN_DATA), + CDEF(LWS_CALLBACK_CGI_STDIN_COMPLETED), + CDEF(LWS_CALLBACK_CGI_PROCESS_ATTACH), + CDEF(LWS_CALLBACK_SESSION_INFO), + CDEF(LWS_CALLBACK_GS_EVENT), + CDEF(LWS_CALLBACK_HTTP_PMO), + CDEF(LWS_CALLBACK_RAW_RX), + CDEF(LWS_CALLBACK_RAW_CLOSE), + CDEF(LWS_CALLBACK_RAW_WRITEABLE), + CDEF(LWS_CALLBACK_RAW_ADOPT), + CDEF(LWS_CALLBACK_RAW_SKT_BIND_PROTOCOL), + CDEF(LWS_CALLBACK_RAW_SKT_DROP_PROTOCOL), + CDEF(LWS_CALLBACK_RAW_ADOPT_FILE), + CDEF(LWS_CALLBACK_RAW_RX_FILE), + CDEF(LWS_CALLBACK_RAW_WRITEABLE_FILE), + CDEF(LWS_CALLBACK_RAW_CLOSE_FILE), + CDEF(LWS_CALLBACK_RAW_FILE_BIND_PROTOCOL), + CDEF(LWS_CALLBACK_RAW_FILE_DROP_PROTOCOL), + CDEF(LWS_CALLBACK_TIMER), + CDEF(LWS_CALLBACK_EVENT_WAIT_CANCELLED), + CDEF(LWS_CALLBACK_CHILD_CLOSING), + CDEF(LWS_CALLBACK_VHOST_CERT_AGING), + CDEF(LWS_CALLBACK_VHOST_CERT_UPDATE), + CDEF(LWS_CALLBACK_USER), + CDEF(LWS_POLLHUP), + CDEF(LWS_POLLIN), + CDEF(LWS_POLLOUT), + JS_CFUNC_DEF("decode_utf8", 1, js_decode_utf8), + JS_CFUNC_DEF("set_log_level", 1, js_lws_set_log_level), + JS_CFUNC_DEF("create_context", 2, js_lws_create_context), +}; + +static const JSClassDef js_lws_context_class = { + "Context", + .finalizer = js_lws_context_finalizer, + .gc_mark = js_lws_context_mark, +}; + +static const JSCFunctionListEntry js_lws_context_proto_funcs[] = { + JS_CGETSET_DEF("connections", js_lws_context_get_connections, NULL), + JS_CFUNC_DEF("client_connect", 8, js_lws_client_connect), + JS_CFUNC_DEF("service_fd", 3, js_lws_service_fd), + JS_CFUNC_DEF("service_periodic", 0, js_lws_service_periodic), +}; + +static const JSClassDef js_lws_wsi_class = { + "WSI", + .finalizer = js_lws_wsi_finalizer, + .gc_mark = js_lws_wsi_mark, +}; + +#define HDRGET(name) JS_CGETSET_MAGIC_DEF(#name, js_lws_wsi_get_hdr, NULL, name) + +static const JSCFunctionListEntry js_lws_wsi_proto_funcs[] = { + JS_CGETSET_DEF("context", js_lws_wsi_get_context, NULL), + JS_CGETSET_DEF("user", js_lws_wsi_get_user, NULL), + HDRGET(WSI_TOKEN_GET_URI), + HDRGET(WSI_TOKEN_POST_URI), + HDRGET(WSI_TOKEN_OPTIONS_URI), + HDRGET(WSI_TOKEN_HOST), + HDRGET(WSI_TOKEN_CONNECTION), + HDRGET(WSI_TOKEN_UPGRADE), + HDRGET(WSI_TOKEN_ORIGIN), + HDRGET(WSI_TOKEN_DRAFT), + HDRGET(WSI_TOKEN_CHALLENGE), + HDRGET(WSI_TOKEN_EXTENSIONS), + HDRGET(WSI_TOKEN_KEY1), + HDRGET(WSI_TOKEN_KEY2), + HDRGET(WSI_TOKEN_PROTOCOL), + HDRGET(WSI_TOKEN_ACCEPT), + HDRGET(WSI_TOKEN_NONCE), + HDRGET(WSI_TOKEN_HTTP), + HDRGET(WSI_TOKEN_HTTP2_SETTINGS), + HDRGET(WSI_TOKEN_HTTP_ACCEPT), + HDRGET(WSI_TOKEN_HTTP_AC_REQUEST_HEADERS), + HDRGET(WSI_TOKEN_HTTP_IF_MODIFIED_SINCE), + HDRGET(WSI_TOKEN_HTTP_IF_NONE_MATCH), + HDRGET(WSI_TOKEN_HTTP_ACCEPT_ENCODING), + HDRGET(WSI_TOKEN_HTTP_ACCEPT_LANGUAGE), + HDRGET(WSI_TOKEN_HTTP_PRAGMA), + HDRGET(WSI_TOKEN_HTTP_CACHE_CONTROL), + HDRGET(WSI_TOKEN_HTTP_AUTHORIZATION), + HDRGET(WSI_TOKEN_HTTP_COOKIE), + HDRGET(WSI_TOKEN_HTTP_CONTENT_LENGTH), + HDRGET(WSI_TOKEN_HTTP_CONTENT_TYPE), + HDRGET(WSI_TOKEN_HTTP_DATE), + HDRGET(WSI_TOKEN_HTTP_RANGE), + HDRGET(WSI_TOKEN_HTTP_REFERER), + HDRGET(WSI_TOKEN_KEY), + HDRGET(WSI_TOKEN_VERSION), + HDRGET(WSI_TOKEN_SWORIGIN), + HDRGET(WSI_TOKEN_HTTP_COLON_AUTHORITY), + HDRGET(WSI_TOKEN_HTTP_COLON_METHOD), + HDRGET(WSI_TOKEN_HTTP_COLON_PATH), + HDRGET(WSI_TOKEN_HTTP_COLON_SCHEME), + HDRGET(WSI_TOKEN_HTTP_COLON_STATUS), + HDRGET(WSI_TOKEN_HTTP_ACCEPT_CHARSET), + HDRGET(WSI_TOKEN_HTTP_ACCEPT_RANGES), + HDRGET(WSI_TOKEN_HTTP_ACCESS_CONTROL_ALLOW_ORIGIN), + HDRGET(WSI_TOKEN_HTTP_AGE), + HDRGET(WSI_TOKEN_HTTP_ALLOW), + HDRGET(WSI_TOKEN_HTTP_CONTENT_DISPOSITION), + HDRGET(WSI_TOKEN_HTTP_CONTENT_ENCODING), + HDRGET(WSI_TOKEN_HTTP_CONTENT_LANGUAGE), + HDRGET(WSI_TOKEN_HTTP_CONTENT_LOCATION), + HDRGET(WSI_TOKEN_HTTP_CONTENT_RANGE), + HDRGET(WSI_TOKEN_HTTP_ETAG), + HDRGET(WSI_TOKEN_HTTP_EXPECT), + HDRGET(WSI_TOKEN_HTTP_EXPIRES), + HDRGET(WSI_TOKEN_HTTP_FROM), + HDRGET(WSI_TOKEN_HTTP_IF_MATCH), + HDRGET(WSI_TOKEN_HTTP_IF_RANGE), + HDRGET(WSI_TOKEN_HTTP_IF_UNMODIFIED_SINCE), + HDRGET(WSI_TOKEN_HTTP_LAST_MODIFIED), + HDRGET(WSI_TOKEN_HTTP_LINK), + HDRGET(WSI_TOKEN_HTTP_LOCATION), + HDRGET(WSI_TOKEN_HTTP_MAX_FORWARDS), + HDRGET(WSI_TOKEN_HTTP_PROXY_AUTHENTICATE), + HDRGET(WSI_TOKEN_HTTP_PROXY_AUTHORIZATION), + HDRGET(WSI_TOKEN_HTTP_REFRESH), + HDRGET(WSI_TOKEN_HTTP_RETRY_AFTER), + HDRGET(WSI_TOKEN_HTTP_SERVER), + HDRGET(WSI_TOKEN_HTTP_SET_COOKIE), + HDRGET(WSI_TOKEN_HTTP_STRICT_TRANSPORT_SECURITY), + HDRGET(WSI_TOKEN_HTTP_TRANSFER_ENCODING), + HDRGET(WSI_TOKEN_HTTP_USER_AGENT), + HDRGET(WSI_TOKEN_HTTP_VARY), + HDRGET(WSI_TOKEN_HTTP_VIA), + HDRGET(WSI_TOKEN_HTTP_WWW_AUTHENTICATE), + HDRGET(WSI_TOKEN_PATCH_URI), + HDRGET(WSI_TOKEN_PUT_URI), + HDRGET(WSI_TOKEN_DELETE_URI), + HDRGET(WSI_TOKEN_HTTP_URI_ARGS), + HDRGET(WSI_TOKEN_PROXY), + HDRGET(WSI_TOKEN_HTTP_X_REAL_IP), + HDRGET(WSI_TOKEN_HTTP1_0), + HDRGET(WSI_TOKEN_X_FORWARDED_FOR), + HDRGET(WSI_TOKEN_CONNECT), + HDRGET(WSI_TOKEN_HEAD_URI), + HDRGET(WSI_TOKEN_TE), + HDRGET(WSI_TOKEN_REPLAY_NONCE), + HDRGET(WSI_TOKEN_COLON_PROTOCOL), + HDRGET(WSI_TOKEN_X_AUTH_TOKEN), + JS_CFUNC_DEF("is_final_fragment", 0, js_lws_is_final_fragment), + JS_CFUNC_DEF("is_first_fragment", 0, js_lws_is_first_fragment), + JS_CFUNC_DEF("frame_is_binary", 0, js_lws_frame_is_binary), + JS_CFUNC_DEF("callback_on_writable", 0, js_lws_callback_on_writable), + JS_CFUNC_DEF("write", 1, js_lws_write), + JS_CFUNC_DEF("close_reason", 2, js_lws_close_reason), +}; + +static int js_lws_init(JSContext *ctx, JSModuleDef *m) +{ + JSValue proto; + + JS_NewClassID(&js_lws_context_class_id); + JS_NewClass(JS_GetRuntime(ctx), js_lws_context_class_id, + &js_lws_context_class); + proto = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, proto, js_lws_context_proto_funcs, + countof(js_lws_context_proto_funcs)); + JS_SetClassProto(ctx, js_lws_context_class_id, proto); + + JS_NewClassID(&js_lws_wsi_class_id); + JS_NewClass(JS_GetRuntime(ctx), js_lws_wsi_class_id, &js_lws_wsi_class); + proto = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, proto, js_lws_wsi_proto_funcs, + countof(js_lws_wsi_proto_funcs)); + JS_SetClassProto(ctx, js_lws_wsi_class_id, proto); + + return JS_SetModuleExportList(ctx, m, js_lws_funcs, countof(js_lws_funcs)); +} + +JSModuleDef *js_init_module(JSContext *ctx, const char *module_name) +{ + JSModuleDef *m = JS_NewCModule(ctx, module_name, js_lws_init); + if (m == NULL) + return NULL; + JS_AddModuleExportList(ctx, m, js_lws_funcs, countof(js_lws_funcs)); + return m; +} diff --git a/quickjs-websocket/src/websocket.js b/quickjs-websocket/src/websocket.js new file mode 100644 index 000000000..1b35dbed4 --- /dev/null +++ b/quickjs-websocket/src/websocket.js @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2020 Genexis B.V. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import * as os from 'os' +import * as lws from './lws-client.so' + +const CONNECTING = 0 +const OPEN = 1 +const CLOSING = 2 +const CLOSED = 3 + +const CLOSING1 = 0x10 | CLOSING +const CLOSING2 = 0x20 | CLOSING + +function serviceScheduler (context) { + let running = false + let timeout = null + + function schedule (time) { + if (timeout) os.clearTimeout(timeout) + timeout = running ? os.setTimeout(callback, time) : null + } + + function callback () { + schedule(context.service_periodic()) + } + + return { + start: function () { + running = true + schedule(0) + }, + stop: function () { + running = false + schedule(0) + }, + reschedule: schedule + } +} + +function fdHandler (fd, events, revents) { + return function () { + service.reschedule(context.service_fd(fd, events, revents)) + } +} + +function contextCallback (wsi, reason, arg) { + switch (reason) { + case lws.LWS_CALLBACK_ADD_POLL_FD: + service.start() + // fallthrough + case lws.LWS_CALLBACK_CHANGE_MODE_POLL_FD: + os.setReadHandler( + arg.fd, + (arg.events & lws.LWS_POLLIN) + ? fdHandler(arg.fd, arg.events, lws.LWS_POLLIN) + : null + ) + os.setWriteHandler( + arg.fd, + (arg.events & lws.LWS_POLLOUT) + ? fdHandler(arg.fd, arg.events, lws.LWS_POLLOUT) + : null + ) + break + case lws.LWS_CALLBACK_DEL_POLL_FD: + os.setReadHandler(arg.fd, null) + os.setWriteHandler(arg.fd, null) + break + case lws.LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + wsi.user.error(typeof arg === 'string' ? arg : '') + break + case lws.LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH: + if (wsi.user.readyState !== CONNECTING) { + return -1 + } + wsi.user.protocol = wsi.WSI_TOKEN_PROTOCOL + wsi.user.extensions = wsi.WSI_TOKEN_EXTENSIONS + break + case lws.LWS_CALLBACK_CLIENT_ESTABLISHED: + if (wsi.user.readyState !== CONNECTING) { + return -1 + } + wsi.user.wsi = wsi + wsi.user.open() + break + case lws.LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: + if (wsi.user.readyState === CLOSED) { + return -1 + } + if (arg instanceof Array) { + wsi.user.closeEvent.code = arg[0] + wsi.user.closeEvent.reason = arg[1] + } else { + wsi.user.closeEvent.code = 1005 + wsi.user.closeEvent.reason = '' + } + wsi.user.readyState = CLOSING2 + break + case lws.LWS_CALLBACK_CLIENT_CLOSED: + wsi.user.close() + break + case lws.LWS_CALLBACK_CLIENT_RECEIVE: + if (!(arg instanceof ArrayBuffer) || + wsi.user.readyState === CONNECTING || + wsi.user.readyState === CLOSED) { + return -1 + } + if (wsi.is_first_fragment()) { + wsi.user.inbuf = [] + } + wsi.user.inbuf.push(arg) + if (wsi.is_final_fragment()) { + wsi.user.message(wsi.frame_is_binary()) + } + break + case lws.LWS_CALLBACK_CLIENT_WRITEABLE: + if ((wsi.user.readyState === OPEN || wsi.user.readyState === CLOSING1) && + wsi.user.outbuf.length > 0) { + const msg = wsi.user.outbuf.shift() + if (msg === null) { + wsi.user.readyState = CLOSING2 + return -1 + } + wsi.write(msg) + if (wsi.user.outbuf.length > 0) { + wsi.callback_on_writable() + } + } + break + case lws.LWS_CALLBACK_WSI_DESTROY: + if (wsi.context.connections === 0) service.stop() + break + } + + return 0 +} + +lws.set_log_level(lws.LLL_ERR | lws.LLL_WARN) +const context = lws.create_context(contextCallback, true) +const service = serviceScheduler(context) + +function arrayBufferJoin (bufs) { + if (!(bufs instanceof Array)) { + throw new TypeError('Array expected') + } + + if (!bufs.every(function (val) { return val instanceof ArrayBuffer })) { + throw new TypeError('ArrayBuffer expected') + } + + const len = bufs.reduce(function (acc, val) { + return acc + val.byteLength + }, 0) + const array = new Uint8Array(len) + + let offset = 0 + for (const b of bufs) { + array.set(new Uint8Array(b), offset) + offset += b.byteLength + } + + return array.buffer +} + +export function WebSocket (url, protocols) { + const pattern = /^(ws|wss):\/\/([^/?#]*)([^#]*)$/i + const match = pattern.exec(url) + if (match === null) { + throw new TypeError('invalid WebSocket URL') + } + const secure = match[1].toLowerCase() === 'wss' + const host = match[2] + const path = match[3].startsWith('/') ? match[3] : '/' + match[3] + + const hostPattern = /^(?:([a-z\d.-]+)|\[([\da-f:]+:[\da-f.]*)\])(?::(\d*))?$/i + const hostMatch = hostPattern.exec(host) + if (hostMatch === null) { + throw new TypeError('invalid WebSocket URL') + } + const address = hostMatch[1] || hostMatch[2] + const port = hostMatch[3] ? parseInt(hostMatch[3]) : (secure ? 443 : 80) + + const validPath = /^\/[A-Za-z0-9_.!~*'()%:@&=+$,;/?-]*$/ + if (!validPath.test(path)) { + throw new TypeError('invalid WebSocket URL') + } + if (!(port >= 1 && port <= 65535)) { + throw new RangeError('port must be between 1 and 65535') + } + + if (protocols === undefined) { + protocols = [] + } else if (!(protocols instanceof Array)) { + protocols = [protocols] + } + const validProto = /^[A-Za-z0-9!#$%&'*+.^_|~-]+$/ + if (!protocols.every(function (val) { return validProto.test(val) })) { + throw new TypeError('invalid WebSocket subprotocol name') + } + const proto = protocols.length > 0 ? protocols.join(', ') : null + + const self = this + const state = { + url: url, + readyState: CONNECTING, + extensions: '', + protocol: '', + onopen: null, + onerror: null, + onclose: null, + onmessage: null, + wsi: null, + inbuf: [], + outbuf: [], + closeEvent: { + type: 'close', + code: 1005, + reason: '', + wasClean: false + }, + open: function () { + if (state.readyState === CONNECTING) { + state.readyState = OPEN + if (state.onopen) { + state.onopen.call(self, { type: 'open' }) + } + } + }, + error: function (e) { + if (state.readyState !== CLOSED) { + state.closeEvent.code = 1006 + state.closeEvent.reason = String(e) + state.readyState = CLOSED + try { + if (state.onerror) { + state.onerror.call(self, { type: 'error' }) + } + } finally { + if (state.onclose) { + state.onclose.call(self, Object.assign({}, state.closeEvent)) + } + } + } + }, + close: function () { + if (state.readyState !== CLOSED) { + state.closeEvent.wasClean = true + state.readyState = CLOSED + if (state.onclose) { + state.onclose.call(self, Object.assign({}, state.closeEvent)) + } + } + }, + message: function (binary) { + if (state.inbuf.length > 0) { + const msg = state.inbuf.length === 1 + ? state.inbuf[0] + : arrayBufferJoin(state.inbuf) + state.inbuf = [] + if (state.readyState === OPEN && state.onmessage) { + state.onmessage.call(self, { + type: 'messasge', + data: binary ? msg : lws.decode_utf8(msg) + }) + } + } + } + } + this._wsState = state + + os.setTimeout(function () { + try { + context.client_connect( + address, port, secure, path, host, null, proto, state + ) + } catch (e) { + state.error(e) + } + }, 0) +} + +const readyStateConstants = { + CONNECTING: { value: CONNECTING }, + OPEN: { value: OPEN }, + CLOSING: { value: CLOSING }, + CLOSED: { value: CLOSED } +} + +Object.defineProperties(WebSocket, readyStateConstants) +Object.defineProperties(WebSocket.prototype, readyStateConstants) + +function checkNullOrFunction (val) { + if (val !== null && typeof val !== 'function') { + throw new TypeError('null or Function expected') + } +} + +Object.defineProperties(WebSocket.prototype, { + url: { get: function () { return this._wsState.url } }, + readyState: { get: function () { return this._wsState.readyState & 0xf } }, + extensions: { get: function () { return this._wsState.extensions } }, + protocol: { get: function () { return this._wsState.protocol } }, + bufferedAmount: { + get: function () { + return this._wsState.outbuf.reduce(function (acc, val) { + if (val instanceof ArrayBuffer) { + acc += val.byteLength + } else if (typeof val === 'string') { + acc += val.length + } + return acc + }, 0) + } + }, + binaryType: { + get: function () { return 'arraybuffer' }, + set: function (val) { + if (val !== 'arraybuffer') { + throw new TypeError('only "arraybuffer" allowed for "binaryType"') + } + } + }, + onopen: { + get: function () { return this._wsState.onopen }, + set: function (val) { + checkNullOrFunction(val) + this._wsState.onopen = val + } + }, + onerror: { + get: function () { return this._wsState.onerror }, + set: function (val) { + checkNullOrFunction(val) + this._wsState.onerror = val + } + }, + onclose: { + get: function () { return this._wsState.onclose }, + set: function (val) { + checkNullOrFunction(val) + this._wsState.onclose = val + } + }, + onmessage: { + get: function () { return this._wsState.onmessage }, + set: function (val) { + checkNullOrFunction(val) + this._wsState.onmessage = val + } + } +}) + +WebSocket.prototype.close = function (code, reason) { + if (code !== undefined) { + code = Math.trunc(code) + reason = reason === undefined ? '' : String(reason) + if (code !== 1000 && !(code >= 3000 && code <= 4999)) { + throw new RangeError('code must be 1000 or between 3000 and 4999') + } + } + const state = this._wsState + if (state.readyState === OPEN) { + if (code !== undefined) { + state.wsi.close_reason(code, reason) + state.closeEvent.code = code + state.closeEvent.reason = reason + } + state.readyState = CLOSING1 + state.outbuf.push(null) + state.wsi.callback_on_writable() + } else if (state.readyState === CONNECTING) { + state.readyState = CLOSING2 + } +} + +WebSocket.prototype.send = function (msg) { + const state = this._wsState + if (state.readyState === CONNECTING) { + throw new TypeError('send() not allowed in CONNECTING state') + } + if (msg instanceof ArrayBuffer) { + state.outbuf.push(msg.slice(0)) + } else if (ArrayBuffer.isView(msg)) { + state.outbuf.push( + msg.buffer.slice(msg.byteOffset, msg.byteOffset + msg.byteLength) + ) + } else { + state.outbuf.push(String(msg)) + } + if (state.readyState === OPEN) { + state.wsi.callback_on_writable() + } +}