From 1de71785239d7b2a7805cfccdc0c51654edf13e2 Mon Sep 17 00:00:00 2001 From: ading2210 Date: Wed, 10 Jan 2024 17:28:17 -0500 Subject: [PATCH 1/7] initial support for wsproxy --- client/build.sh | 3 ++- client/main.c | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/client/build.sh b/client/build.sh index 57dfd04..eeddbbc 100755 --- a/client/build.sh +++ b/client/build.sh @@ -12,7 +12,7 @@ WRAPPER_SOURCE="main.js" EXPORTED_FUNCS="_load_certs,_perform_request" RUNTIME_METHODS="addFunction,removeFunction,allocate,ALLOC_NORMAL" COMPILER_OPTIONS="-o $MODULE_FILE -lcurl -lssl -lcrypto -lcjson -lz -lbrotlidec -lbrotlicommon -I $INCLUDE_DIR -L $LIB_DIR" -EMSCRIPTEN_OPTIONS="-lwebsocket.js -sSINGLE_FILE -sASYNCIFY -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" +EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASYNCIFY -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" if [ "$1" = "release" ]; then COMPILER_OPTIONS="-O3 $COMPILER_OPTIONS" @@ -34,6 +34,7 @@ $COMPILE_CMD #patch the output to work around some emscripten bugs sed -i 's/err("__syscall_getsockname " \?+ \?fd);//' $MODULE_FILE sed -i 's/function _emscripten_console_error(str) {/& if(UTF8ToString(str).endsWith("__syscall_setsockopt\\n")) return;/' $MODULE_FILE +sed -i "s/var \?opts \?= \?undefined;/& var parts=addr.split('\/');url=url+parts[0]+':'+port;/" $MODULE_FILE #merge compiled emscripten module and wrapper code cp $WRAPPER_SOURCE $OUT_FILE diff --git a/client/main.c b/client/main.c index 9e7f7fc..c94b0d3 100644 --- a/client/main.c +++ b/client/main.c @@ -35,8 +35,6 @@ void perform_request(const char* url, const char* json_params, DataCallback data http_handle = curl_easy_init(); curl_easy_setopt(http_handle, CURLOPT_URL, url); - curl_easy_setopt(http_handle, CURLOPT_PROXY, "socks5h://127.0.0.1:1234"); - curl_easy_setopt(http_handle, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); curl_easy_setopt(http_handle, CURLOPT_CAINFO, "/cacert.pem"); curl_easy_setopt(http_handle, CURLOPT_CAPATH, "/cacert.pem"); From 09ff32dcd388ba07faa7dd7d5a62284ee5af2ae6 Mon Sep 17 00:00:00 2001 From: ading2210 Date: Wed, 10 Jan 2024 20:42:58 -0500 Subject: [PATCH 2/7] add python wsproxy implementation --- client/build.sh | 1 + client/main.c | 2 - client/main.js | 2 +- server/main.py | 84 +++++++++++++++++++++++------------------ server/requirements.txt | 3 +- server/run.sh | 7 +--- 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/client/build.sh b/client/build.sh index eeddbbc..c5f343c 100755 --- a/client/build.sh +++ b/client/build.sh @@ -16,6 +16,7 @@ EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASYNCIFY -sALLOW_TABLE_GROWTH -sEXPORTED_FU if [ "$1" = "release" ]; then COMPILER_OPTIONS="-O3 $COMPILER_OPTIONS" + EMSCRIPTEN_OPTIONS="-sSINGLE_FILE $EMSCRIPTEN_OPTIONS" fi #ensure deps are compiled diff --git a/client/main.c b/client/main.c index c94b0d3..c2f4110 100644 --- a/client/main.c +++ b/client/main.c @@ -24,8 +24,6 @@ int write_function(void *data, size_t size, size_t nmemb, DataCallback data_call } void perform_request(const char* url, const char* json_params, DataCallback data_callback, EndCallback end_callback, const char* body, int body_length) { - printf("downloading %s\n", url); - CURL *http_handle; CURLM *multi_handle; int still_running = 1; diff --git a/client/main.js b/client/main.js index d8ff917..03548cb 100644 --- a/client/main.js +++ b/client/main.js @@ -4,7 +4,7 @@ window.libcurl = (function() { //emscripten compiled code is inserted here /* __emscripten_output__ */ -const websocket_url = `wss://${location.hostname}/ws`; +const websocket_url = `wss://${location.hostname}/ws/`; const status_messages = { 100: "Continue", diff --git a/server/main.py b/server/main.py index 60ea254..8bf4bc8 100644 --- a/server/main.py +++ b/server/main.py @@ -1,45 +1,55 @@ -import logging -import os +import asyncio +from websockets.server import serve +from websockets.exceptions import ConnectionClosed -from asyncio_socks_server.app import SocksServer -from websockify.websocketproxy import WebSocketProxy +buffer_size = 64*1024 -#start a socks5 proxy as well as websockify +class Connection: + def __init__(self, ws, path): + self.ws = ws + self.path = path -def setup_logging(prefix): - stderr_handler = logging.StreamHandler() - stderr_handler.setLevel(logging.DEBUG) - log_formatter = logging.Formatter(prefix + "%(message)s") - stderr_handler.setFormatter(log_formatter) - root = logging.getLogger() - root.addHandler(stderr_handler) - root.setLevel(logging.INFO) + async def setup_connection(self): + addr_str = self.path.split("/")[-1] + self.tcp_host, self.tcp_port = addr_str.split(":") + self.tcp_port = int(self.tcp_port) -def start_websockify(listen_port, proxy_port): - options = { - "listen_host": "127.0.0.1", - "listen_port": int(listen_port), - "target_host": "127.0.0.1", - "target_port": int(proxy_port) - } + self.tcp_reader, self.tcp_writer = await asyncio.open_connection(host=self.tcp_host, port=self.tcp_port, limit=buffer_size) - server = WebSocketProxy(**options) - server.start_server() + async def handle_ws(self): + while True: + try: + data = await self.ws.recv() + except ConnectionClosed: + break + self.tcp_writer.write(data) + await self.tcp_writer.drain() + print("sent data to tcp") + + self.tcp_writer.close() + + async def handle_tcp(self): + while True: + data = await self.tcp_reader.read(buffer_size) + if len(data) == 0: + break #socket closed + await self.ws.send(data) + print("sent data to ws") + + await self.ws.close() -def start_socks(proxy_port): - socks_app = SocksServer( - LISTEN_HOST="127.0.0.1", - LISTEN_PORT=int(proxy_port) - ) - socks_app.run() +async def connection_handler(websocket, path): + print("incoming connection from "+path) + connection = Connection(websocket, path) + await connection.setup_connection() + ws_handler = asyncio.create_task(connection.handle_ws()) + tcp_handler = asyncio.create_task(connection.handle_tcp()) + + await asyncio.gather(ws_handler, tcp_handler) + +async def main(): + async with serve(connection_handler, "127.0.0.1", 6001): + await asyncio.Future() if __name__ == "__main__": - listen_port = os.environ.get("PORT") or 6001 - proxy_port = os.environ.get("SOCKS5_PORT") or 6002 - - pid = os.fork() - if pid == 0: - setup_logging("[websockify] ") - start_websockify(listen_port, proxy_port) - else: - start_socks(proxy_port) \ No newline at end of file + asyncio.run(main()) diff --git a/server/requirements.txt b/server/requirements.txt index 3241b52..7a38911 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,2 +1 @@ -websockify -asyncio-socks-server \ No newline at end of file +websockets \ No newline at end of file diff --git a/server/run.sh b/server/run.sh index 011d553..ca6acd2 100755 --- a/server/run.sh +++ b/server/run.sh @@ -9,11 +9,8 @@ if [ ! -d ".venv" ]; then fi source .venv/bin/activate -if ! python3 -c "import asyncio_socks_server, websockify" 2> /dev/null; then - pip3 install asyncio-socks-server - git clone https://github.com/novnc/websockify -b master --depth=1 - pip3 install ./websockify - rm -rf websockify +if ! python3 -c "import websockets" 2> /dev/null; then + pip3 install -r requirements.txt fi python3 main.py \ No newline at end of file From 248d5b0161d2cef8c2912d0e6463be4984753ccb Mon Sep 17 00:00:00 2001 From: ading2210 Date: Mon, 15 Jan 2024 15:15:26 -0500 Subject: [PATCH 3/7] add support for parallel requests --- client/build.sh | 8 ++-- client/main.c | 102 +++++++++++++++++++++++++++++++----------------- client/main.js | 4 +- server/main.py | 4 +- 4 files changed, 75 insertions(+), 43 deletions(-) diff --git a/client/build.sh b/client/build.sh index c5f343c..6c1d4c6 100755 --- a/client/build.sh +++ b/client/build.sh @@ -9,14 +9,16 @@ ES6_FILE="out/libcurl_module.mjs" MODULE_FILE="out/emscripten_compiled.js" WRAPPER_SOURCE="main.js" -EXPORTED_FUNCS="_load_certs,_perform_request" +EXPORTED_FUNCS="_init_curl,_start_request,_request_loop" RUNTIME_METHODS="addFunction,removeFunction,allocate,ALLOC_NORMAL" COMPILER_OPTIONS="-o $MODULE_FILE -lcurl -lssl -lcrypto -lcjson -lz -lbrotlidec -lbrotlicommon -I $INCLUDE_DIR -L $LIB_DIR" -EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASYNCIFY -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" +EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASYNCIFY -sASYNCIFY_ONLY=start_request,request_loop -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" if [ "$1" = "release" ]; then - COMPILER_OPTIONS="-O3 $COMPILER_OPTIONS" + COMPILER_OPTIONS="-O3 -flto $COMPILER_OPTIONS" EMSCRIPTEN_OPTIONS="-sSINGLE_FILE $EMSCRIPTEN_OPTIONS" +else + COMPILER_OPTIONS="$COMPILER_OPTIONS --profiling" fi #ensure deps are compiled diff --git a/client/main.c b/client/main.c index c2f4110..a78a93d 100644 --- a/client/main.c +++ b/client/main.c @@ -8,12 +8,24 @@ #include "curl/header.h" #include "cjson/cJSON.h" #include "cacert.h" +#include "curl/multi.h" typedef void(*DataCallback)(char* chunk_ptr, int chunk_size); typedef void(*EndCallback)(int error, char* response_json); +void finish_request(CURLMsg *curl_msg); #define ERROR_REDIRECT_DISALLOWED -1 +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 write_function(void *data, size_t size, size_t nmemb, DataCallback data_callback) { long real_size = size * nmemb; char* chunk = malloc(real_size); @@ -23,14 +35,36 @@ int write_function(void *data, size_t size, size_t nmemb, DataCallback data_call return real_size; } -void perform_request(const char* url, const char* json_params, DataCallback data_callback, EndCallback end_callback, const char* body, int body_length) { - CURL *http_handle; - CURLM *multi_handle; - int still_running = 1; - int abort_on_redirect = 0; +void request_loop() { + CURLMcode mc; + struct CURLMsg *curl_msg; + request_active = 1; + do { + mc = curl_multi_perform(multi_handle, &request_active); - curl_global_init(CURL_GLOBAL_DEFAULT); - http_handle = curl_easy_init(); + if(!mc) + mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL); + + if(mc) { + fprintf(stderr, "curl_multi_poll() failed, code %d.\n", (int)mc); + break; + } + + int msgq = 0; + curl_msg = curl_multi_info_read(multi_handle, &msgq); + if (curl_msg && curl_msg->msg == CURLMSG_DONE) { + finish_request(curl_msg); + } + + //ensure we dont block the main thread + emscripten_sleep(0); + + } while(request_active); +} + +void 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; curl_easy_setopt(http_handle, CURLOPT_URL, url); curl_easy_setopt(http_handle, CURLOPT_CAINFO, "/cacert.pem"); @@ -94,36 +128,31 @@ void perform_request(const char* url, const char* json_params, DataCallback data curl_easy_setopt(http_handle, CURLOPT_POSTFIELDS, body); curl_easy_setopt(http_handle, CURLOPT_POSTFIELDSIZE, body_length); } + + struct RequestInfo *request_info = malloc(sizeof(struct RequestInfo)); + request_info->abort_on_redirect = abort_on_redirect; + request_info->curl_msg = NULL; + request_info->headers_list = headers_list; + request_info->end_callback = end_callback; + curl_easy_setopt(http_handle, CURLOPT_PRIVATE, request_info); - multi_handle = curl_multi_init(); curl_multi_add_handle(multi_handle, http_handle); - - CURLMcode mc; - struct CURLMsg *m; - do { - mc = curl_multi_perform(multi_handle, &still_running); - - if(!mc) - mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL); - - if(mc) { - fprintf(stderr, "curl_multi_poll() failed, code %d.\n", (int)mc); - break; - } + if (!request_active) { + request_loop(); + } +} - int msgq = 0; - m = curl_multi_info_read(multi_handle, &msgq); +void finish_request(CURLMsg *curl_msg) { + //get initial request info from the http handle + struct RequestInfo *request_info; + CURL *http_handle = curl_msg->easy_handle; + curl_easy_getinfo(http_handle, CURLINFO_PRIVATE, &request_info); - //ensure we dont block the main thread - emscripten_sleep(0); - - } while(still_running); - - int error = (int) m->data.result; + int error = (int) curl_msg->data.result; long response_code; curl_easy_getinfo(http_handle, CURLINFO_RESPONSE_CODE, &response_code); - if (abort_on_redirect && response_code / 100 == 3) { + if (request_info->abort_on_redirect && response_code / 100 == 3) { error = ERROR_REDIRECT_DISALLOWED; } @@ -157,13 +186,11 @@ void perform_request(const char* url, const char* json_params, DataCallback data cJSON_Delete(response_json); //clean up curl - curl_slist_free_all(headers_list); + curl_slist_free_all(request_info->headers_list); curl_multi_remove_handle(multi_handle, http_handle); curl_easy_cleanup(http_handle); - curl_multi_cleanup(multi_handle); - curl_global_cleanup(); - - (*end_callback)(error, response_json_str); + (*request_info->end_callback)(error, response_json_str); + free(request_info); } char* copy_bytes(const char* ptr, const int size) { @@ -172,7 +199,10 @@ char* copy_bytes(const char* ptr, const int size) { return new_ptr; } -void load_certs() { +void init_curl() { + curl_global_init(CURL_GLOBAL_DEFAULT); + multi_handle = curl_multi_init(); + FILE *file = fopen("/cacert.pem", "wb"); fwrite(_cacert_pem, 1, _cacert_pem_len, file); fclose(file); diff --git a/client/main.js b/client/main.js index 03548cb..dd09398 100644 --- a/client/main.js +++ b/client/main.js @@ -149,7 +149,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"); - _perform_request(url_ptr, params_ptr, data_callback_ptr, end_callback_ptr, body_ptr, body_length); + _start_request(url_ptr, params_ptr, data_callback_ptr, end_callback_ptr, body_ptr, body_length); _free(params_ptr); } @@ -231,7 +231,7 @@ function set_websocket_url(url) { function main() { console.log("emscripten module loaded"); - _load_certs(); + _init_curl(); set_websocket_url(websocket_url); let load_event = new Event("libcurl_load"); diff --git a/server/main.py b/server/main.py index 8bf4bc8..68d84be 100644 --- a/server/main.py +++ b/server/main.py @@ -2,7 +2,7 @@ import asyncio from websockets.server import serve from websockets.exceptions import ConnectionClosed -buffer_size = 64*1024 +buffer_size = 1024*1024 class Connection: def __init__(self, ws, path): @@ -48,7 +48,7 @@ async def connection_handler(websocket, path): await asyncio.gather(ws_handler, tcp_handler) async def main(): - async with serve(connection_handler, "127.0.0.1", 6001): + async with serve(connection_handler, "127.0.0.1", 6001, subprotocols=["binary"]): await asyncio.Future() if __name__ == "__main__": From 95121068bcdb140f3d75592e386130d2c80fe981 Mon Sep 17 00:00:00 2001 From: ading2210 Date: Mon, 15 Jan 2024 20:57:50 -0500 Subject: [PATCH 4/7] add wisp support --- .gitmodules | 3 +++ client/build.sh | 8 +++++++- client/main.c | 7 +++---- client/main.js | 3 +++ client/wisp_client | 1 + 5 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 .gitmodules create mode 160000 client/wisp_client diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ae9314b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "client/wisp_client"] + path = client/wisp_client + url = https://github.com/mercuryWorkshop/wisp-client-js diff --git a/client/build.sh b/client/build.sh index 6c1d4c6..1523b70 100755 --- a/client/build.sh +++ b/client/build.sh @@ -8,11 +8,12 @@ OUT_FILE="out/libcurl.js" ES6_FILE="out/libcurl_module.mjs" MODULE_FILE="out/emscripten_compiled.js" WRAPPER_SOURCE="main.js" +WISP_CLIENT="wisp_client" EXPORTED_FUNCS="_init_curl,_start_request,_request_loop" RUNTIME_METHODS="addFunction,removeFunction,allocate,ALLOC_NORMAL" COMPILER_OPTIONS="-o $MODULE_FILE -lcurl -lssl -lcrypto -lcjson -lz -lbrotlidec -lbrotlicommon -I $INCLUDE_DIR -L $LIB_DIR" -EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASYNCIFY -sASYNCIFY_ONLY=start_request,request_loop -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" +EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASYNCIFY -sASYNCIFY_ONLY=start_request,request_loop -sASSERTIONS=1 -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" if [ "$1" = "release" ]; then COMPILER_OPTIONS="-O3 -flto $COMPILER_OPTIONS" @@ -44,6 +45,11 @@ cp $WRAPPER_SOURCE $OUT_FILE sed -i "/__emscripten_output__/r $MODULE_FILE" $OUT_FILE rm $MODULE_FILE +#add wisp libraries +sed -i "s/new WebSocketConstructor/new WispWebSocket/" $OUT_FILE +sed -i "/__wisp_libraries__/r $WISP_CLIENT/polyfill.js" $OUT_FILE +sed -i "/__wisp_libraries__/r $WISP_CLIENT/wisp.js" $OUT_FILE + #generate es6 module cp $OUT_FILE $ES6_FILE sed -i 's/window.libcurl/export const libcurl/' $ES6_FILE \ No newline at end of file diff --git a/client/main.c b/client/main.c index a78a93d..b43b265 100644 --- a/client/main.c +++ b/client/main.c @@ -50,15 +50,14 @@ void request_loop() { break; } + //ensure we dont block the main thread + emscripten_sleep(0); + int msgq = 0; curl_msg = curl_multi_info_read(multi_handle, &msgq); if (curl_msg && curl_msg->msg == CURLMSG_DONE) { finish_request(curl_msg); } - - //ensure we dont block the main thread - emscripten_sleep(0); - } while(request_active); } diff --git a/client/main.js b/client/main.js index dd09398..2a5d10b 100644 --- a/client/main.js +++ b/client/main.js @@ -4,6 +4,9 @@ window.libcurl = (function() { //emscripten compiled code is inserted here /* __emscripten_output__ */ +//wisp client code goes here +/* __wisp_libraries__ */ + const websocket_url = `wss://${location.hostname}/ws/`; const status_messages = { diff --git a/client/wisp_client b/client/wisp_client new file mode 160000 index 0000000..0a80885 --- /dev/null +++ b/client/wisp_client @@ -0,0 +1 @@ +Subproject commit 0a80885090b6247f42bc07cc85b441d8d719f551 From badeaa8ac368574c39b32a53f01aa59c61e8b698 Mon Sep 17 00:00:00 2001 From: ading2210 Date: Tue, 16 Jan 2024 00:25:29 -0500 Subject: [PATCH 5/7] use wisp server instead of wsproxy --- .gitmodules | 5 +++- README.md | 6 ++--- server/main.py | 55 ----------------------------------------- server/requirements.txt | 1 - server/run.sh | 1 + server/wisp_server | 1 + 6 files changed, 9 insertions(+), 60 deletions(-) delete mode 100644 server/main.py delete mode 100644 server/requirements.txt create mode 160000 server/wisp_server diff --git a/.gitmodules b/.gitmodules index ae9314b..46e8a75 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "client/wisp_client"] path = client/wisp_client - url = https://github.com/mercuryWorkshop/wisp-client-js + url = https://github.com/MercuryWorkshop/wisp-client-js +[submodule "server/wisp_server"] + path = server/wisp_server + url = https://github.com/MercuryWorkshop/wisp-server-python diff --git a/README.md b/README.md index 031a1cb..db1997d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This is an experimental port of [libcurl](https://curl.se/libcurl/) to WebAssemb ## Building: You can build this project by running the following commands: ``` -git clone https://github.com/ading2210/libcurl.js +git clone https://github.com/ading2210/libcurl.js --recursive cd libcurl.js/client ./build.sh ``` @@ -48,11 +48,11 @@ libcurl.set_websocket("ws://localhost:6001/"); ``` ## Proxy Server: -The proxy server consists of a [SOCKS5 proxy server](https://github.com/Amaindex/asyncio-socks-server) behind a [websocket TCP proxy](https://github.com/novnc/websockify). +The proxy server consists of a standard [Wisp](https://github.com/MercuryWorkshop/wisp-protocol) server, allowing multiple TCP connections to share the same websocket. To host the proxy server, run the following commands: ``` -git clone https://github.com/ading2210/libcurl.js +git clone https://github.com/ading2210/libcurl.js --recursive cd libcurl.js/server ./run.sh ``` diff --git a/server/main.py b/server/main.py deleted file mode 100644 index 68d84be..0000000 --- a/server/main.py +++ /dev/null @@ -1,55 +0,0 @@ -import asyncio -from websockets.server import serve -from websockets.exceptions import ConnectionClosed - -buffer_size = 1024*1024 - -class Connection: - def __init__(self, ws, path): - self.ws = ws - self.path = path - - async def setup_connection(self): - addr_str = self.path.split("/")[-1] - self.tcp_host, self.tcp_port = addr_str.split(":") - self.tcp_port = int(self.tcp_port) - - self.tcp_reader, self.tcp_writer = await asyncio.open_connection(host=self.tcp_host, port=self.tcp_port, limit=buffer_size) - - async def handle_ws(self): - while True: - try: - data = await self.ws.recv() - except ConnectionClosed: - break - self.tcp_writer.write(data) - await self.tcp_writer.drain() - print("sent data to tcp") - - self.tcp_writer.close() - - async def handle_tcp(self): - while True: - data = await self.tcp_reader.read(buffer_size) - if len(data) == 0: - break #socket closed - await self.ws.send(data) - print("sent data to ws") - - await self.ws.close() - -async def connection_handler(websocket, path): - print("incoming connection from "+path) - connection = Connection(websocket, path) - await connection.setup_connection() - ws_handler = asyncio.create_task(connection.handle_ws()) - tcp_handler = asyncio.create_task(connection.handle_tcp()) - - await asyncio.gather(ws_handler, tcp_handler) - -async def main(): - async with serve(connection_handler, "127.0.0.1", 6001, subprotocols=["binary"]): - await asyncio.Future() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/server/requirements.txt b/server/requirements.txt deleted file mode 100644 index 7a38911..0000000 --- a/server/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -websockets \ No newline at end of file diff --git a/server/run.sh b/server/run.sh index ca6acd2..4f3cfeb 100755 --- a/server/run.sh +++ b/server/run.sh @@ -4,6 +4,7 @@ set -e +cd wisp_server if [ ! -d ".venv" ]; then python3 -m venv .venv fi diff --git a/server/wisp_server b/server/wisp_server new file mode 160000 index 0000000..8747346 --- /dev/null +++ b/server/wisp_server @@ -0,0 +1 @@ +Subproject commit 874734623e4dfc4652b34a1bc61e1e35ca86dee8 From 4af7f6a4a0106e35364f42c27deabe128f31807e Mon Sep 17 00:00:00 2001 From: ading2210 Date: Tue, 16 Jan 2024 18:21:19 -0500 Subject: [PATCH 6/7] remove usage of asyncify --- README.md | 2 +- client/build.sh | 10 +++--- client/main.c | 50 +++++++++++++++------------ client/main.js | 80 +++++++------------------------------------- client/messages.js | 65 +++++++++++++++++++++++++++++++++++ client/tools/curl.sh | 2 +- 6 files changed, 113 insertions(+), 96 deletions(-) create mode 100644 client/messages.js diff --git a/README.md b/README.md index db1997d..5cbddb6 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ cd libcurl.js/server ./run.sh ``` -You can use the `PORT` and `SOCKS5_PORT` environment variables to control which ports the websocket proxy and the SOCKS5 server run on. +You can use the `HOST` and `PORT` environment variables to control the hostname and port that the proxy server listens on. ## Copyright: This project is licensed under the GNU AGPL v3. diff --git a/client/build.sh b/client/build.sh index 1523b70..3fbbaac 100755 --- a/client/build.sh +++ b/client/build.sh @@ -7,13 +7,14 @@ LIB_DIR="build/curl-wasm/lib/" OUT_FILE="out/libcurl.js" ES6_FILE="out/libcurl_module.mjs" MODULE_FILE="out/emscripten_compiled.js" +FRAGMENTS_DIR="fragments" WRAPPER_SOURCE="main.js" WISP_CLIENT="wisp_client" -EXPORTED_FUNCS="_init_curl,_start_request,_request_loop" +EXPORTED_FUNCS="_init_curl,_start_request,_tick_request,_active_requests" RUNTIME_METHODS="addFunction,removeFunction,allocate,ALLOC_NORMAL" COMPILER_OPTIONS="-o $MODULE_FILE -lcurl -lssl -lcrypto -lcjson -lz -lbrotlidec -lbrotlicommon -I $INCLUDE_DIR -L $LIB_DIR" -EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASYNCIFY -sASYNCIFY_ONLY=start_request,request_loop -sASSERTIONS=1 -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" +EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASSERTIONS=1 -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" if [ "$1" = "release" ]; then COMPILER_OPTIONS="-O3 -flto $COMPILER_OPTIONS" @@ -47,8 +48,9 @@ rm $MODULE_FILE #add wisp libraries sed -i "s/new WebSocketConstructor/new WispWebSocket/" $OUT_FILE -sed -i "/__wisp_libraries__/r $WISP_CLIENT/polyfill.js" $OUT_FILE -sed -i "/__wisp_libraries__/r $WISP_CLIENT/wisp.js" $OUT_FILE +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 #generate es6 module cp $OUT_FILE $ES6_FILE diff --git a/client/main.c b/client/main.c index b43b265..45fcda8 100644 --- a/client/main.c +++ b/client/main.c @@ -26,6 +26,10 @@ struct RequestInfo { 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); @@ -35,30 +39,30 @@ int write_function(void *data, size_t size, size_t nmemb, DataCallback data_call return real_size; } -void request_loop() { +int active_requests() { + return request_active; +} + +void tick_request() { CURLMcode mc; struct CURLMsg *curl_msg; request_active = 1; - do { - mc = curl_multi_perform(multi_handle, &request_active); - - if(!mc) - mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL); - - if(mc) { - fprintf(stderr, "curl_multi_poll() failed, code %d.\n", (int)mc); - break; - } + + mc = curl_multi_perform(multi_handle, &request_active); - //ensure we dont block the main thread - emscripten_sleep(0); + if(!mc) + mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL); - int msgq = 0; - curl_msg = curl_multi_info_read(multi_handle, &msgq); - if (curl_msg && curl_msg->msg == CURLMSG_DONE) { - finish_request(curl_msg); - } - } while(request_active); + if(mc) { + fprintf(stderr, "curl_multi_poll() failed, code %d.\n", (int)mc); + return; + } + + int msgq = 0; + curl_msg = curl_multi_info_read(multi_handle, &msgq); + if (curl_msg && curl_msg->msg == CURLMSG_DONE) { + finish_request(curl_msg); + } } void start_request(const char* url, const char* json_params, DataCallback data_callback, EndCallback end_callback, const char* body, int body_length) { @@ -77,6 +81,11 @@ void start_request(const char* url, const char* json_params, DataCallback data_c curl_easy_setopt(http_handle, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(http_handle, CURLOPT_ACCEPT_ENCODING, ""); + //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); + } + //parse json options cJSON* request_json = cJSON_Parse(json_params); cJSON* item = NULL; @@ -136,9 +145,6 @@ void start_request(const char* url, const char* json_params, DataCallback data_c curl_easy_setopt(http_handle, CURLOPT_PRIVATE, request_info); curl_multi_add_handle(multi_handle, http_handle); - if (!request_active) { - request_loop(); - } } void finish_request(CURLMsg *curl_msg) { diff --git a/client/main.js b/client/main.js index 2a5d10b..59d1c28 100644 --- a/client/main.js +++ b/client/main.js @@ -4,77 +4,11 @@ window.libcurl = (function() { //emscripten compiled code is inserted here /* __emscripten_output__ */ -//wisp client code goes here -/* __wisp_libraries__ */ +//extra client code goes here +/* __extra_libraries__ */ const websocket_url = `wss://${location.hostname}/ws/`; -const status_messages = { - 100: "Continue", - 101: "Switching Protocols", - 102: "Processing", - 103: "Early Hints", - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 207: "Multi-Status", - 208: "Already Reported", - 226: "IM Used", - 300: "Multiple Choices", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 305: "Use Proxy", - 306: "Switch Proxy", - 307: "Temporary Redirect", - 308: "Permanent Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Payload Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "I'm a teapot", - 421: "Misdirected Request", - 422: "Unprocessable Content", - 423: "Locked", - 424: "Failed Dependency", - 425: "Too Early", - 426: "Upgrade Required", - 428: "Precondition Required", - 429: "Too Many Requests", - 431: "Request Header Fields Too Large", - 451: "Unavailable For Legal Reasons", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported", - 506: "Variant Also Negotiates", - 507: "Insufficient Storage", - 508: "Loop Detected", - 510: "Not Extended", - 511: "Network Authentication Required" -} - //a case insensitive dictionary for request headers class Headers { constructor(obj) { @@ -154,6 +88,16 @@ function perform_request(url, params, js_data_callback, js_end_callback, body=nu data_callback_ptr = Module.addFunction(data_callback, "vii"); _start_request(url_ptr, params_ptr, data_callback_ptr, end_callback_ptr, body_ptr, body_length); _free(params_ptr); + + if (!_active_requests()) { + //time out requests after a while + let event_loop = setInterval(() => { + _tick_request(); + if (!_active_requests()) { + clearInterval(event_loop); + } + }, 500); + } } function merge_arrays(arrays) { diff --git a/client/messages.js b/client/messages.js new file mode 100644 index 0000000..1d1f488 --- /dev/null +++ b/client/messages.js @@ -0,0 +1,65 @@ +const status_messages = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 103: "Early Hints", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Payload Too Large", + 414: "URI Too Long", + 415: "Unsupported Media Type", + 416: "Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 421: "Misdirected Request", + 422: "Unprocessable Content", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 451: "Unavailable For Legal Reasons", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required" +} diff --git a/client/tools/curl.sh b/client/tools/curl.sh index cd4e40e..20fb1cb 100755 --- a/client/tools/curl.sh +++ b/client/tools/curl.sh @@ -17,7 +17,7 @@ git clone -b master --depth=1 https://github.com/curl/curl cd curl autoreconf -fi -emconfigure ./configure --host i686-linux --disable-shared --disable-threaded-resolver --without-libpsl --disable-netrc --disable-ipv6 --disable-tftp --disable-ntlm-wb --with-ssl=$OPENSSL_PREFIX --with-zlib=$ZLIB_PREFIX --with-brotli=$BROTLI_PREFIX +emconfigure ./configure --host i686-linux --disable-shared --disable-threaded-resolver --without-libpsl --disable-netrc --disable-ipv6 --disable-tftp --disable-ntlm-wb --enable-websockets --with-ssl=$OPENSSL_PREFIX --with-zlib=$ZLIB_PREFIX --with-brotli=$BROTLI_PREFIX emmake make -j$CORE_COUNT CFLAGS="-pthread" LIBS="-lbrotlicommon" rm -rf $PREFIX From fc32c7c1fb51a80e7b66d40ffd7dd7b606d33a0f Mon Sep 17 00:00:00 2001 From: ading2210 Date: Wed, 17 Jan 2024 01:04:43 -0500 Subject: [PATCH 7/7] patch js in a better way, fix event loop logic --- .gitignore | 1 + client/build.sh | 13 +++++-------- client/fragments/force_wsproxy.js | 5 +++++ client/fragments/silence_socket.js | 9 +++++++++ client/fragments/wisp_support.js | 4 ++++ client/main.c | 8 -------- client/main.js | 12 +++++++----- client/patcher.py | 26 ++++++++++++++++++++++++++ 8 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 client/fragments/force_wsproxy.js create mode 100644 client/fragments/silence_socket.js create mode 100644 client/fragments/wisp_support.js create mode 100644 client/patcher.py diff --git a/.gitignore b/.gitignore index 7b3cd3e..c322792 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /client/build /client/out +/client/fragments/tmp /server/.venv /server/websockify \ No newline at end of file diff --git a/client/build.sh b/client/build.sh index 3fbbaac..13b5c78 100755 --- a/client/build.sh +++ b/client/build.sh @@ -14,10 +14,10 @@ WISP_CLIENT="wisp_client" EXPORTED_FUNCS="_init_curl,_start_request,_tick_request,_active_requests" RUNTIME_METHODS="addFunction,removeFunction,allocate,ALLOC_NORMAL" COMPILER_OPTIONS="-o $MODULE_FILE -lcurl -lssl -lcrypto -lcjson -lz -lbrotlidec -lbrotlicommon -I $INCLUDE_DIR -L $LIB_DIR" -EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASSERTIONS=1 -sALLOW_TABLE_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" +EMSCRIPTEN_OPTIONS="-lwebsocket.js -sASSERTIONS=1 -sALLOW_TABLE_GROWTH -sALLOW_MEMORY_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" if [ "$1" = "release" ]; then - COMPILER_OPTIONS="-O3 -flto $COMPILER_OPTIONS" + COMPILER_OPTIONS="-Oz -flto $COMPILER_OPTIONS" EMSCRIPTEN_OPTIONS="-sSINGLE_FILE $EMSCRIPTEN_OPTIONS" else COMPILER_OPTIONS="$COMPILER_OPTIONS --profiling" @@ -36,22 +36,19 @@ COMPILE_CMD="emcc main.c $COMPILER_OPTIONS $EMSCRIPTEN_OPTIONS" echo $COMPILE_CMD $COMPILE_CMD -#patch the output to work around some emscripten bugs -sed -i 's/err("__syscall_getsockname " \?+ \?fd);//' $MODULE_FILE -sed -i 's/function _emscripten_console_error(str) {/& if(UTF8ToString(str).endsWith("__syscall_setsockopt\\n")) return;/' $MODULE_FILE -sed -i "s/var \?opts \?= \?undefined;/& var parts=addr.split('\/');url=url+parts[0]+':'+port;/" $MODULE_FILE - #merge compiled emscripten module and wrapper code cp $WRAPPER_SOURCE $OUT_FILE sed -i "/__emscripten_output__/r $MODULE_FILE" $OUT_FILE rm $MODULE_FILE #add wisp libraries -sed -i "s/new WebSocketConstructor/new WispWebSocket/" $OUT_FILE 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 +#apply patches +python3 patcher.py $FRAGMENTS_DIR $OUT_FILE + #generate es6 module cp $OUT_FILE $ES6_FILE sed -i 's/window.libcurl/export const libcurl/' $ES6_FILE \ No newline at end of file diff --git a/client/fragments/force_wsproxy.js b/client/fragments/force_wsproxy.js new file mode 100644 index 0000000..32993a8 --- /dev/null +++ b/client/fragments/force_wsproxy.js @@ -0,0 +1,5 @@ +/* INSERT +var ?opts ?= ?undefined; +*/ +var parts=addr.split("/"); +url = url + parts[0] + ":" + port; \ No newline at end of file diff --git a/client/fragments/silence_socket.js b/client/fragments/silence_socket.js new file mode 100644 index 0000000..2178f6a --- /dev/null +++ b/client/fragments/silence_socket.js @@ -0,0 +1,9 @@ +/* DELETE +err\("__syscall_getsockname " ?\+ ?fd\); +*/ + + +/* INSERT +function _emscripten_console_error\(str\) { +*/ +if (UTF8ToString(str).endsWith("__syscall_setsockopt\\n")) return; \ No newline at end of file diff --git a/client/fragments/wisp_support.js b/client/fragments/wisp_support.js new file mode 100644 index 0000000..8ed84cd --- /dev/null +++ b/client/fragments/wisp_support.js @@ -0,0 +1,4 @@ +/* REPLACE +new WebSocketConstructor +*/ +new WispWebSocket \ No newline at end of file diff --git a/client/main.c b/client/main.c index 45fcda8..be843ef 100644 --- a/client/main.c +++ b/client/main.c @@ -50,14 +50,6 @@ void tick_request() { mc = curl_multi_perform(multi_handle, &request_active); - if(!mc) - mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL); - - if(mc) { - fprintf(stderr, "curl_multi_poll() failed, code %d.\n", (int)mc); - return; - } - int msgq = 0; curl_msg = curl_multi_info_read(multi_handle, &msgq); if (curl_msg && curl_msg->msg == CURLMSG_DONE) { diff --git a/client/main.js b/client/main.js index 59d1c28..0f90b49 100644 --- a/client/main.js +++ b/client/main.js @@ -8,6 +8,7 @@ window.libcurl = (function() { /* __extra_libraries__ */ const websocket_url = `wss://${location.hostname}/ws/`; +var event_loop = null; //a case insensitive dictionary for request headers class Headers { @@ -88,15 +89,16 @@ function perform_request(url, params, js_data_callback, js_end_callback, body=nu data_callback_ptr = Module.addFunction(data_callback, "vii"); _start_request(url_ptr, params_ptr, data_callback_ptr, end_callback_ptr, body_ptr, body_length); _free(params_ptr); - - if (!_active_requests()) { - //time out requests after a while - let event_loop = setInterval(() => { + + _tick_request(); + if (!event_loop) { + event_loop = setInterval(() => { _tick_request(); if (!_active_requests()) { clearInterval(event_loop); + event_loop = null; } - }, 500); + }, 0); } } diff --git a/client/patcher.py b/client/patcher.py new file mode 100644 index 0000000..bf06c6d --- /dev/null +++ b/client/patcher.py @@ -0,0 +1,26 @@ +import re +import sys +import pathlib + +match_regex = r'/\* (.+?)\n(.+?)\n\*/\n(.*?)(\n\n|$)' + +fragments_dir = sys.argv[1] +target_dir = sys.argv[2] +fragments_path = pathlib.Path(fragments_dir) +target_path = pathlib.Path(target_dir) +target_text = target_path.read_text() + +for fragment_file in fragments_path.iterdir(): + print(f"applying patch from {fragment_file.name}") + fragment_text = fragment_file.read_text() + matches = re.findall(match_regex, fragment_text, re.S) + + for mode, patch_regex, patch_text, _ in matches: + if mode == "DELETE": + target_text = re.sub(patch_regex, "", target_text) + elif mode == "REPLACE": + target_text = re.sub(patch_regex, patch_text, target_text) + elif mode == "INSERT": + target_text = re.sub("("+patch_regex+")", r'\1'+patch_text, target_text) + +target_path.write_text(target_text) \ No newline at end of file