add basic websocket support

This commit is contained in:
ading2210 2024-01-26 18:27:17 -05:00
parent 8ad11cd515
commit 17d8c9fa8c
10 changed files with 292 additions and 29 deletions

View file

@ -14,11 +14,17 @@ FRAGMENTS_DIR="fragments"
WRAPPER_SOURCE="main.js"
WISP_CLIENT="wisp_client"
#read exported functions
EXPORTED_FUNCS=""
for func in $(cat exported_funcs.txt); do
EXPORTED_FUNCS="$EXPORTED_FUNCS,_$func"
done
EXPORTED_FUNCS="${EXPORTED_FUNCS:1}"
#compile options
EXPORTED_FUNCS="_init_curl,_start_request,_tick_request,_active_requests,_free"
RUNTIME_METHODS="addFunction,removeFunction,allocate,ALLOC_NORMAL"
COMPILER_OPTIONS="-o $MODULE_FILE -lcurl -lssl -lcrypto -lcjson -lz -lbrotlidec -lbrotlicommon -lnghttp2 -I $INCLUDE_DIR -L $LIB_DIR"
EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASSERTIONS=1 -sALLOW_TABLE_GROWTH -sALLOW_MEMORY_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS"
EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASSERTIONS=1 -sLLD_REPORT_UNDEFINED -sALLOW_TABLE_GROWTH -sALLOW_MEMORY_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS"
if [[ "$*" == *"release"* ]]; then
COMPILER_OPTIONS="-Oz -flto $COMPILER_OPTIONS"
@ -41,7 +47,7 @@ rm -rf out
mkdir -p out
#compile the main c file - but only if the source has been modified
COMPILE_CMD="emcc main.c $COMPILER_OPTIONS $EMSCRIPTEN_OPTIONS"
COMPILE_CMD="emcc *.c $COMPILER_OPTIONS $EMSCRIPTEN_OPTIONS"
echo $COMPILE_CMD
$COMPILE_CMD
mv $COMPILED_FILE $WASM_FILE || true
@ -51,10 +57,11 @@ cp $WRAPPER_SOURCE $OUT_FILE
sed -i "/__emscripten_output__/r $MODULE_FILE" $OUT_FILE
rm $MODULE_FILE
#add wisp libraries
#add extra libraries
sed -i "/__extra_libraries__/r $WISP_CLIENT/polyfill.js" $OUT_FILE
sed -i "/__extra_libraries__/r $WISP_CLIENT/wisp.js" $OUT_FILE
sed -i "/__extra_libraries__/r ./messages.js" $OUT_FILE
sed -i "/__extra_libraries__/r ./websocket.js" $OUT_FILE
#apply patches
python3 scripts/patcher.py $FRAGMENTS_DIR $OUT_FILE

15
client/exported_funcs.txt Normal file
View file

@ -0,0 +1,15 @@
init_curl
start_request
tick_request
active_requests
recv_from_websocket
send_to_websocket
close_websocket
cleanup_websocket
get_result_size
get_result_buffer
get_result_code
get_result_closed
free

View file

