mirror of
https://github.com/ading2210/libcurl.js.git
synced 2025-05-12 14:00:01 -04:00
add basic websocket support
This commit is contained in:
parent
8ad11cd515
commit
17d8c9fa8c
10 changed files with 292 additions and 29 deletions
|
@ -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
15
client/exported_funcs.txt
Normal 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
|
|
@ -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!");
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
17
client/types.h
Normal 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
5
client/util.c
Normal 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
1
client/util.h
Normal file
|
@ -0,0 +1 @@
|
|||
int starts_with(const char *a, const char *b);
|
57
client/websocket.c
Normal file
57
client/websocket.c
Normal 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
166
client/websocket.js
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue