From: Felix Fietkau Date: Fri, 19 Apr 2024 18:04:54 +0000 (+0200) Subject: uclient: fix http regression X-Git-Url: http://git.openwrt.org/?p=project%2Fuclient.git;a=commitdiff_plain;h=HEAD;hp=19571e4f947c27d6bb406d5a88334cc7ad901b7e uclient: fix http regression Signed-off-by: Felix Fietkau --- diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b4e74d7..cae7102 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ variables: include: - remote: https://gitlab.com/ynezz/openwrt-ci/raw/master/openwrt-ci/gitlab/main.yml - + - local: .gitlab/wolfssl.yml .native ustream-ssl backend: extends: .openwrt-native-build @@ -13,13 +13,22 @@ include: - git clone https://git.openwrt.org/project/ustream-ssl.git - | cd ustream-ssl && - git checkout -b testing origin/$CI_COMMIT_BRANCH && + git log -1 && export VERBOSE=1 && mkdir -p build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr $CI_CMAKE_EXTRA_BUILD_ARGS && cd .. && make -j$(($(nproc)+1)) -C build && sudo make install -C build && cd .. + - cd $CI_PROJECT_DIR + - rm -fr ustream-ssl + +various native checks with ustream-ssl/wolfSSL backend (master branch): + extends: .ustream-ssl wolfSSL master + +various native checks with ustream-ssl/wolfSSL backend (release branch): + extends: .ustream-ssl wolfSSL release + various native checks with ustream-ssl/OpenSSL backend: extends: .native ustream-ssl backend @@ -28,11 +37,6 @@ various native checks with ustream-ssl/mbedTLS backend: variables: CI_CMAKE_EXTRA_BUILD_ARGS: -DMBEDTLS=on -various native checks with ustream-ssl/wolfSSL backend: - extends: .native ustream-ssl backend - variables: - CI_CMAKE_EXTRA_BUILD_ARGS: -DWOLFSSL=on - build with Atheros ATH79 SDK (out of tree): extends: .openwrt-sdk-oot-build_ath79-generic diff --git a/.gitlab/wolfssl.yml b/.gitlab/wolfssl.yml new file mode 100644 index 0000000..3e4b6fb --- /dev/null +++ b/.gitlab/wolfssl.yml @@ -0,0 +1,42 @@ +.ustream-ssl wolfSSL: + extends: .openwrt-native-build + variables: + CI_CMAKE_EXTRA_BUILD_ARGS: -DWOLFSSL=on + + before_script: + - git clone -b $CI_WOLFSSL_TEST_BRANCH --depth 1 https://github.com/wolfSSL/wolfssl + - | + cd wolfssl && + git log -1 && + ./autogen.sh && + ./configure \ + --enable-sni \ + --enable-opensslall \ + --enable-opensslextra \ + --enable-altcertchains \ + --prefix=/usr && + make -j$(($(nproc)+1)) all && + sudo make install && cd .. + + - git clone https://git.openwrt.org/project/ustream-ssl.git + - | + cd ustream-ssl && + git log -1 && + export VERBOSE=1 && + mkdir -p build && cd build && + cmake .. -DCMAKE_INSTALL_PREFIX=/usr $CI_CMAKE_EXTRA_BUILD_ARGS && cd .. && + make -j$(($(nproc)+1)) -C build && + sudo make install -C build + + - cd $CI_PROJECT_DIR + - rm -fr wolfssl ustream-ssl + +.ustream-ssl wolfSSL master: + extends: .ustream-ssl wolfSSL + variables: + CI_WOLFSSL_TEST_BRANCH: master + +.ustream-ssl wolfSSL release: + extends: .ustream-ssl wolfSSL + variables: + CI_WOLFSSL_TEST_BRANCH: release diff --git a/CMakeLists.txt b/CMakeLists.txt index 07796a6..c4aced7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") FIND_PATH(ubox_include_dir libubox/ustream-ssl.h) INCLUDE_DIRECTORIES(${ubox_include_dir}) +OPTION(BUILD_UCODE "build ucode plugin" ON) IF(BUILD_STATIC) FIND_LIBRARY(ubox_library NAMES ubox.a) @@ -21,6 +22,10 @@ ELSE(BUILD_STATIC) FIND_LIBRARY(ubox_library NAMES ubox) ENDIF(BUILD_STATIC) +IF(APPLE) + SET(UCODE_MODULE_LINK_OPTIONS "LINKER:-undefined,dynamic_lookup") +ENDIF() + SET(LIB_SOURCES uclient.c uclient-http.c uclient-utils.c) ADD_LIBRARY(uclient SHARED ${LIB_SOURCES}) TARGET_LINK_LIBRARIES(uclient ${ubox_library} dl) @@ -47,6 +52,16 @@ IF(UNIT_TESTING) ENDIF() ENDIF() +IF(BUILD_UCODE) + ADD_LIBRARY(uclient_lib MODULE ucode.c) + SET_TARGET_PROPERTIES(uclient_lib PROPERTIES OUTPUT_NAME uclient PREFIX "") + TARGET_LINK_OPTIONS(uclient_lib PRIVATE ${UCODE_MODULE_LINK_OPTIONS}) + TARGET_LINK_LIBRARIES(uclient_lib uclient) + INSTALL(TARGETS uclient_lib + LIBRARY DESTINATION lib/ucode + ) +ENDIF() + INSTALL(FILES uclient.h uclient-utils.h DESTINATION include/libubox ) @@ -54,3 +69,4 @@ INSTALL(TARGETS uclient uclient-fetch LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) + diff --git a/tests/cram/test-san_uclient-fetch.t b/tests/cram/test-san_uclient-fetch.t index 3158bde..435659b 100644 --- a/tests/cram/test-san_uclient-fetch.t +++ b/tests/cram/test-san_uclient-fetch.t @@ -65,6 +65,8 @@ check that SSL works: $ uc -q -O /dev/null 'https://www.openwrt.org' + $ uc -q -O /dev/null 'https://letsencrypt.org' + $ uc -O /dev/null 'https://downloads.openwrt.org/does-not-exist' 2>&1 | grep error HTTP error 404 diff --git a/tests/cram/test_uclient-fetch.t b/tests/cram/test_uclient-fetch.t index 4ffe719..e22aa40 100644 --- a/tests/cram/test_uclient-fetch.t +++ b/tests/cram/test_uclient-fetch.t @@ -65,6 +65,8 @@ check that SSL works: $ uc -q -O /dev/null 'https://www.openwrt.org' + $ uc -q -O /dev/null 'https://letsencrypt.org' + $ uc -O /dev/null 'https://downloads.openwrt.org/does-not-exist' 2>&1 | grep error HTTP error 404 diff --git a/uclient-backend.h b/uclient-backend.h index c2b9fd5..1b808ac 100644 --- a/uclient-backend.h +++ b/uclient-backend.h @@ -34,6 +34,7 @@ struct uclient_backend { int (*read)(struct uclient *cl, char *buf, unsigned int len); int (*write)(struct uclient *cl, const char *buf, unsigned int len); + int (*pending_bytes)(struct uclient *cl, bool write); }; void uclient_backend_set_error(struct uclient *cl, int code); @@ -41,5 +42,9 @@ void uclient_backend_set_eof(struct uclient *cl); void uclient_backend_reset_state(struct uclient *cl); struct uclient_url *uclient_get_url(const char *url_str, const char *auth_str); struct uclient_url *uclient_get_url_location(struct uclient_url *url, const char *location); +static inline void uclient_backend_read_notify(struct uclient *cl) +{ + uloop_timeout_set(&cl->read_notify, 1); +} #endif diff --git a/uclient-fetch.c b/uclient-fetch.c index 282092e..b2b8d0d 100644 --- a/uclient-fetch.c +++ b/uclient-fetch.c @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -35,10 +34,8 @@ #include "uclient.h" #include "uclient-utils.h" -#ifdef __APPLE__ -#define LIB_EXT "dylib" -#else -#define LIB_EXT "so" +#ifndef strdupa +#define strdupa(x) strcpy(alloca(strlen(x)+1),x) #endif static const char *user_agent = "uclient-fetch"; @@ -423,6 +420,23 @@ static void eof_cb(struct uclient *cl) request_done(cl); } +static void +handle_uclient_log_msg(struct uclient *cl, enum uclient_log_type type, const char *msg) +{ + static const char * const type_str_list[] = { + [UCLIENT_LOG_SSL_ERROR] = "SSL error", + [UCLIENT_LOG_SSL_VERIFY_ERROR] = "SSL verify error", + }; + const char *type_str = NULL; + + if (type < ARRAY_SIZE(type_str_list)) + type_str = type_str_list[type]; + if (!type_str) + type_str = "Unknown"; + + fprintf(stderr, "%s: %s\n", type_str, msg); +} + static void handle_uclient_error(struct uclient *cl, int code) { const char *type = "Unknown error"; @@ -466,6 +480,7 @@ static const struct uclient_cb cb = { .data_read = read_data_cb, .data_eof = eof_cb, .error = handle_uclient_error, + .log_msg = handle_uclient_log_msg, }; static int usage(const char *progname) @@ -509,21 +524,6 @@ static void init_ca_cert(void) globfree(&gl); } -static void init_ustream_ssl(void) -{ - void *dlh; - - dlh = dlopen("libustream-ssl." LIB_EXT, RTLD_LAZY | RTLD_LOCAL); - if (!dlh) - return; - - ssl_ops = dlsym(dlh, "ustream_ssl_ops"); - if (!ssl_ops) - return; - - ssl_ctx = ssl_ops->context_new(false); -} - static int no_ssl(const char *progname) { fprintf(stderr, @@ -535,6 +535,11 @@ static int no_ssl(const char *progname) return 1; } +static void debug_cb(void *priv, int level, const char *msg) +{ + fprintf(stderr, "%s\n", msg); +} + enum { L_NO_CHECK_CERTIFICATE, L_CA_CERTIFICATE, @@ -550,6 +555,7 @@ enum { L_PROXY, L_NO_PROXY, L_QUIET, + L_VERBOSE, }; static const struct option longopts[] = { @@ -567,6 +573,7 @@ static const struct option longopts[] = { [L_PROXY] = { "proxy", required_argument, NULL, 0 }, [L_NO_PROXY] = { "no-proxy", no_argument, NULL, 0 }, [L_QUIET] = { "quiet", no_argument, NULL, 0 }, + [L_VERBOSE] = { "verbose", no_argument, NULL, 0 }, {} }; @@ -584,11 +591,12 @@ int main(int argc, char **argv) int i, ch; int rc; int af = -1; + int debug_level = 0; signal(SIGPIPE, SIG_IGN); - init_ustream_ssl(); + ssl_ctx = uclient_new_ssl_context(&ssl_ops); - while ((ch = getopt_long(argc, argv, "46cO:P:qsT:U:Y:", longopts, &longopt_idx)) != -1) { + while ((ch = getopt_long(argc, argv, "46cO:P:qsT:U:vY:", longopts, &longopt_idx)) != -1) { switch(ch) { case 0: switch (longopt_idx) { @@ -651,6 +659,9 @@ int main(int argc, char **argv) case L_QUIET: quiet = true; break; + case L_VERBOSE: + debug_level++; + break; default: return usage(progname); } @@ -686,6 +697,9 @@ int main(int argc, char **argv) case 'T': timeout = atoi(optarg); break; + case 'v': + debug_level++; + break; case 'Y': if (strcmp(optarg, "on") != 0) proxy = false; @@ -698,6 +712,9 @@ int main(int argc, char **argv) argv += optind; argc -= optind; + if (debug_level) + ssl_ops->context_set_debug(ssl_ctx, debug_level, debug_cb, NULL); + if (verify && !has_cert) default_certs = true; diff --git a/uclient-http.c b/uclient-http.c index 349e69c..deeb456 100644 --- a/uclient-http.c +++ b/uclient-http.c @@ -72,11 +72,14 @@ struct uclient_http { struct ustream_ssl_ctx *ssl_ctx; struct ustream *us; - struct ustream_fd ufd; - struct ustream_ssl ussl; + union { + struct ustream_fd ufd; + struct ustream_ssl ussl; + }; struct uloop_timeout disconnect_t; unsigned int seq; + int fd; bool ssl_require_validation; bool ssl; @@ -131,7 +134,7 @@ static int uclient_do_connect(struct uclient_http *uh, const char *port) return -1; fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); - ustream_fd_init(&uh->ufd, fd); + uh->fd = fd; sl = sizeof(uh->uc.local_addr); memset(&uh->uc.local_addr, 0, sl); @@ -148,9 +151,10 @@ static void uclient_http_disconnect(struct uclient_http *uh) if (uh->ssl) ustream_free(&uh->ussl.stream); - ustream_free(&uh->ufd.stream); - if(uh->ufd.fd.fd) - close(uh->ufd.fd.fd); + else + ustream_free(&uh->ufd.stream); + if(uh->fd >= 0) + close(uh->fd >= 0); uh->us = NULL; } @@ -655,7 +659,8 @@ static void uclient_http_headers_complete(struct uclient_http *uh) if (uh->eof || seq != uh->uc.seq) return; - if (uh->req_type == REQ_HEAD || uh->uc.status_code == 204) { + if (uh->req_type == REQ_HEAD || uh->uc.status_code == 204 || + uh->content_length == 0) { uh->eof = true; uclient_notify_eof(uh); } @@ -779,9 +784,7 @@ static void __uclient_notify_read(struct uclient_http *uh) if (uh->state == HTTP_STATE_RECV_DATA) { /* Now it's uclient user turn to read some data */ uloop_timeout_cancel(&uc->connection_timeout); - - if (uc->cb->data_read) - uc->cb->data_read(uc); + uclient_backend_read_notify(uc); } } @@ -823,6 +826,7 @@ static int uclient_setup_http(struct uclient_http *uh) struct ustream *us = &uh->ufd.stream; int ret; + memset(&uh->ufd, 0, sizeof(uh->ufd)); uh->us = us; uh->ssl = false; @@ -835,6 +839,8 @@ static int uclient_setup_http(struct uclient_http *uh) if (ret) return UCLIENT_ERROR_CONNECT; + ustream_fd_init(&uh->ufd, uh->fd); + return 0; } @@ -862,17 +868,23 @@ static void uclient_ssl_notify_state(struct ustream *us) static void uclient_ssl_notify_error(struct ustream_ssl *ssl, int error, const char *str) { struct uclient_http *uh = container_of(ssl, struct uclient_http, ussl); + struct uclient *uc = &uh->uc; + if (uc->cb->log_msg) + uc->cb->log_msg(uc, UCLIENT_LOG_SSL_ERROR, str); uclient_http_error(uh, UCLIENT_ERROR_CONNECT); } static void uclient_ssl_notify_verify_error(struct ustream_ssl *ssl, int error, const char *str) { struct uclient_http *uh = container_of(ssl, struct uclient_http, ussl); + struct uclient *uc = &uh->uc; if (!uh->ssl_require_validation) return; + if (uc->cb->log_msg) + uc->cb->log_msg(uc, UCLIENT_LOG_SSL_VERIFY_ERROR, str); uclient_http_error(uh, UCLIENT_ERROR_SSL_INVALID_CERT); } @@ -892,6 +904,7 @@ static int uclient_setup_https(struct uclient_http *uh) struct ustream *us = &uh->ussl.stream; int ret; + memset(&uh->ussl, 0, sizeof(uh->ussl)); uh->ssl = true; uh->us = us; @@ -910,7 +923,7 @@ static int uclient_setup_https(struct uclient_http *uh) uh->ussl.notify_verify_error = uclient_ssl_notify_verify_error; uh->ussl.notify_connected = uclient_ssl_notify_connected; uh->ussl.server_name = uh->uc.url->host; - uh->ssl_ops->init(&uh->ussl, &uh->ufd.stream, uh->ssl_ctx, false); + uh->ssl_ops->init_fd(&uh->ussl, uh->fd, uh->ssl_ctx, false); uh->ssl_ops->set_peer_cn(&uh->ussl, uh->uc.url->host); return 0; @@ -1078,8 +1091,12 @@ uclient_http_read(struct uclient *cl, char *buf, unsigned int len) return 0; data = ustream_get_read_buf(uh->us, &read_len); - if (!data || !read_len) - return 0; + if (!data || !read_len) { + ustream_poll(uh->us); + data = ustream_get_read_buf(uh->us, &read_len); + if (!data || !read_len) + return 0; + } data_end = data + read_len; read_len = 0; @@ -1158,14 +1175,8 @@ int uclient_http_redirect(struct uclient *cl) if (cl->backend != &uclient_backend_http) return false; - switch (cl->status_code) { - case 301: - case 302: - case 307: - break; - default: + if (!uclient_http_status_redirect(cl)) return false; - } blobmsg_parse(&location, 1, &tb, blob_data(uh->meta.head), blob_len(uh->meta.head)); if (!tb) @@ -1232,6 +1243,14 @@ int uclient_http_set_address_family(struct uclient *cl, int af) return 0; } +static int +uclient_http_pending_bytes(struct uclient *cl, bool write) +{ + struct uclient_http *uh = container_of(cl, struct uclient_http, uc); + + return ustream_pending_data(uh->us, write); +} + const struct uclient_backend uclient_backend_http = { .prefix = uclient_http_prefix, @@ -1245,4 +1264,5 @@ const struct uclient_backend uclient_backend_http = { .read = uclient_http_read, .write = uclient_http_send_data, .request = uclient_http_request_done, + .pending_bytes = uclient_http_pending_bytes, }; diff --git a/uclient-test.uc b/uclient-test.uc new file mode 100755 index 0000000..7333aa4 --- /dev/null +++ b/uclient-test.uc @@ -0,0 +1,50 @@ +#!/usr/bin/env ucode +'use strict'; +import { basename, stdout } from "fs"; +let uloop = require("uloop"); +let uclient = require("uclient"); + +function fetch_data() { + let data; + while (length(data = uc.read()) > 0) + print(data); +} + +let url = shift(ARGV); +if (!url) { + warn(`Usage: ${basename(sourcepath())} \n`); + exit(1); +} + +uloop.init(); +uc = uclient.new(url, null, { + header_done: (cb) => { + warn(sprintf("Headers: %.J\nStatus: %.J\n", uc.get_headers(), uc.status())); + }, + data_read: fetch_data, + data_eof: (cb) => { + stdout.flush(); + uloop.end(); + }, + error: (cb, code) => { + warn(`Error: ${code}\n`); + uloop.end(); + } +}); + +if (!uc.ssl_init({ verify: false })) { + warn(`Failed to initialize SSL\n`); + exit(1); +} + +if (!uc.connect()) { + warn(`Failed to connect\n`); + exit(1); +} + +if (!uc.request("GET")) { + warn(`Failed to send request\n`); + exit(1); +} + +uloop.run(); diff --git a/uclient.c b/uclient.c index a372d4a..0fb92b8 100644 --- a/uclient.c +++ b/uclient.c @@ -16,11 +16,18 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include +#include #include #include "uclient.h" #include "uclient-utils.h" #include "uclient-backend.h" +#ifdef __APPLE__ +#define LIB_EXT "dylib" +#else +#define LIB_EXT "so" +#endif + char *uclient_get_addr(char *dest, int *port, union uclient_addr *a) { int portval; @@ -232,6 +239,14 @@ static void uclient_connection_timeout(struct uloop_timeout *timeout) uclient_backend_set_error(cl, UCLIENT_ERROR_TIMEDOUT); } +static void __uclient_read_notify(struct uloop_timeout *timeout) +{ + struct uclient *cl = container_of(timeout, struct uclient, read_notify); + + if (cl->cb->data_read) + cl->cb->data_read(cl); +} + struct uclient *uclient_new(const char *url_str, const char *auth_str, const struct uclient_cb *cb) { struct uclient *cl; @@ -250,6 +265,7 @@ struct uclient *uclient_new(const char *url_str, const char *auth_str, const str cl->url = url; cl->timeout_msecs = UCLIENT_DEFAULT_TIMEOUT_MS; cl->connection_timeout.cb = uclient_connection_timeout; + cl->read_notify.cb = __uclient_read_notify; return cl; } @@ -361,6 +377,27 @@ int uclient_request(struct uclient *cl) return 0; } +struct ustream_ssl_ctx *uclient_new_ssl_context(const struct ustream_ssl_ops **ops) +{ + static const struct ustream_ssl_ops *ssl_ops; + void *dlh; + + if (!ssl_ops) { + dlh = dlopen("libustream-ssl." LIB_EXT, RTLD_LAZY | RTLD_LOCAL); + if (!dlh) + return NULL; + + ssl_ops = dlsym(dlh, "ustream_ssl_ops"); + if (!ssl_ops) { + dlclose(dlh); + return NULL; + } + } + + *ops = ssl_ops; + return ssl_ops->context_new(false); +} + int uclient_read(struct uclient *cl, char *buf, int len) { if (!cl->backend->read) @@ -369,9 +406,19 @@ int uclient_read(struct uclient *cl, char *buf, int len) return cl->backend->read(cl, buf, len); } +int uclient_pending_bytes(struct uclient *cl, bool write) +{ + if (!cl->backend->pending_bytes) + return -1; + + return cl->backend->pending_bytes(cl, write); +} + void uclient_disconnect(struct uclient *cl) { uloop_timeout_cancel(&cl->connection_timeout); + uloop_timeout_cancel(&cl->timeout); + uloop_timeout_cancel(&cl->read_notify); if (!cl->backend->disconnect) return; @@ -421,6 +468,7 @@ void __hidden uclient_backend_reset_state(struct uclient *cl) cl->eof = false; cl->error_code = 0; uloop_timeout_cancel(&cl->timeout); + uloop_timeout_cancel(&cl->read_notify); } const char * uclient_strerror(unsigned err) diff --git a/uclient.h b/uclient.h index 4f37364..772a5bd 100644 --- a/uclient.h +++ b/uclient.h @@ -39,6 +39,12 @@ enum uclient_error_code { __UCLIENT_ERROR_MAX }; +enum uclient_log_type { + UCLIENT_LOG_SSL_ERROR, + UCLIENT_LOG_SSL_VERIFY_ERROR, + __UCLIENT_LOG_MAX +}; + union uclient_addr { struct sockaddr sa; struct sockaddr_in sin; @@ -75,6 +81,7 @@ struct uclient { struct blob_attr *meta; struct uloop_timeout connection_timeout; + struct uloop_timeout read_notify; struct uloop_timeout timeout; }; @@ -84,6 +91,7 @@ struct uclient_cb { void (*data_eof)(struct uclient *cl); void (*header_done)(struct uclient *cl); void (*error)(struct uclient *cl, int code); + void (*log_msg)(struct uclient *cl, enum uclient_log_type type, const char *msg); }; struct uclient *uclient_new(const char *url, const char *auth_str, const struct uclient_cb *cb); @@ -112,9 +120,11 @@ void uclient_disconnect(struct uclient *cl); int uclient_read(struct uclient *cl, char *buf, int len); int uclient_write(struct uclient *cl, const char *buf, int len); +int uclient_pending_bytes(struct uclient *cl, bool write); int uclient_request(struct uclient *cl); char *uclient_get_addr(char *dest, int *port, union uclient_addr *a); +struct ustream_ssl_ctx *uclient_new_ssl_context(const struct ustream_ssl_ops **ops); /* HTTP */ extern const struct uclient_backend uclient_backend_http; @@ -124,6 +134,18 @@ int uclient_http_set_header(struct uclient *cl, const char *name, const char *va int uclient_http_set_request_type(struct uclient *cl, const char *type); int uclient_http_redirect(struct uclient *cl); +static inline bool uclient_http_status_redirect(struct uclient *cl) +{ + switch (cl->status_code) { + case 301: + case 302: + case 307: + return true; + default: + return false; + } +} + int uclient_http_set_ssl_ctx(struct uclient *cl, const struct ustream_ssl_ops *ops, struct ustream_ssl_ctx *ctx, bool require_validation); int uclient_http_set_address_family(struct uclient *cl, int af); diff --git a/ucode.c b/ucode.c new file mode 100644 index 0000000..fa60806 --- /dev/null +++ b/ucode.c @@ -0,0 +1,559 @@ +/* + * uclient - ustream based protocol client library - ucode binding + * + * Copyright (C) 2024 Felix Fietkau + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#include +#include +#include +#include "uclient.h" + +static uc_resource_type_t *uc_uclient_type; +static uc_value_t *registry; +static uc_vm_t *uc_vm; + +struct uc_uclient_priv { + struct uclient_cb cb; + const struct ustream_ssl_ops *ssl_ops; + struct ustream_ssl_ctx *ssl_ctx; + uc_value_t *resource; + unsigned int idx; + int offset; +}; + +static void uc_uclient_register(struct uc_uclient_priv *ucl, uc_value_t *cb) +{ + size_t i, len; + + len = ucv_array_length(registry); + for (i = 0; i < len; i++) + if (!ucv_array_get(registry, i)) + break; + + ucv_array_set(registry, i, ucv_get(cb)); + ucl->idx = i; +} + +static void free_uclient(void *ptr) +{ + struct uclient *cl = ptr; + struct uc_uclient_priv *ucl; + + if (!cl) + return; + + ucl = cl->priv; + ucv_array_set(registry, ucl->idx, NULL); + ucv_array_set(registry, ucl->idx + 1, NULL); + uclient_free(cl); + free(ucl); +} + +static uc_value_t * +uc_uclient_free(uc_vm_t *vm, size_t nargs) +{ + struct uclient **cl = uc_fn_this("uclient"); + + free_uclient(*cl); + *cl = NULL; + + return NULL; +} + +static uc_value_t * +uc_uclient_ssl_init(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + const struct ustream_ssl_ops *ops; + struct ustream_ssl_ctx *ctx; + struct uc_uclient_priv *ucl; + uc_value_t *args = uc_fn_arg(0); + bool verify = false; + uc_value_t *cur; + + if (!cl) + return NULL; + + ucl = cl->priv; + if (ucl->ssl_ctx) { + uclient_http_set_ssl_ctx(cl, NULL, NULL, false); + ucl->ssl_ctx = NULL; + ucl->ssl_ops = NULL; + } + + ctx = uclient_new_ssl_context(&ops); + if (!ctx) + return NULL; + + ucl->ssl_ops = ops; + ucl->ssl_ctx = ctx; + + if ((cur = ucv_object_get(args, "cert_file", NULL)) != NULL) { + const char *str = ucv_string_get(cur); + if (!str || ops->context_set_crt_file(ctx, str)) + goto err; + } + + if ((cur = ucv_object_get(args, "key_file", NULL)) != NULL) { + const char *str = ucv_string_get(cur); + if (!str || ops->context_set_key_file(ctx, str)) + goto err; + } + + if ((cur = ucv_object_get(args, "ca_files", NULL)) != NULL) { + size_t len; + + if (ucv_type(cur) != UC_ARRAY) + goto err; + + len = ucv_array_length(cur); + for (size_t i = 0; i < len; i++) { + uc_value_t *c = ucv_array_get(cur, i); + const char *str; + + if (!c) + continue; + + str = ucv_string_get(c); + if (!str) + goto err; + + ops->context_add_ca_crt_file(ctx, str); + } + + verify = true; + } + + if ((cur = ucv_object_get(args, "verify", NULL)) != NULL) + verify = ucv_is_truish(cur); + + ops->context_set_require_validation(ctx, verify); + uclient_http_set_ssl_ctx(cl, ops, ctx, verify); + + return ucv_boolean_new(true); + +err: + ops->context_free(ctx); + return NULL; +} + +static uc_value_t * +uc_uclient_set_timeout(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + uc_value_t *val = uc_fn_arg(0); + + if (!cl || ucv_type(val) != UC_INTEGER) + return NULL; + + if (uclient_set_timeout(cl, ucv_int64_get(val))) + return NULL; + + return ucv_boolean_new(true); +} + +static uc_value_t * +uc_uclient_set_url(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + uc_value_t *url = uc_fn_arg(0); + uc_value_t *auth_str = uc_fn_arg(1); + + if (!cl || ucv_type(url) != UC_STRING || + (auth_str && ucv_type(auth_str) != UC_STRING)) + return NULL; + + if (uclient_set_url(cl, ucv_string_get(url), ucv_string_get(auth_str))) + return NULL; + + return ucv_boolean_new(true); +} + +static uc_value_t * +uc_uclient_set_proxy_url(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + uc_value_t *url = uc_fn_arg(0); + uc_value_t *auth_str = uc_fn_arg(1); + + if (!cl || ucv_type(url) != UC_STRING || + (auth_str && ucv_type(auth_str) != UC_STRING)) + return NULL; + + if (uclient_set_proxy_url(cl, ucv_string_get(url), ucv_string_get(auth_str))) + return NULL; + + return ucv_boolean_new(true); +} + +static uc_value_t * +uc_uclient_get_headers(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + struct blob_attr *cur; + uc_value_t *ret; + size_t rem; + + if (!cl) + return NULL; + + ret = ucv_object_new(uc_vm); + blobmsg_for_each_attr(cur, cl->meta, rem) { + uc_value_t *str; + + if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING) + continue; + + str = ucv_string_new(blobmsg_get_string(cur)); + ucv_object_add(ret, blobmsg_name(cur), ucv_get(str)); + } + + return ret; +} + +static uc_value_t * +uc_uclient_connect(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + + if (!cl || uclient_connect(cl)) + return NULL; + + return ucv_boolean_new(true); +} + +static uc_value_t * +uc_uclient_disconnect(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + + if (!cl) + return NULL; + + uclient_disconnect(cl); + + return ucv_boolean_new(true); +} + +static uc_value_t * +__uc_uclient_cb(struct uclient *cl, const char *name, uc_value_t *arg) +{ + struct uc_uclient_priv *ucl = cl->priv; + uc_vm_t *vm = uc_vm; + uc_value_t *cb, *cb_obj; + + cb_obj = ucv_array_get(registry, ucl->idx); + if (!cb_obj) + return NULL; + + cb = ucv_property_get(cb_obj, name); + if (!cb) + return NULL; + + if (!ucv_is_callable(cb)) + return NULL; + + uc_vm_stack_push(vm, ucv_get(ucl->resource)); + uc_vm_stack_push(vm, ucv_get(cb)); + uc_vm_stack_push(vm, ucv_get(cb_obj)); + if (arg) + uc_vm_stack_push(vm, ucv_get(arg)); + + if (uc_vm_call(vm, true, !!arg + 1) != EXCEPTION_NONE) { + if (vm->exhandler) + vm->exhandler(vm, &vm->exception); + return NULL; + } + + return uc_vm_stack_pop(vm); +} + +static void +uc_write_str(struct uclient *cl, uc_value_t *val) +{ + uclient_write(cl, ucv_string_get(val), ucv_string_length(val)); +} + +static bool uc_cb_data_write(struct uclient *cl) +{ + struct uc_uclient_priv *ucl = cl->priv; + bool ret = false; + uc_value_t *val; + size_t len; + + val = __uc_uclient_cb(cl, "get_post_data", ucv_int64_new(ucl->offset)); + if (ucv_type(val) != UC_STRING) + goto out; + + len = ucv_string_length(val); + if (!len) + goto out; + + ucl->offset += len; + uc_write_str(cl, val); + ret = true; + +out: + ucv_put(val); + return ret; +} + +static uc_value_t * +uc_uclient_request(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + struct uc_uclient_priv *ucl; + uc_value_t *type = uc_fn_arg(0); + uc_value_t *arg = uc_fn_arg(1); + uc_value_t *cur; + const char *type_str = ucv_string_get(type); + + if (!cl || !type_str) + return NULL; + + ucl = cl->priv; + ucl->offset = 0; + + if (uclient_http_set_request_type(cl, type_str)) + return NULL; + + uclient_http_reset_headers(cl); + + if ((cur = ucv_property_get(arg, "headers")) != NULL) { + if (ucv_type(cur) != UC_OBJECT) + return NULL; + + ucv_object_foreach(cur, key, val) { + char *str; + + if (!val) + continue; + + if (ucv_type(val) == UC_STRING) { + uclient_http_set_header(cl, key, ucv_string_get(val)); + continue; + } + + str = ucv_to_string(uc_vm, val); + uclient_http_set_header(cl, key, str); + free(str); + } + } + + if ((cur = ucv_property_get(arg, "post_data")) != NULL) { + if (ucv_type(cur) != UC_STRING) + return NULL; + + uc_write_str(cl, cur); + } + + while (uc_cb_data_write(cl)) + if (uclient_pending_bytes(cl, true)) + return ucv_boolean_new(true); + + ucl->offset = -1; + if (uclient_request(cl)) + return NULL; + + return ucv_boolean_new(true); +} + +static uc_value_t * +uc_uclient_redirect(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + + if (!cl || uclient_http_redirect(cl)) + return NULL; + + return ucv_boolean_new(true); +} + +static uc_value_t * +uc_uclient_status(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + char addr[INET6_ADDRSTRLEN]; + uc_value_t *ret; + int port; + + if (!cl) + return NULL; + + ret = ucv_object_new(vm); + ucv_object_add(ret, "eof", ucv_boolean_new(cl->eof)); + ucv_object_add(ret, "data_eof", ucv_boolean_new(cl->data_eof)); + ucv_object_add(ret, "status", ucv_int64_new(cl->status_code)); + ucv_object_add(ret, "redirect", ucv_boolean_new(uclient_http_status_redirect(cl))); + + uclient_get_addr(addr, &port, &cl->local_addr); + ucv_object_add(ret, "local_addr", ucv_get(ucv_string_new(addr))); + ucv_object_add(ret, "local_port", ucv_get(ucv_int64_new(port))); + + uclient_get_addr(addr, &port, &cl->remote_addr); + ucv_object_add(ret, "remote_addr", ucv_get(ucv_string_new(addr))); + ucv_object_add(ret, "remote_port", ucv_get(ucv_int64_new(port))); + + return ret; +} + +static uc_value_t * +uc_uclient_read(uc_vm_t *vm, size_t nargs) +{ + struct uclient *cl = uc_fn_thisval("uclient"); + size_t len = ucv_int64_get(uc_fn_arg(0)); + uc_stringbuf_t *strbuf = NULL; + static char buf[4096]; + int cur; + + if (!cl) + return NULL; + + if (!len) + len = sizeof(buf); + + while (len > 0) { + cur = uclient_read(cl, buf, len); + if (cur <= 0) + break; + + if (!strbuf) + strbuf = ucv_stringbuf_new(); + + ucv_stringbuf_addstr(strbuf, buf, cur); + len -= cur; + } + + if (!strbuf) + return NULL; + + return ucv_stringbuf_finish(strbuf); +} + +static void +uc_uclient_cb(struct uclient *cl, const char *name, uc_value_t *arg) +{ + ucv_put(__uc_uclient_cb(cl, name, arg)); +} + +static void uc_cb_data_read(struct uclient *cl) +{ + uc_uclient_cb(cl, "data_read", NULL); +} + +static void uc_cb_data_sent(struct uclient *cl) +{ + struct uc_uclient_priv *ucl = cl->priv; + + if (ucl->offset < 0 || uclient_pending_bytes(cl, true)) + return; + + while (uc_cb_data_write(cl)) + if (uclient_pending_bytes(cl, true)) + return; + + ucl->offset = -1; + uclient_request(cl); +} + +static void uc_cb_data_eof(struct uclient *cl) +{ + uc_uclient_cb(cl, "data_eof", NULL); +} + +static void uc_cb_header_done(struct uclient *cl) +{ + uc_uclient_cb(cl, "header_done", NULL); +} + +static void uc_cb_error(struct uclient *cl, int code) +{ + uc_uclient_cb(cl, "error", ucv_int64_new(code)); +} + +static uc_value_t * +uc_uclient_new(uc_vm_t *vm, size_t nargs) +{ + struct uc_uclient_priv *ucl; + uc_value_t *url = uc_fn_arg(0); + uc_value_t *auth_str = uc_fn_arg(1); + uc_value_t *cb = uc_fn_arg(2); + static bool _init_done; + struct uclient *cl; + + if (!_init_done) { + uloop_init(); + _init_done = true; + } + + uc_vm = vm; + + if (ucv_type(url) != UC_STRING || + (auth_str && ucv_type(auth_str) != UC_STRING) || + ucv_type(cb) != UC_OBJECT) + return NULL; + + ucl = calloc(1, sizeof(*ucl)); + if (ucv_property_get(cb, "data_read")) + ucl->cb.data_read = uc_cb_data_read; + if (ucv_property_get(cb, "get_post_data")) + ucl->cb.data_sent = uc_cb_data_sent; + if (ucv_property_get(cb, "data_eof")) + ucl->cb.data_eof = uc_cb_data_eof; + if (ucv_property_get(cb, "header_done")) + ucl->cb.header_done = uc_cb_header_done; + if (ucv_property_get(cb, "error")) + ucl->cb.error = uc_cb_error; + + cl = uclient_new(ucv_string_get(url), ucv_string_get(auth_str), &ucl->cb); + if (!cl) { + free(ucl); + return NULL; + } + + cl->priv = ucl; + uc_uclient_register(ucl, cb); + ucl->resource = ucv_resource_new(uc_uclient_type, cl); + + return ucl->resource; +} +static const uc_function_list_t uclient_fns[] = { + { "free", uc_uclient_free }, + { "ssl_init", uc_uclient_ssl_init }, + { "set_url", uc_uclient_set_url }, + { "set_proxy_url", uc_uclient_set_proxy_url }, + { "set_timeout", uc_uclient_set_timeout }, + { "get_headers", uc_uclient_get_headers }, + + { "connect", uc_uclient_connect }, + { "disconnect", uc_uclient_disconnect }, + { "request", uc_uclient_request }, + { "redirect", uc_uclient_redirect }, + { "status", uc_uclient_status }, + + { "read", uc_uclient_read }, +}; + +static const uc_function_list_t global_fns[] = { + { "new", uc_uclient_new }, +}; + +void uc_module_init(uc_vm_t *vm, uc_value_t *scope) +{ + uc_uclient_type = uc_type_declare(vm, "uclient", uclient_fns, free_uclient); + registry = ucv_array_new(vm); + uc_vm_registry_set(vm, "uclient.registry", registry); + uc_function_list_register(scope, global_fns); +}