@ -3,7 +3,7 @@
<head>
<link rel="icon" href="data:;base64,=">
<script defer src="./out/libcurl.js" onload="libcurl.load_wasm('/out/emscripten_compiled.wasm');"></script>
<script defer src="./out/libcurl.js" onload="libcurl.load_wasm('/out/libcurl.wasm');"></script>
<script>
document.addEventListener("libcurl_load", ()=>{
console.log("libcurl.js ready!");

View file

@ -10,8 +10,9 @@
#include "cacert.h"
#include "curl/multi.h"
typedef void(*DataCallback)(char* chunk_ptr, int chunk_size);
typedef void(*EndCallback)(int error, char* response_json);
#include "util.h"
#include "types.h"
void finish_request(CURLMsg *curl_msg);
#define ERROR_REDIRECT_DISALLOWED -1
@ -19,17 +20,6 @@ void finish_request(CURLMsg *curl_msg);
CURLM *multi_handle;
int request_active = 0;
struct RequestInfo {
int abort_on_redirect;
struct CURLMsg *curl_msg;
struct curl_slist* headers_list;
EndCallback end_callback;
};
int starts_with(const char *a, const char *b) {
return strncmp(a, b, strlen(b)) == 0;
}
int write_function(void *data, size_t size, size_t nmemb, DataCallback data_callback) {
long real_size = size * nmemb;
char* chunk = malloc(real_size);
@ -57,9 +47,10 @@ void tick_request() {
}
}
void start_request(const char* url, const char* json_params, DataCallback data_callback, EndCallback end_callback, const char* body, int body_length) {
CURL* start_request(const char* url, const char* json_params, DataCallback data_callback, EndCallback end_callback, const char* body, int body_length) {
CURL *http_handle = curl_easy_init();
int abort_on_redirect = 0;
int prevent_cleanup = 0;
curl_easy_setopt(http_handle, CURLOPT_URL, url);
curl_easy_setopt(http_handle, CURLOPT_CAINFO, "/cacert.pem");
@ -77,6 +68,7 @@ void start_request(const char* url, const char* json_params, DataCallback data_c
//if url is a websocket, tell curl that we should handle the connection manually
if (starts_with(url, "wss://") || starts_with(url, "ws://")) {
curl_easy_setopt(http_handle, CURLOPT_CONNECT_ONLY, 2L);
prevent_cleanup = 1;
}
//parse json options
@ -135,9 +127,12 @@ void start_request(const char* url, const char* json_params, DataCallback data_c
request_info->curl_msg = NULL;
request_info->headers_list = headers_list;
request_info->end_callback = end_callback;
request_info->prevent_cleanup = prevent_cleanup;
curl_easy_setopt(http_handle, CURLOPT_PRIVATE, request_info);
curl_multi_add_handle(multi_handle, http_handle);
return http_handle;
}
void finish_request(CURLMsg *curl_msg) {
@ -185,23 +180,20 @@ void finish_request(CURLMsg *curl_msg) {
//clean up curl
curl_slist_free_all(request_info->headers_list);
(*request_info->end_callback)(error, response_json_str);
if (request_info->prevent_cleanup) {
return;
}
curl_multi_remove_handle(multi_handle, http_handle);
curl_easy_cleanup(http_handle);
(*request_info->end_callback)(error, response_json_str);
free(request_info);
}
char* copy_bytes(const char* ptr, const int size) {
char* new_ptr = malloc(size);
memcpy(new_ptr, ptr, size);
return new_ptr;
}
void init_curl() {
curl_global_init(CURL_GLOBAL_DEFAULT);
multi_handle = curl_multi_init();
FILE *file = fopen("/cacert.pem", "wb");
FILE* file = fopen("/cacert.pem", "wb");
fwrite(_cacert_pem, 1, _cacert_pem_len, file);
fclose(file);
}

View file

@ -96,7 +96,7 @@ function perform_request(url, params, js_data_callback, js_end_callback, body=nu
end_callback_ptr = Module.addFunction(end_callback, "vii");
data_callback_ptr = Module.addFunction(data_callback, "vii");
_start_request(url_ptr, params_ptr, data_callback_ptr, end_callback_ptr, body_ptr, body_length);
let http_handle = _start_request(url_ptr, params_ptr, data_callback_ptr, end_callback_ptr, body_ptr, body_length);
_free(params_ptr);
active_requests ++;
@ -112,6 +112,8 @@ function perform_request(url, params, js_data_callback, js_end_callback, body=nu
}
}, 0);
}
return http_handle;
}
function merge_arrays(arrays) {
@ -271,7 +273,8 @@ return {
fetch: libcurl_fetch,
set_websocket: set_websocket_url,
load_wasm: load_wasm,
wisp: _wisp_connections
wisp: _wisp_connections,
WebSocket: CurlWebSocket
}
})()

17
client/types.h Normal file
View file

@ -0,0 +1,17 @@
typedef void(*DataCallback)(char* chunk_ptr, int chunk_size);
typedef void(*EndCallback)(int error, char* response_json);
struct RequestInfo {
int abort_on_redirect;
int prevent_cleanup;
struct CURLMsg *curl_msg;
struct curl_slist* headers_list;
EndCallback end_callback;
};
struct WSResult {
int res;
int buffer_size;
int closed;
char* buffer;
};

5
client/util.c Normal file
View file

@ -0,0 +1,5 @@
#include <string.h>
int starts_with(const char *a, const char *b) {
return strncmp(a, b, strlen(b)) == 0;
}

1
client/util.h Normal file
View file

@ -0,0 +1 @@
int starts_with(const char *a, const char *b);

57
client/websocket.c Normal file
View file

@ -0,0 +1,57 @@
#include <stdlib.h>
#include "curl/curl.h"
#include "curl/websockets.h"
#include "types.h"
extern CURLM* multi_handle;
struct WSResult* recv_from_websocket(CURL* http_handle, int buffer_size) {
const struct curl_ws_frame* ws_meta;
char* buffer = malloc(buffer_size);
size_t result_len;
CURLcode res = curl_ws_recv(http_handle, buffer, buffer_size, &result_len, &ws_meta);
struct WSResult* result = malloc(sizeof(struct WSResult));
result->buffer_size = result_len;
result->buffer = buffer;
result->res = (int) res;
result->closed = (ws_meta->flags & CURLWS_CLOSE);
return result;
}
int send_to_websocket(CURL* http_handle, const char* data, int data_len) {
size_t sent;
CURLcode res = curl_ws_send(http_handle, data, data_len, &sent, 0, CURLWS_BINARY);
return (int) res;
}
void close_websocket(CURL* http_handle) {
size_t sent;
curl_ws_send(http_handle, "", 0, &sent, 0, CURLWS_CLOSE);
}
//allow the main code to automatically clean up this websocket
void cleanup_websocket(CURL* http_handle) {
struct RequestInfo *request_info;
curl_easy_getinfo(http_handle, CURLINFO_PRIVATE, &request_info);
curl_multi_remove_handle(multi_handle, http_handle);
curl_easy_cleanup(http_handle);
free(request_info);
}
int get_result_size (const struct WSResult* result) {
return result->buffer_size;
}
char* get_result_buffer (const struct WSResult* result) {
return result->buffer;
}
int get_result_code (const struct WSResult* result) {
return result->res;
}
int get_result_closed (const struct WSResult* result) {
return result->closed;
}

166
client/websocket.js Normal file
View file

@ -0,0 +1,166 @@
//class for custom websocket
class CurlWebSocket extends EventTarget {
constructor(url, protocols=[]) {
super();
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
throw new SyntaxError("invalid url");
}
this.url = url;
this.protocols = protocols;
this.binaryType = "blob";
this.recv_buffer = [];
//legacy event handlers
this.onopen = () => {};
this.onerror = () => {};
this.onmessage = () => {};
this.onclose = () => {};
this.CONNECTING = 0;
this.OPEN = 1;
this.CLOSING = 2;
this.CLOSED = 3;
this.connect();
}
connect() {
this.status = this.CONNECTING;
let data_callback = () => {};
let finish_callback = (error, response_info) => {
this.finish_callback(error, response_info);
}
this.http_handle = perform_request(this.url, {_libcurl_verbose: 1}, data_callback, finish_callback, null);
this.recv_loop();
}
recv() {
let buffer_size = 64*1024;
let result_ptr = _recv_from_websocket(this.http_handle, buffer_size);
let result_code = _get_result_code(result_ptr);
if (result_code == 0) { //CURLE_OK - data recieved
if (_get_result_closed(result_ptr)) {
this.close_callback();
return;
}
let data_size = _get_result_size(result_ptr);
let data_ptr = _get_result_buffer(result_ptr);
let data_heap = Module.HEAPU8.subarray(data_ptr, data_ptr + data_size);
let data = new Uint8Array(data_heap);
_free(data_ptr);
this.recv_buffer.push(data);
if (data_size !== buffer_size) { //message finished
let full_data = merge_arrays(this.recv_buffer);
this.recv_buffer = [];
this.recv_callback(full_data);
}
}
if (result_code == 52) { //CURLE_GOT_NOTHING - socket closed
this.close_callback();
}
_free(result_ptr);
}
recv_loop() {
this.event_loop = setInterval(() => {
this.recv();
}, 1);
}
recv_callback(data) {
if (this.binaryType == "blob") {
data = new Blob(data);
}
else if (this.binaryType == "arraybuffer") {
data = data.buffer;
}
else {
throw "invalid binaryType string";
}
let msg_event = new MessageEvent("message", {data: data});
this.onmessage(msg_event);
this.dispatchEvent(msg_event);
}
close_callback(error=false) {
if (this.status == this.CLOSED) return;
this.status = this.CLOSED;
clearInterval(this.event_loop);
_cleanup_websocket();
if (error) {
let error_event = new Event("error");
this.dispatchEvent(error_event);
this.onerror(error_event);
}
else {
let close_event = new CloseEvent("close");
this.dispatchEvent(close_event);
this.onclose(close_event);
}
}
finish_callback(error, response_info) {
this.status = this.OPEN;
if (error != 0) this.close_callback(true);
let open_event = new Event("open");
this.onopen(open_event);
this.dispatchEvent(open_event);
}
send(data) {
if (this.status === this.CONNECTING) {
throw new DOMException("ws not ready yet");
}
if (this.status === this.CLOSED) {
return;
}
let data_array;
if (typeof data === "string") {
data_array = new TextEncoder().encode(data);
}
else if (data instanceof Blob) {
data.arrayBuffer().then(array_buffer => {
data_array = new Uint8Array(array_buffer);
this.send(data_array);
});
return;
}
//any typedarray
else if (data instanceof ArrayBuffer) {
//dataview objects
if (ArrayBuffer.isView(data) && data instanceof DataView) {
data_array = new Uint8Array(data.buffer);
}
//regular typed arrays
else if (ArrayBuffer.isView(data)) {
data_array = Uint8Array.from(data);
}
//regular arraybuffers
else {
data_array = new Uint8Array(data);
}
}
else {
throw "invalid data type to be sent";
}
let data_ptr = allocate_array(data_array);
let data_len = data.length;
_send_to_websocket(this.http_handle, data_ptr, data_len);
_free(data_ptr);
}
close() {
_close_websocket(this.http_handle);
}
}