diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3b874a887..4ccb5ddd8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -64,6 +64,8 @@ sentry_target_sources_cwd(sentry transports/sentry_disk_transport.c transports/sentry_disk_transport.h transports/sentry_function_transport.c + transports/sentry_http_transport.c + transports/sentry_http_transport.h unwinder/sentry_unwinder.c ) @@ -119,11 +121,11 @@ endif() # transport if(SENTRY_TRANSPORT_CURL) sentry_target_sources_cwd(sentry - transports/sentry_transport_curl.c + transports/sentry_http_transport_curl.c ) elseif(SENTRY_TRANSPORT_WINHTTP) sentry_target_sources_cwd(sentry - transports/sentry_transport_winhttp.c + transports/sentry_http_transport_winhttp.c ) elseif(SENTRY_TRANSPORT_NONE) sentry_target_sources_cwd(sentry diff --git a/src/sentry_transport.c b/src/sentry_transport.c index e53a6f8af..6b63c5783 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -2,22 +2,6 @@ #include "sentry_alloc.h" #include "sentry_envelope.h" #include "sentry_options.h" -#include "sentry_ratelimiter.h" -#include "sentry_string.h" - -#ifdef SENTRY_TRANSPORT_COMPRESSION -# include "zlib.h" -#endif - -#define ENVELOPE_MIME "application/x-sentry-envelope" -#ifdef SENTRY_TRANSPORT_COMPRESSION -// The headers we use are: `x-sentry-auth`, `content-type`, `content-encoding`, -// `content-length` -# define MAX_HTTP_HEADERS 4 -#else -// The headers we use are: `x-sentry-auth`, `content-type`, `content-length` -# define MAX_HTTP_HEADERS 3 -#endif struct sentry_transport_s { void (*send_envelope_func)(sentry_envelope_t *envelope, void *state); @@ -158,162 +142,8 @@ sentry_transport_free(sentry_transport_t *transport) sentry_free(transport); } -#ifdef SENTRY_UNITTEST void * -sentry__transport_get_bgworker(sentry_transport_t *transport) +sentry__transport_get_state(sentry_transport_t *transport) { - // For HTTP transports (curl/winhttp), the transport state is the bgworker return transport ? transport->state : NULL; } -#endif - -#ifdef SENTRY_TRANSPORT_COMPRESSION -static bool -gzipped_with_compression(const char *body, const size_t body_len, - char **compressed_body, size_t *compressed_body_len) -{ - if (!body || body_len == 0) { - return false; - } - - z_stream stream; - memset(&stream, 0, sizeof(stream)); - stream.next_in = (unsigned char *)body; - stream.avail_in = (unsigned int)body_len; - - int err = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, - MAX_WBITS + 16, 9, Z_DEFAULT_STRATEGY); - if (err != Z_OK) { - SENTRY_WARNF("deflateInit2 failed: %d", err); - return false; - } - - size_t len = compressBound((unsigned long)body_len); - char *buffer = sentry_malloc(len); - if (!buffer) { - deflateEnd(&stream); - return false; - } - - while (err == Z_OK) { - stream.next_out = (unsigned char *)(buffer + stream.total_out); - stream.avail_out = (unsigned int)(len - stream.total_out); - err = deflate(&stream, Z_FINISH); - } - - if (err != Z_STREAM_END) { - SENTRY_WARNF("deflate failed: %d", err); - sentry_free(buffer); - buffer = NULL; - deflateEnd(&stream); - return false; - } - - *compressed_body_len = stream.total_out; - *compressed_body = buffer; - - deflateEnd(&stream); - return true; -} -#endif - -sentry_prepared_http_request_t * -sentry__prepare_http_request(sentry_envelope_t *envelope, - const sentry_dsn_t *dsn, const sentry_rate_limiter_t *rl, - const char *user_agent) -{ - if (!dsn || !dsn->is_valid) { - return NULL; - } - - size_t body_len = 0; - bool body_owned = true; - char *body = sentry_envelope_serialize_ratelimited( - envelope, rl, &body_len, &body_owned); - if (!body) { - return NULL; - } - -#ifdef SENTRY_TRANSPORT_COMPRESSION - bool compressed = false; - char *compressed_body = NULL; - size_t compressed_body_len = 0; - compressed = gzipped_with_compression( - body, body_len, &compressed_body, &compressed_body_len); - if (compressed) { - if (body_owned) { - sentry_free(body); - body_owned = false; - } - body = compressed_body; - body_len = compressed_body_len; - body_owned = true; - } -#endif - - sentry_prepared_http_request_t *req - = SENTRY_MAKE(sentry_prepared_http_request_t); - if (!req) { - if (body_owned) { - sentry_free(body); - } - return NULL; - } - req->headers = sentry_malloc( - sizeof(sentry_prepared_http_header_t) * MAX_HTTP_HEADERS); - if (!req->headers) { - sentry_free(req); - if (body_owned) { - sentry_free(body); - } - return NULL; - } - req->headers_len = 0; - - req->method = "POST"; - req->url = sentry__dsn_get_envelope_url(dsn); - - sentry_prepared_http_header_t *h; - h = &req->headers[req->headers_len++]; - h->key = "x-sentry-auth"; - h->value = sentry__dsn_get_auth_header(dsn, user_agent); - - h = &req->headers[req->headers_len++]; - h->key = "content-type"; - h->value = sentry__string_clone(ENVELOPE_MIME); - -#ifdef SENTRY_TRANSPORT_COMPRESSION - if (compressed) { - h = &req->headers[req->headers_len++]; - h->key = "content-encoding"; - h->value = sentry__string_clone("gzip"); - } -#endif - - h = &req->headers[req->headers_len++]; - h->key = "content-length"; - h->value = sentry__int64_to_string((int64_t)body_len); - - req->body = body; - req->body_len = body_len; - req->body_owned = body_owned; - - return req; -} - -void -sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) -{ - if (!req) { - return; - } - sentry_free(req->url); - for (size_t i = 0; i < req->headers_len; i++) { - sentry_free(req->headers[i].value); - } - sentry_free(req->headers); - if (req->body_owned) { - sentry_free(req->body); - } - sentry_free(req); -} diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 723d15085..036233284 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -3,7 +3,6 @@ #include "sentry_boot.h" #include "sentry_database.h" -#include "sentry_ratelimiter.h" #include "sentry_utils.h" /** @@ -56,44 +55,6 @@ sentry_transport_t *sentry__transport_new_default(void); size_t sentry__transport_dump_queue( sentry_transport_t *transport, sentry_run_t *run); -#ifdef SENTRY_UNITTEST -/** - * Test helper function to get the bgworker from a transport. - * Only available in unit tests and only works for HTTP transports. - */ -void *sentry__transport_get_bgworker(sentry_transport_t *transport); -#endif - -typedef struct sentry_prepared_http_header_s { - const char *key; - char *value; -} sentry_prepared_http_header_t; - -/** - * This represents a HTTP request, with method, url, headers and a body. - */ -typedef struct sentry_prepared_http_request_s { - const char *method; - char *url; - sentry_prepared_http_header_t *headers; - size_t headers_len; - char *body; - size_t body_len; - bool body_owned; -} sentry_prepared_http_request_t; - -/** - * Consumes the given envelope and transforms it into into a prepared http - * request. This can return NULL when all the items in the envelope have been - * rate limited. - */ -sentry_prepared_http_request_t *sentry__prepare_http_request( - sentry_envelope_t *envelope, const sentry_dsn_t *dsn, - const sentry_rate_limiter_t *rl, const char *user_agent); - -/** - * Free a previously allocated HTTP request. - */ -void sentry__prepared_http_request_free(sentry_prepared_http_request_t *req); +void *sentry__transport_get_state(sentry_transport_t *transport); #endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c new file mode 100644 index 000000000..24b1ba566 --- /dev/null +++ b/src/transports/sentry_http_transport.c @@ -0,0 +1,365 @@ +#include "sentry_http_transport.h" +#include "sentry_alloc.h" +#include "sentry_database.h" +#include "sentry_envelope.h" +#include "sentry_options.h" +#include "sentry_ratelimiter.h" +#include "sentry_string.h" +#include "sentry_transport.h" + +#ifdef SENTRY_TRANSPORT_COMPRESSION +# include "zlib.h" +#endif + +#include + +#define ENVELOPE_MIME "application/x-sentry-envelope" +#ifdef SENTRY_TRANSPORT_COMPRESSION +# define MAX_HTTP_HEADERS 4 +#else +# define MAX_HTTP_HEADERS 3 +#endif + +typedef struct { + sentry_dsn_t *dsn; + char *user_agent; + sentry_rate_limiter_t *ratelimiter; + void *client; + void (*free_client)(void *); + int (*start_client)(void *, const sentry_options_t *); + sentry_http_send_func_t send_func; + void (*shutdown_client)(void *client); +} http_transport_state_t; + +#ifdef SENTRY_TRANSPORT_COMPRESSION +static bool +gzipped_with_compression(const char *body, const size_t body_len, + char **compressed_body, size_t *compressed_body_len) +{ + if (!body || body_len == 0) { + return false; + } + + z_stream stream; + memset(&stream, 0, sizeof(stream)); + stream.next_in = (unsigned char *)body; + stream.avail_in = (unsigned int)body_len; + + int err = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + MAX_WBITS + 16, 9, Z_DEFAULT_STRATEGY); + if (err != Z_OK) { + SENTRY_WARNF("deflateInit2 failed: %d", err); + return false; + } + + size_t len = compressBound((unsigned long)body_len); + char *buffer = sentry_malloc(len); + if (!buffer) { + deflateEnd(&stream); + return false; + } + + while (err == Z_OK) { + stream.next_out = (unsigned char *)(buffer + stream.total_out); + stream.avail_out = (unsigned int)(len - stream.total_out); + err = deflate(&stream, Z_FINISH); + } + + if (err != Z_STREAM_END) { + SENTRY_WARNF("deflate failed: %d", err); + sentry_free(buffer); + buffer = NULL; + deflateEnd(&stream); + return false; + } + + *compressed_body_len = stream.total_out; + *compressed_body = buffer; + + deflateEnd(&stream); + return true; +} +#endif + +sentry_prepared_http_request_t * +sentry__prepare_http_request(sentry_envelope_t *envelope, + const sentry_dsn_t *dsn, const sentry_rate_limiter_t *rl, + const char *user_agent) +{ + if (!dsn || !dsn->is_valid) { + return NULL; + } + + size_t body_len = 0; + bool body_owned = true; + char *body = sentry_envelope_serialize_ratelimited( + envelope, rl, &body_len, &body_owned); + if (!body) { + return NULL; + } + +#ifdef SENTRY_TRANSPORT_COMPRESSION + bool compressed = false; + char *compressed_body = NULL; + size_t compressed_body_len = 0; + compressed = gzipped_with_compression( + body, body_len, &compressed_body, &compressed_body_len); + if (compressed) { + if (body_owned) { + sentry_free(body); + body_owned = false; + } + body = compressed_body; + body_len = compressed_body_len; + body_owned = true; + } +#endif + + sentry_prepared_http_request_t *req + = SENTRY_MAKE(sentry_prepared_http_request_t); + if (!req) { + if (body_owned) { + sentry_free(body); + } + return NULL; + } + req->headers = sentry_malloc( + sizeof(sentry_prepared_http_header_t) * MAX_HTTP_HEADERS); + if (!req->headers) { + sentry_free(req); + if (body_owned) { + sentry_free(body); + } + return NULL; + } + req->headers_len = 0; + + req->method = "POST"; + req->url = sentry__dsn_get_envelope_url(dsn); + + sentry_prepared_http_header_t *h; + h = &req->headers[req->headers_len++]; + h->key = "x-sentry-auth"; + h->value = sentry__dsn_get_auth_header(dsn, user_agent); + + h = &req->headers[req->headers_len++]; + h->key = "content-type"; + h->value = sentry__string_clone(ENVELOPE_MIME); + +#ifdef SENTRY_TRANSPORT_COMPRESSION + if (compressed) { + h = &req->headers[req->headers_len++]; + h->key = "content-encoding"; + h->value = sentry__string_clone("gzip"); + } +#endif + + h = &req->headers[req->headers_len++]; + h->key = "content-length"; + h->value = sentry__int64_to_string((int64_t)body_len); + + req->body = body; + req->body_len = body_len; + req->body_owned = body_owned; + + return req; +} + +void +sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) +{ + if (!req) { + return; + } + sentry_free(req->url); + for (size_t i = 0; i < req->headers_len; i++) { + sentry_free(req->headers[i].value); + } + sentry_free(req->headers); + if (req->body_owned) { + sentry_free(req->body); + } + sentry_free(req); +} + +static void +http_transport_state_free(void *_state) +{ + http_transport_state_t *state = _state; + if (state->free_client) { + state->free_client(state->client); + } + sentry__dsn_decref(state->dsn); + sentry_free(state->user_agent); + sentry__rate_limiter_free(state->ratelimiter); + sentry_free(state); +} + +static void +http_send_task(void *_envelope, void *_state) +{ + sentry_envelope_t *envelope = _envelope; + http_transport_state_t *state = _state; + + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + envelope, state->dsn, state->ratelimiter, state->user_agent); + if (!req) { + return; + } + + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + if (state->send_func(state->client, req, &resp)) { + if (resp.x_sentry_rate_limits) { + sentry__rate_limiter_update_from_header( + state->ratelimiter, resp.x_sentry_rate_limits); + } else if (resp.retry_after) { + sentry__rate_limiter_update_from_http_retry_after( + state->ratelimiter, resp.retry_after); + } else if (resp.status_code == 429) { + sentry__rate_limiter_update_from_429(state->ratelimiter); + } + } + sentry_free(resp.retry_after); + sentry_free(resp.x_sentry_rate_limits); + sentry__prepared_http_request_free(req); +} + +static int +http_transport_start(const sentry_options_t *options, void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + http_transport_state_t *state = sentry__bgworker_get_state(bgworker); + + sentry__bgworker_setname(bgworker, options->transport_thread_name); + + state->dsn = sentry__dsn_incref(options->dsn); + state->user_agent = sentry__string_clone(options->user_agent); + + if (state->start_client) { + int rv = state->start_client(state->client, options); + if (rv != 0) { + return rv; + } + } + + return sentry__bgworker_start(bgworker); +} + +static int +http_transport_flush(uint64_t timeout, void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + return sentry__bgworker_flush(bgworker, timeout); +} + +static int +http_transport_shutdown(uint64_t timeout, void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + http_transport_state_t *state = sentry__bgworker_get_state(bgworker); + + int rv = sentry__bgworker_shutdown(bgworker, timeout); + if (rv != 0 && state->shutdown_client) { + state->shutdown_client(state->client); + } + return rv; +} + +static void +http_transport_send_envelope(sentry_envelope_t *envelope, void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + sentry__bgworker_submit(bgworker, http_send_task, + (void (*)(void *))sentry_envelope_free, envelope); +} + +static bool +http_dump_task_cb(void *envelope, void *run) +{ + sentry__run_write_envelope( + (sentry_run_t *)run, (sentry_envelope_t *)envelope); + return true; +} + +static size_t +http_dump_queue(sentry_run_t *run, void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + return sentry__bgworker_foreach_matching( + bgworker, http_send_task, http_dump_task_cb, run); +} + +static http_transport_state_t * +http_transport_get_state(sentry_transport_t *transport) +{ + sentry_bgworker_t *bgworker = sentry__transport_get_state(transport); + return sentry__bgworker_get_state(bgworker); +} + +sentry_transport_t * +sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) +{ + http_transport_state_t *state = SENTRY_MAKE(http_transport_state_t); + if (!state) { + return NULL; + } + memset(state, 0, sizeof(http_transport_state_t)); + state->ratelimiter = sentry__rate_limiter_new(); + state->client = client; + state->send_func = send_func; + + sentry_bgworker_t *bgworker + = sentry__bgworker_new(state, http_transport_state_free); + if (!bgworker) { + http_transport_state_free(state); + return NULL; + } + + sentry_transport_t *transport + = sentry_transport_new(http_transport_send_envelope); + if (!transport) { + sentry__bgworker_decref(bgworker); + return NULL; + } + + sentry_transport_set_state(transport, bgworker); + sentry_transport_set_free_func( + transport, (void (*)(void *))sentry__bgworker_decref); + sentry_transport_set_startup_func(transport, http_transport_start); + sentry_transport_set_flush_func(transport, http_transport_flush); + sentry_transport_set_shutdown_func(transport, http_transport_shutdown); + sentry__transport_set_dump_func(transport, http_dump_queue); + + return transport; +} + +void +sentry__http_transport_set_free_client( + sentry_transport_t *transport, void (*free_client)(void *)) +{ + http_transport_get_state(transport)->free_client = free_client; +} + +void +sentry__http_transport_set_start_client(sentry_transport_t *transport, + int (*start_client)(void *, const sentry_options_t *)) +{ + http_transport_get_state(transport)->start_client = start_client; +} + +void +sentry__http_transport_set_shutdown_client( + sentry_transport_t *transport, void (*shutdown_client)(void *)) +{ + http_transport_get_state(transport)->shutdown_client = shutdown_client; +} + +#ifdef SENTRY_UNITTEST +void * +sentry__http_transport_get_bgworker(sentry_transport_t *transport) +{ + return sentry__transport_get_state(transport); +} +#endif diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h new file mode 100644 index 000000000..30493d564 --- /dev/null +++ b/src/transports/sentry_http_transport.h @@ -0,0 +1,57 @@ +#ifndef SENTRY_HTTP_TRANSPORT_H_INCLUDED +#define SENTRY_HTTP_TRANSPORT_H_INCLUDED + +#include "sentry_boot.h" +#include "sentry_ratelimiter.h" +#include "sentry_sync.h" +#include "sentry_transport.h" + +typedef struct sentry_prepared_http_header_s { + const char *key; + char *value; +} sentry_prepared_http_header_t; + +typedef struct sentry_prepared_http_request_s { + const char *method; + char *url; + sentry_prepared_http_header_t *headers; + size_t headers_len; + char *body; + size_t body_len; + bool body_owned; +} sentry_prepared_http_request_t; + +sentry_prepared_http_request_t *sentry__prepare_http_request( + sentry_envelope_t *envelope, const sentry_dsn_t *dsn, + const sentry_rate_limiter_t *rl, const char *user_agent); + +void sentry__prepared_http_request_free(sentry_prepared_http_request_t *req); + +typedef struct { + int status_code; + char *retry_after; + char *x_sentry_rate_limits; +} sentry_http_response_t; + +typedef bool (*sentry_http_send_func_t)(void *client, + sentry_prepared_http_request_t *req, sentry_http_response_t *resp); + +/** + * Creates a new HTTP transport with the given client and send function. + * Use the setter functions below to configure optional client callbacks. + */ +sentry_transport_t *sentry__http_transport_new( + void *client, sentry_http_send_func_t send_func); + +void sentry__http_transport_set_free_client( + sentry_transport_t *transport, void (*free_client)(void *)); +void sentry__http_transport_set_start_client(sentry_transport_t *transport, + int (*start_client)(void *, const sentry_options_t *)); +void sentry__http_transport_set_shutdown_client( + sentry_transport_t *transport, void (*shutdown_client)(void *)); + +#ifdef SENTRY_UNITTEST +void *sentry__http_transport_get_bgworker(sentry_transport_t *transport); +#endif + +#endif diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c new file mode 100644 index 000000000..b0c967f5c --- /dev/null +++ b/src/transports/sentry_http_transport_curl.c @@ -0,0 +1,257 @@ +#include "sentry_alloc.h" +#include "sentry_core.h" +#include "sentry_envelope.h" +#include "sentry_http_transport.h" +#include "sentry_options.h" +#include "sentry_string.h" +#include "sentry_transport.h" +#include "sentry_utils.h" + +#include +#include +#include + +#ifdef SENTRY_PLATFORM_NX +# include "sentry_transport_curl_nx.h" +#endif + +typedef struct { + CURL *curl_handle; + char *proxy; + char *ca_certs; + bool debug; +#ifdef SENTRY_PLATFORM_NX + void *nx_state; +#endif +} curl_client_t; + +static curl_client_t * +curl_client_new(void) +{ + curl_client_t *client = SENTRY_MAKE(curl_client_t); + if (!client) { + return NULL; + } + memset(client, 0, sizeof(curl_client_t)); + +#ifdef SENTRY_PLATFORM_NX + client->nx_state = sentry_nx_curl_state_new(); +#endif + return client; +} + +static void +curl_client_free(void *_client) +{ + curl_client_t *client = _client; + if (client->curl_handle) { + curl_easy_cleanup(client->curl_handle); + curl_global_cleanup(); + } + sentry_free(client->ca_certs); + sentry_free(client->proxy); +#ifdef SENTRY_PLATFORM_NX + sentry_nx_curl_state_free(client->nx_state); +#endif + sentry_free(client); +} + +static int +curl_client_start(void *_client, const sentry_options_t *options) +{ + curl_client_t *client = _client; + + static bool curl_initialized = false; + if (!curl_initialized) { + CURLcode rv = curl_global_init(CURL_GLOBAL_ALL); + if (rv != CURLE_OK) { + SENTRY_WARNF("`curl_global_init` failed with code `%d`", (int)rv); + return 1; + } + + curl_version_info_data *version_data + = curl_version_info(CURLVERSION_NOW); + + if (!version_data) { + SENTRY_WARN("Failed to retrieve `curl_version_info`"); + return 1; + } + + sentry_version_t curl_version = { + .major = (version_data->version_num >> 16) & 0xff, + .minor = (version_data->version_num >> 8) & 0xff, + .patch = version_data->version_num & 0xff, + }; + + if (!sentry__check_min_version( + curl_version, (sentry_version_t) { 7, 21, 7 })) { + SENTRY_WARNF("`libcurl` is at unsupported version `%u.%u.%u`", + curl_version.major, curl_version.minor, curl_version.patch); + return 1; + } + + if ((version_data->features & CURL_VERSION_ASYNCHDNS) == 0) { + SENTRY_WARN("`libcurl` was not compiled with feature `AsynchDNS`"); + return 1; + } + } + + client->proxy = sentry__string_clone(options->proxy); + client->ca_certs = sentry__string_clone(options->ca_certs); + client->curl_handle = curl_easy_init(); + client->debug = options->debug; + + if (!client->curl_handle) { + // In this case we don't start the worker at all, which means we can + // still dump all unsent envelopes to disk on shutdown. + SENTRY_WARN("`curl_easy_init` failed"); + return 1; + } + +#ifdef SENTRY_PLATFORM_NX + if (!sentry_nx_transport_start(client->nx_state, options)) { + return 1; + } +#endif + + return 0; +} + +static size_t +swallow_data( + char *UNUSED(ptr), size_t size, size_t nmemb, void *UNUSED(userdata)) +{ + return size * nmemb; +} + +static size_t +header_callback(char *buffer, size_t size, size_t nitems, void *userdata) +{ + size_t bytes = size * nitems; + sentry_http_response_t *info = userdata; + char *header = sentry__string_clone_n(buffer, bytes); + if (!header) { + return bytes; + } + + char *sep = strchr(header, ':'); + if (sep) { + *sep = '\0'; + sentry__string_ascii_lower(header); + if (sentry__string_eq(header, "retry-after")) { + info->retry_after = sentry__string_clone(sep + 1); + } else if (sentry__string_eq(header, "x-sentry-rate-limits")) { + info->x_sentry_rate_limits = sentry__string_clone(sep + 1); + } + } + + sentry_free(header); + return bytes; +} + +static bool +curl_send_task(void *_client, sentry_prepared_http_request_t *req, + sentry_http_response_t *resp) +{ + curl_client_t *client = (curl_client_t *)_client; + +#ifdef SENTRY_PLATFORM_NX + if (!sentry_nx_curl_connect(client->nx_state)) { + return false; // TODO should we dump the envelope to disk? + } +#endif + + struct curl_slist *headers = NULL; + headers = curl_slist_append(headers, "expect:"); + for (size_t i = 0; i < req->headers_len; i++) { + char buf[512]; + size_t written = (size_t)snprintf(buf, sizeof(buf), "%s:%s", + req->headers[i].key, req->headers[i].value); + if (written >= sizeof(buf)) { + continue; + } + buf[written] = '\0'; + headers = curl_slist_append(headers, buf); + } + + CURL *curl = client->curl_handle; + curl_easy_reset(curl); + if (client->debug) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, stderr); + // CURLOPT_WRITEFUNCTION will `fwrite` by default + } else { + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, swallow_data); + } + curl_easy_setopt(curl, CURLOPT_URL, req->url); + curl_easy_setopt(curl, CURLOPT_POST, (long)1); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); + curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); + + char error_buf[CURL_ERROR_SIZE]; + error_buf[0] = 0; + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buf); + + curl_easy_setopt(curl, CURLOPT_HEADERDATA, (void *)resp); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); + + if (client->proxy) { + curl_easy_setopt(curl, CURLOPT_PROXY, client->proxy); + } + if (client->ca_certs) { + curl_easy_setopt(curl, CURLOPT_CAINFO, client->ca_certs); + } + +#ifdef SENTRY_PLATFORM_NX + CURLcode rv = sentry_nx_curl_easy_setopt(client->nx_state, curl, req); +#else + CURLcode rv = CURLE_OK; +#endif + + if (rv == CURLE_OK) { + rv = curl_easy_perform(curl); + } + + if (rv == CURLE_OK) { + long response_code; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + resp->status_code = (int)response_code; + } else { + size_t len = strlen(error_buf); + if (len) { + if (error_buf[len - 1] == '\n') { + error_buf[len - 1] = 0; + } + SENTRY_WARNF("`curl_easy_perform` failed with code `%d`: %s", + (int)rv, error_buf); + } else { + SENTRY_WARNF("`curl_easy_perform` failed with code `%d`: %s", + (int)rv, curl_easy_strerror(rv)); + } + } + + curl_slist_free_all(headers); + return rv == CURLE_OK; +} + +sentry_transport_t * +sentry__transport_new_default(void) +{ + SENTRY_INFO("initializing curl transport"); + curl_client_t *client = curl_client_new(); + if (!client) { + return NULL; + } + + sentry_transport_t *transport + = sentry__http_transport_new(client, curl_send_task); + if (!transport) { + curl_client_free(client); + return NULL; + } + sentry__http_transport_set_free_client(transport, curl_client_free); + sentry__http_transport_set_start_client(transport, curl_client_start); + return transport; +} diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c new file mode 100644 index 000000000..0997d3562 --- /dev/null +++ b/src/transports/sentry_http_transport_winhttp.c @@ -0,0 +1,336 @@ +#include "sentry_alloc.h" +#include "sentry_core.h" +#include "sentry_envelope.h" +#include "sentry_http_transport.h" +#include "sentry_options.h" +#include "sentry_string.h" +#include "sentry_transport.h" +#include "sentry_utils.h" + +#ifdef SENTRY_PLATFORM_XBOX +# include "sentry_transport_xbox.h" +#endif + +#include +#include +#include + +typedef struct { + wchar_t *proxy; + wchar_t *proxy_username; + wchar_t *proxy_password; + HINTERNET session; + HINTERNET connect; + HINTERNET request; + bool debug; +} winhttp_client_t; + +static winhttp_client_t * +winhttp_client_new(void) +{ + winhttp_client_t *client = SENTRY_MAKE(winhttp_client_t); + if (!client) { + return NULL; + } + memset(client, 0, sizeof(winhttp_client_t)); + + return client; +} + +static void +winhttp_client_free(void *_client) +{ + winhttp_client_t *client = _client; + if (client->connect) { + WinHttpCloseHandle(client->connect); + } + if (client->session) { + WinHttpCloseHandle(client->session); + } + sentry_free(client->proxy_username); + sentry_free(client->proxy_password); + sentry_free(client->proxy); + sentry_free(client); +} + +// Function to extract and set credentials +static void +set_proxy_credentials(winhttp_client_t *state, const char *proxy) +{ + sentry_url_t url; + sentry__url_parse(&url, proxy, false); + if (url.username && url.password) { + // Convert user and pass to LPCWSTR + int user_wlen + = MultiByteToWideChar(CP_UTF8, 0, url.username, -1, NULL, 0); + int pass_wlen + = MultiByteToWideChar(CP_UTF8, 0, url.password, -1, NULL, 0); + wchar_t *user_w + = (wchar_t *)malloc((size_t)user_wlen * sizeof(wchar_t)); + wchar_t *pass_w + = (wchar_t *)malloc((size_t)pass_wlen * sizeof(wchar_t)); + MultiByteToWideChar(CP_UTF8, 0, url.username, -1, user_w, user_wlen); + MultiByteToWideChar(CP_UTF8, 0, url.password, -1, pass_w, pass_wlen); + + state->proxy_username = user_w; + state->proxy_password = pass_w; + } + sentry__url_cleanup(&url); +} + +static int +winhttp_client_start(void *_client, const sentry_options_t *opts) +{ + winhttp_client_t *client = _client; + + wchar_t *user_agent = sentry__string_to_wstr(opts->user_agent); + client->debug = opts->debug; + + const char *env_proxy = opts->dsn + ? getenv(opts->dsn->is_secure ? "https_proxy" : "http_proxy") + : NULL; + const char *proxy = opts->proxy ? opts->proxy : env_proxy ? env_proxy : ""; + + // ensure the proxy starts with `http://`, otherwise ignore it + if (proxy && strstr(proxy, "http://") == proxy) { + const char *ptr = proxy + 7; + const char *at_sign = strchr(ptr, '@'); + const char *slash = strchr(ptr, '/'); + if (at_sign && (!slash || at_sign < slash)) { + ptr = at_sign + 1; + set_proxy_credentials(client, proxy); + } + if (slash) { + char *copy = sentry__string_clone_n(ptr, (size_t)(slash - ptr)); + client->proxy = sentry__string_to_wstr(copy); + sentry_free(copy); + } else { + client->proxy = sentry__string_to_wstr(ptr); + } + } + + if (client->proxy) { + client->session + = WinHttpOpen(user_agent, WINHTTP_ACCESS_TYPE_NAMED_PROXY, + client->proxy, WINHTTP_NO_PROXY_BYPASS, 0); + } else { +#if _WIN32_WINNT >= 0x0603 + client->session + = WinHttpOpen(user_agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); +#endif + // On windows 8.0 or lower, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY does + // not work on error we fallback to WINHTTP_ACCESS_TYPE_DEFAULT_PROXY + if (!client->session) { + client->session + = WinHttpOpen(user_agent, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + } + } + sentry_free(user_agent); + + if (!client->session) { + SENTRY_WARN("`WinHttpOpen` failed"); + return 1; + } + + return 0; +} + +static void +winhttp_client_shutdown(void *_client) +{ + winhttp_client_t *client = _client; + // Seems like some requests are taking too long/hanging + // Just close them to make sure the background thread is exiting. + if (client->connect) { + WinHttpCloseHandle(client->connect); + client->connect = NULL; + } + + // NOTE: We need to close the session before closing the request. + // This will cancel all other requests which might be queued as well. + if (client->session) { + WinHttpCloseHandle(client->session); + client->session = NULL; + } + if (client->request) { + WinHttpCloseHandle(client->request); + client->request = NULL; + } +} + +static bool +winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, + sentry_http_response_t *resp) +{ + winhttp_client_t *client = (winhttp_client_t *)_client; + bool result = false; + + uint64_t started = sentry__monotonic_time(); + + wchar_t *url = sentry__string_to_wstr(req->url); + wchar_t *headers = NULL; + + URL_COMPONENTS url_components; + wchar_t hostname[128]; + wchar_t url_path[4096]; + memset(&url_components, 0, sizeof(URL_COMPONENTS)); + url_components.dwStructSize = sizeof(URL_COMPONENTS); + url_components.lpszHostName = hostname; + url_components.dwHostNameLength = 128; + url_components.lpszUrlPath = url_path; + url_components.dwUrlPathLength = 1024; + + WinHttpCrackUrl(url, 0, 0, &url_components); + +#ifdef SENTRY_PLATFORM_XBOX + // Ensure Xbox network connectivity is initialized before HTTP requests + if (!sentry__xbox_ensure_network_initialized()) { + SENTRY_WARN("Xbox: Network not ready, skipping HTTP request"); + goto exit; + } +#endif + + if (!client->connect) { + client->connect = WinHttpConnect(client->session, + url_components.lpszHostName, url_components.nPort, 0); + } + if (!client->connect) { + SENTRY_WARNF("`WinHttpConnect` failed with code `%d`", GetLastError()); + goto exit; + } + + bool is_secure = strstr(req->url, "https") == req->url; + client->request = WinHttpOpenRequest(client->connect, L"POST", + url_components.lpszUrlPath, NULL, WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, is_secure ? WINHTTP_FLAG_SECURE : 0); + if (!client->request) { + SENTRY_WARNF( + "`WinHttpOpenRequest` failed with code `%d`", GetLastError()); + goto exit; + } + + sentry_stringbuilder_t sb; + sentry__stringbuilder_init(&sb); + + for (size_t i = 0; i < req->headers_len; i++) { + sentry__stringbuilder_append(&sb, req->headers[i].key); + sentry__stringbuilder_append_char(&sb, ':'); + sentry__stringbuilder_append(&sb, req->headers[i].value); + sentry__stringbuilder_append(&sb, "\r\n"); + } + + char *headers_buf = sentry__stringbuilder_into_string(&sb); + headers = sentry__string_to_wstr(headers_buf); + + if (headers_buf) { + SENTRY_DEBUGF("sending request using winhttp to \"%s\":\n%s", req->url, + headers_buf); + } + sentry_free(headers_buf); + + if (!headers) { + SENTRY_WARN("winhttp_send_task: failed to allocate headers"); + goto exit; + } + + if (client->proxy_username && client->proxy_password) { + WinHttpSetCredentials(client->request, WINHTTP_AUTH_TARGET_PROXY, + WINHTTP_AUTH_SCHEME_BASIC, client->proxy_username, + client->proxy_password, 0); + } + + if ((result = WinHttpSendRequest(client->request, headers, (DWORD)-1, + (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, + 0))) { + WinHttpReceiveResponse(client->request, NULL); + + if (client->debug) { + // this is basically the example from: + // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpqueryheaders#examples + DWORD dwSize = 0; + LPVOID lpOutBuffer = NULL; + WinHttpQueryHeaders(client->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, + WINHTTP_NO_HEADER_INDEX); + + // Allocate memory for the buffer. + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + lpOutBuffer = sentry_malloc(dwSize); + + // Now, use WinHttpQueryHeaders to retrieve the header. + if (lpOutBuffer + && WinHttpQueryHeaders(client->request, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, lpOutBuffer, &dwSize, + WINHTTP_NO_HEADER_INDEX)) { + SENTRY_DEBUGF( + "received response:\n%S", (wchar_t *)lpOutBuffer); + } + sentry_free(lpOutBuffer); + } + } + + // lets just assume we won't have headers > 2k + wchar_t buf[2048]; + DWORD buf_size = sizeof(buf); + + DWORD status_code = 0; + DWORD status_code_size = sizeof(status_code); + + WinHttpQueryHeaders(client->request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, + WINHTTP_NO_HEADER_INDEX); + resp->status_code = (int)status_code; + + if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, + L"x-sentry-rate-limits", buf, &buf_size, + WINHTTP_NO_HEADER_INDEX)) { + resp->x_sentry_rate_limits = sentry__string_from_wstr(buf); + } else if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, + L"retry-after", buf, &buf_size, + WINHTTP_NO_HEADER_INDEX)) { + resp->retry_after = sentry__string_from_wstr(buf); + } + } else { + SENTRY_WARNF( + "`WinHttpSendRequest` failed with code `%d`", GetLastError()); + } + + uint64_t now = sentry__monotonic_time(); + SENTRY_DEBUGF("request handled in %llums", now - started); + +exit: + if (client->request) { + HINTERNET request = client->request; + client->request = NULL; + WinHttpCloseHandle(request); + } + sentry_free(url); + sentry_free(headers); + return result; +} + +sentry_transport_t * +sentry__transport_new_default(void) +{ + SENTRY_INFO("initializing winhttp transport"); + winhttp_client_t *client = winhttp_client_new(); + if (!client) { + return NULL; + } + + sentry_transport_t *transport + = sentry__http_transport_new(client, winhttp_send_task); + if (!transport) { + winhttp_client_free(client); + return NULL; + } + sentry__http_transport_set_free_client(transport, winhttp_client_free); + sentry__http_transport_set_start_client(transport, winhttp_client_start); + sentry__http_transport_set_shutdown_client( + transport, winhttp_client_shutdown); + return transport; +} diff --git a/src/transports/sentry_transport_curl.c b/src/transports/sentry_transport_curl.c deleted file mode 100644 index 4ae3ea324..000000000 --- a/src/transports/sentry_transport_curl.c +++ /dev/null @@ -1,349 +0,0 @@ -#include "sentry_alloc.h" -#include "sentry_core.h" -#include "sentry_database.h" -#include "sentry_envelope.h" -#include "sentry_options.h" -#include "sentry_ratelimiter.h" -#include "sentry_string.h" -#include "sentry_sync.h" -#include "sentry_transport.h" -#include "sentry_utils.h" - -#include -#include -#include - -#ifdef SENTRY_PLATFORM_NX -# include "sentry_transport_curl_nx.h" -#endif - -typedef struct curl_transport_state_s { - sentry_dsn_t *dsn; - CURL *curl_handle; - char *user_agent; - char *proxy; - char *ca_certs; - sentry_rate_limiter_t *ratelimiter; - bool debug; -#ifdef SENTRY_PLATFORM_NX - void *nx_state; -#endif -} curl_bgworker_state_t; - -struct header_info { - char *x_sentry_rate_limits; - char *retry_after; -}; - -static curl_bgworker_state_t * -sentry__curl_bgworker_state_new(void) -{ - curl_bgworker_state_t *state = SENTRY_MAKE(curl_bgworker_state_t); - if (!state) { - return NULL; - } - memset(state, 0, sizeof(curl_bgworker_state_t)); - - state->ratelimiter = sentry__rate_limiter_new(); -#ifdef SENTRY_PLATFORM_NX - state->nx_state = sentry_nx_curl_state_new(); -#endif - return state; -} - -static void -sentry__curl_bgworker_state_free(void *_state) -{ - curl_bgworker_state_t *state = _state; - if (state->curl_handle) { - curl_easy_cleanup(state->curl_handle); - curl_global_cleanup(); - } - sentry__dsn_decref(state->dsn); - sentry__rate_limiter_free(state->ratelimiter); - sentry_free(state->ca_certs); - sentry_free(state->user_agent); - sentry_free(state->proxy); -#ifdef SENTRY_PLATFORM_NX - sentry_nx_curl_state_free(state->nx_state); -#endif - sentry_free(state); -} - -static int -sentry__curl_transport_start( - const sentry_options_t *options, void *transport_state) -{ - static bool curl_initialized = false; - if (!curl_initialized) { - CURLcode rv = curl_global_init(CURL_GLOBAL_ALL); - if (rv != CURLE_OK) { - SENTRY_WARNF("`curl_global_init` failed with code `%d`", (int)rv); - return 1; - } - - curl_version_info_data *version_data - = curl_version_info(CURLVERSION_NOW); - - if (!version_data) { - SENTRY_WARN("Failed to retrieve `curl_version_info`"); - return 1; - } - - sentry_version_t curl_version = { - .major = (version_data->version_num >> 16) & 0xff, - .minor = (version_data->version_num >> 8) & 0xff, - .patch = version_data->version_num & 0xff, - }; - - if (!sentry__check_min_version( - curl_version, (sentry_version_t) { 7, 21, 7 })) { - SENTRY_WARNF("`libcurl` is at unsupported version `%u.%u.%u`", - curl_version.major, curl_version.minor, curl_version.patch); - return 1; - } - - if ((version_data->features & CURL_VERSION_ASYNCHDNS) == 0) { - SENTRY_WARN("`libcurl` was not compiled with feature `AsynchDNS`"); - return 1; - } - } - - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - curl_bgworker_state_t *state = sentry__bgworker_get_state(bgworker); - - state->dsn = sentry__dsn_incref(options->dsn); - state->proxy = sentry__string_clone(options->proxy); - state->user_agent = sentry__string_clone(options->user_agent); - state->ca_certs = sentry__string_clone(options->ca_certs); - state->curl_handle = curl_easy_init(); - state->debug = options->debug; - - sentry__bgworker_setname(bgworker, options->transport_thread_name); - - if (!state->curl_handle) { - // In this case we don’t start the worker at all, which means we can - // still dump all unsent envelopes to disk on shutdown. - SENTRY_WARN("`curl_easy_init` failed"); - return 1; - } - -#ifdef SENTRY_PLATFORM_NX - if (!sentry_nx_transport_start(state->nx_state, options)) { - return 1; - } -#endif - - return sentry__bgworker_start(bgworker); -} - -static int -sentry__curl_transport_flush(uint64_t timeout, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - return sentry__bgworker_flush(bgworker, timeout); -} - -static int -sentry__curl_transport_shutdown(uint64_t timeout, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - return sentry__bgworker_shutdown(bgworker, timeout); -} - -static size_t -swallow_data( - char *UNUSED(ptr), size_t size, size_t nmemb, void *UNUSED(userdata)) -{ - return size * nmemb; -} - -static size_t -header_callback(char *buffer, size_t size, size_t nitems, void *userdata) -{ - size_t bytes = size * nitems; - struct header_info *info = userdata; - char *header = sentry__string_clone_n(buffer, bytes); - if (!header) { - return bytes; - } - - char *sep = strchr(header, ':'); - if (sep) { - *sep = '\0'; - sentry__string_ascii_lower(header); - if (sentry__string_eq(header, "retry-after")) { - info->retry_after = sentry__string_clone(sep + 1); - } else if (sentry__string_eq(header, "x-sentry-rate-limits")) { - info->x_sentry_rate_limits = sentry__string_clone(sep + 1); - } - } - - sentry_free(header); - return bytes; -} - -static void -sentry__curl_send_task(void *_envelope, void *_state) -{ - sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; - curl_bgworker_state_t *state = (curl_bgworker_state_t *)_state; - -#ifdef SENTRY_PLATFORM_NX - if (!sentry_nx_curl_connect(state->nx_state)) { - return; // TODO should we dump the envelope to disk? - } -#endif - - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, state->user_agent); - if (!req) { - return; - } - - struct curl_slist *headers = NULL; - headers = curl_slist_append(headers, "expect:"); - for (size_t i = 0; i < req->headers_len; i++) { - char buf[512]; - size_t written = (size_t)snprintf(buf, sizeof(buf), "%s:%s", - req->headers[i].key, req->headers[i].value); - if (written >= sizeof(buf)) { - continue; - } - buf[written] = '\0'; - headers = curl_slist_append(headers, buf); - } - - CURL *curl = state->curl_handle; - curl_easy_reset(curl); - if (state->debug) { - curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, stderr); - // CURLOPT_WRITEFUNCTION will `fwrite` by default - } else { - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, swallow_data); - } - curl_easy_setopt(curl, CURLOPT_URL, req->url); - curl_easy_setopt(curl, CURLOPT_POST, (long)1); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); - curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); - - char error_buf[CURL_ERROR_SIZE]; - error_buf[0] = 0; - curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buf); - - struct header_info info; - info.retry_after = NULL; - info.x_sentry_rate_limits = NULL; - curl_easy_setopt(curl, CURLOPT_HEADERDATA, (void *)&info); - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); - - if (state->proxy) { - curl_easy_setopt(curl, CURLOPT_PROXY, state->proxy); - } - if (state->ca_certs) { - curl_easy_setopt(curl, CURLOPT_CAINFO, state->ca_certs); - } - -#ifdef SENTRY_PLATFORM_NX - CURLcode rv = sentry_nx_curl_easy_setopt(state->nx_state, curl, req); -#else - CURLcode rv = CURLE_OK; -#endif - - if (rv == CURLE_OK) { - rv = curl_easy_perform(curl); - } - - if (rv == CURLE_OK) { - long response_code; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); - - if (info.x_sentry_rate_limits) { - sentry__rate_limiter_update_from_header( - state->ratelimiter, info.x_sentry_rate_limits); - } else if (info.retry_after) { - sentry__rate_limiter_update_from_http_retry_after( - state->ratelimiter, info.retry_after); - } else if (response_code == 429) { - sentry__rate_limiter_update_from_429(state->ratelimiter); - } - } else { - size_t len = strlen(error_buf); - if (len) { - if (error_buf[len - 1] == '\n') { - error_buf[len - 1] = 0; - } - SENTRY_WARNF("`curl_easy_perform` failed with code `%d`: %s", - (int)rv, error_buf); - } else { - SENTRY_WARNF("`curl_easy_perform` failed with code `%d`: %s", - (int)rv, curl_easy_strerror(rv)); - } - } - - curl_slist_free_all(headers); - sentry_free(info.retry_after); - sentry_free(info.x_sentry_rate_limits); - sentry__prepared_http_request_free(req); -} - -static void -sentry__curl_transport_send_envelope( - sentry_envelope_t *envelope, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - sentry__bgworker_submit(bgworker, sentry__curl_send_task, - (void (*)(void *))sentry_envelope_free, envelope); -} - -static bool -sentry__curl_dump_task(void *envelope, void *run) -{ - sentry__run_write_envelope( - (sentry_run_t *)run, (sentry_envelope_t *)envelope); - return true; -} - -size_t -sentry__curl_dump_queue(sentry_run_t *run, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - return sentry__bgworker_foreach_matching( - bgworker, sentry__curl_send_task, sentry__curl_dump_task, run); -} - -sentry_transport_t * -sentry__transport_new_default(void) -{ - SENTRY_INFO("initializing curl transport"); - curl_bgworker_state_t *state = sentry__curl_bgworker_state_new(); - if (!state) { - return NULL; - } - - sentry_bgworker_t *bgworker - = sentry__bgworker_new(state, sentry__curl_bgworker_state_free); - if (!bgworker) { - return NULL; - } - - sentry_transport_t *transport - = sentry_transport_new(sentry__curl_transport_send_envelope); - if (!transport) { - sentry__bgworker_decref(bgworker); - return NULL; - } - sentry_transport_set_state(transport, bgworker); - sentry_transport_set_free_func( - transport, (void (*)(void *))sentry__bgworker_decref); - sentry_transport_set_startup_func(transport, sentry__curl_transport_start); - sentry_transport_set_flush_func(transport, sentry__curl_transport_flush); - sentry_transport_set_shutdown_func( - transport, sentry__curl_transport_shutdown); - sentry__transport_set_dump_func(transport, sentry__curl_dump_queue); - - return transport; -} diff --git a/src/transports/sentry_transport_winhttp.c b/src/transports/sentry_transport_winhttp.c deleted file mode 100644 index e60a6cc96..000000000 --- a/src/transports/sentry_transport_winhttp.c +++ /dev/null @@ -1,416 +0,0 @@ -#include "sentry_alloc.h" -#include "sentry_core.h" -#include "sentry_database.h" -#include "sentry_envelope.h" -#include "sentry_options.h" -#include "sentry_ratelimiter.h" -#include "sentry_string.h" -#include "sentry_sync.h" -#include "sentry_transport.h" -#include "sentry_utils.h" - -#ifdef SENTRY_PLATFORM_XBOX -# include "sentry_transport_xbox.h" -#endif - -#include -#include -#include - -typedef struct { - sentry_dsn_t *dsn; - wchar_t *user_agent; - wchar_t *proxy; - wchar_t *proxy_username; - wchar_t *proxy_password; - sentry_rate_limiter_t *ratelimiter; - HINTERNET session; - HINTERNET connect; - HINTERNET request; - bool debug; -} winhttp_bgworker_state_t; - -static winhttp_bgworker_state_t * -sentry__winhttp_bgworker_state_new(void) -{ - winhttp_bgworker_state_t *state = SENTRY_MAKE(winhttp_bgworker_state_t); - if (!state) { - return NULL; - } - memset(state, 0, sizeof(winhttp_bgworker_state_t)); - - state->ratelimiter = sentry__rate_limiter_new(); - - return state; -} - -static void -sentry__winhttp_bgworker_state_free(void *_state) -{ - winhttp_bgworker_state_t *state = _state; - if (state->connect) { - WinHttpCloseHandle(state->connect); - } - if (state->session) { - WinHttpCloseHandle(state->session); - } - sentry__dsn_decref(state->dsn); - sentry__rate_limiter_free(state->ratelimiter); - sentry_free(state->user_agent); - sentry_free(state->proxy_username); - sentry_free(state->proxy_password); - sentry_free(state->proxy); - sentry_free(state); -} - -// Function to extract and set credentials -static void -set_proxy_credentials(winhttp_bgworker_state_t *state, const char *proxy) -{ - sentry_url_t url; - sentry__url_parse(&url, proxy, false); - if (url.username && url.password) { - // Convert user and pass to LPCWSTR - int user_wlen - = MultiByteToWideChar(CP_UTF8, 0, url.username, -1, NULL, 0); - int pass_wlen - = MultiByteToWideChar(CP_UTF8, 0, url.password, -1, NULL, 0); - wchar_t *user_w - = (wchar_t *)malloc((size_t)user_wlen * sizeof(wchar_t)); - wchar_t *pass_w - = (wchar_t *)malloc((size_t)pass_wlen * sizeof(wchar_t)); - MultiByteToWideChar(CP_UTF8, 0, url.username, -1, user_w, user_wlen); - MultiByteToWideChar(CP_UTF8, 0, url.password, -1, pass_w, pass_wlen); - - state->proxy_username = user_w; - state->proxy_password = pass_w; - } - sentry__url_cleanup(&url); -} - -static int -sentry__winhttp_transport_start( - const sentry_options_t *opts, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - winhttp_bgworker_state_t *state = sentry__bgworker_get_state(bgworker); - - state->dsn = sentry__dsn_incref(opts->dsn); - state->user_agent = sentry__string_to_wstr(opts->user_agent); - state->debug = opts->debug; - - sentry__bgworker_setname(bgworker, opts->transport_thread_name); - - const char *env_proxy = opts->dsn - ? getenv(opts->dsn->is_secure ? "https_proxy" : "http_proxy") - : NULL; - const char *proxy = opts->proxy ? opts->proxy : env_proxy ? env_proxy : ""; - - // ensure the proxy starts with `http://`, otherwise ignore it - if (proxy && strstr(proxy, "http://") == proxy) { - const char *ptr = proxy + 7; - const char *at_sign = strchr(ptr, '@'); - const char *slash = strchr(ptr, '/'); - if (at_sign && (!slash || at_sign < slash)) { - ptr = at_sign + 1; - set_proxy_credentials(state, proxy); - } - if (slash) { - char *copy = sentry__string_clone_n(ptr, (size_t)(slash - ptr)); - state->proxy = sentry__string_to_wstr(copy); - sentry_free(copy); - } else { - state->proxy = sentry__string_to_wstr(ptr); - } - } - - if (state->proxy) { - state->session - = WinHttpOpen(state->user_agent, WINHTTP_ACCESS_TYPE_NAMED_PROXY, - state->proxy, WINHTTP_NO_PROXY_BYPASS, 0); - } else { -#if _WIN32_WINNT >= 0x0603 - state->session = WinHttpOpen(state->user_agent, - WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, - WINHTTP_NO_PROXY_BYPASS, 0); -#endif - // On windows 8.0 or lower, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY does - // not work on error we fallback to WINHTTP_ACCESS_TYPE_DEFAULT_PROXY - if (!state->session) { - state->session = WinHttpOpen(state->user_agent, - WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, - WINHTTP_NO_PROXY_BYPASS, 0); - } - } - if (!state->session) { - SENTRY_WARN("`WinHttpOpen` failed"); - return 1; - } - - return sentry__bgworker_start(bgworker); -} - -static int -sentry__winhttp_transport_flush(uint64_t timeout, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - return sentry__bgworker_flush(bgworker, timeout); -} - -static int -sentry__winhttp_transport_shutdown(uint64_t timeout, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - winhttp_bgworker_state_t *state = sentry__bgworker_get_state(bgworker); - - int rv = sentry__bgworker_shutdown(bgworker, timeout); - if (rv != 0) { - // Seems like some requests are taking too long/hanging - // Just close them to make sure the background thread is exiting. - if (state->connect) { - WinHttpCloseHandle(state->connect); - state->connect = NULL; - } - - // NOTE: We need to close the session before closing the request. - // This will cancel all other requests which might be queued as well. - if (state->session) { - WinHttpCloseHandle(state->session); - state->session = NULL; - } - if (state->request) { - WinHttpCloseHandle(state->request); - state->request = NULL; - } - } - - return rv; -} - -static void -sentry__winhttp_send_task(void *_envelope, void *_state) -{ - sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; - winhttp_bgworker_state_t *state = (winhttp_bgworker_state_t *)_state; - - uint64_t started = sentry__monotonic_time(); - - char *user_agent = sentry__string_from_wstr(state->user_agent); - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, user_agent); - if (!req) { - sentry_free(user_agent); - return; - } - - wchar_t *url = sentry__string_to_wstr(req->url); - wchar_t *headers = NULL; - - URL_COMPONENTS url_components; - wchar_t hostname[128]; - wchar_t url_path[4096]; - memset(&url_components, 0, sizeof(URL_COMPONENTS)); - url_components.dwStructSize = sizeof(URL_COMPONENTS); - url_components.lpszHostName = hostname; - url_components.dwHostNameLength = 128; - url_components.lpszUrlPath = url_path; - url_components.dwUrlPathLength = 1024; - - WinHttpCrackUrl(url, 0, 0, &url_components); - -#ifdef SENTRY_PLATFORM_XBOX - // Ensure Xbox network connectivity is initialized before HTTP requests - if (!sentry__xbox_ensure_network_initialized()) { - SENTRY_WARN("Xbox: Network not ready, skipping HTTP request"); - goto exit; - } -#endif - - if (!state->connect) { - state->connect = WinHttpConnect(state->session, - url_components.lpszHostName, url_components.nPort, 0); - } - if (!state->connect) { - SENTRY_WARNF("`WinHttpConnect` failed with code `%d`", GetLastError()); - goto exit; - } - - bool is_secure = strstr(req->url, "https") == req->url; - state->request = WinHttpOpenRequest(state->connect, L"POST", - url_components.lpszUrlPath, NULL, WINHTTP_NO_REFERER, - WINHTTP_DEFAULT_ACCEPT_TYPES, is_secure ? WINHTTP_FLAG_SECURE : 0); - if (!state->request) { - SENTRY_WARNF( - "`WinHttpOpenRequest` failed with code `%d`", GetLastError()); - goto exit; - } - - sentry_stringbuilder_t sb; - sentry__stringbuilder_init(&sb); - - for (size_t i = 0; i < req->headers_len; i++) { - sentry__stringbuilder_append(&sb, req->headers[i].key); - sentry__stringbuilder_append_char(&sb, ':'); - sentry__stringbuilder_append(&sb, req->headers[i].value); - sentry__stringbuilder_append(&sb, "\r\n"); - } - - char *headers_buf = sentry__stringbuilder_into_string(&sb); - headers = sentry__string_to_wstr(headers_buf); - - if (headers_buf) { - SENTRY_DEBUGF("sending request using winhttp to \"%s\":\n%s", req->url, - headers_buf); - } - sentry_free(headers_buf); - - if (!headers) { - SENTRY_WARN("sentry__winhttp_send_task: failed to allocate headers"); - goto exit; - } - - if (state->proxy_username && state->proxy_password) { - WinHttpSetCredentials(state->request, WINHTTP_AUTH_TARGET_PROXY, - WINHTTP_AUTH_SCHEME_BASIC, state->proxy_username, - state->proxy_password, 0); - } - - if (WinHttpSendRequest(state->request, headers, (DWORD)-1, - (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, 0)) { - WinHttpReceiveResponse(state->request, NULL); - - if (state->debug) { - // this is basically the example from: - // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpqueryheaders#examples - DWORD dwSize = 0; - LPVOID lpOutBuffer = NULL; - WinHttpQueryHeaders(state->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, - WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, - WINHTTP_NO_HEADER_INDEX); - - // Allocate memory for the buffer. - if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - lpOutBuffer = sentry_malloc(dwSize); - - // Now, use WinHttpQueryHeaders to retrieve the header. - if (lpOutBuffer - && WinHttpQueryHeaders(state->request, - WINHTTP_QUERY_RAW_HEADERS_CRLF, - WINHTTP_HEADER_NAME_BY_INDEX, lpOutBuffer, &dwSize, - WINHTTP_NO_HEADER_INDEX)) { - SENTRY_DEBUGF( - "received response:\n%S", (wchar_t *)lpOutBuffer); - } - sentry_free(lpOutBuffer); - } - } - - // lets just assume we won’t have headers > 2k - wchar_t buf[2048]; - DWORD buf_size = sizeof(buf); - - DWORD status_code = 0; - DWORD status_code_size = sizeof(status_code); - - if (WinHttpQueryHeaders(state->request, WINHTTP_QUERY_CUSTOM, - L"x-sentry-rate-limits", buf, &buf_size, - WINHTTP_NO_HEADER_INDEX)) { - char *h = sentry__string_from_wstr(buf); - if (h) { - sentry__rate_limiter_update_from_header(state->ratelimiter, h); - sentry_free(h); - } - } else if (WinHttpQueryHeaders(state->request, WINHTTP_QUERY_CUSTOM, - L"retry-after", buf, &buf_size, - WINHTTP_NO_HEADER_INDEX)) { - char *h = sentry__string_from_wstr(buf); - if (h) { - sentry__rate_limiter_update_from_http_retry_after( - state->ratelimiter, h); - sentry_free(h); - } - } else if (WinHttpQueryHeaders(state->request, - WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, - WINHTTP_HEADER_NAME_BY_INDEX, &status_code, - &status_code_size, WINHTTP_NO_HEADER_INDEX) - && status_code == 429) { - sentry__rate_limiter_update_from_429(state->ratelimiter); - } - } else { - SENTRY_WARNF( - "`WinHttpSendRequest` failed with code `%d`", GetLastError()); - } - - uint64_t now = sentry__monotonic_time(); - SENTRY_DEBUGF("request handled in %llums", now - started); - -exit: - if (state->request) { - HINTERNET request = state->request; - state->request = NULL; - WinHttpCloseHandle(request); - } - sentry_free(user_agent); - sentry_free(url); - sentry_free(headers); - sentry__prepared_http_request_free(req); -} - -static void -sentry__winhttp_transport_send_envelope( - sentry_envelope_t *envelope, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - sentry__bgworker_submit(bgworker, sentry__winhttp_send_task, - (void (*)(void *))sentry_envelope_free, envelope); -} - -static bool -sentry__winhttp_dump_task(void *envelope, void *run) -{ - sentry__run_write_envelope( - (sentry_run_t *)run, (sentry_envelope_t *)envelope); - return true; -} - -static size_t -sentry__winhttp_dump_queue(sentry_run_t *run, void *transport_state) -{ - sentry_bgworker_t *bgworker = (sentry_bgworker_t *)transport_state; - return sentry__bgworker_foreach_matching( - bgworker, sentry__winhttp_send_task, sentry__winhttp_dump_task, run); -} - -sentry_transport_t * -sentry__transport_new_default(void) -{ - SENTRY_INFO("initializing winhttp transport"); - winhttp_bgworker_state_t *state = sentry__winhttp_bgworker_state_new(); - if (!state) { - return NULL; - } - - sentry_bgworker_t *bgworker - = sentry__bgworker_new(state, sentry__winhttp_bgworker_state_free); - if (!bgworker) { - return NULL; - } - - sentry_transport_t *transport - = sentry_transport_new(sentry__winhttp_transport_send_envelope); - if (!transport) { - sentry__bgworker_decref(bgworker); - return NULL; - } - sentry_transport_set_state(transport, bgworker); - sentry_transport_set_free_func( - transport, (void (*)(void *))sentry__bgworker_decref); - sentry_transport_set_startup_func( - transport, sentry__winhttp_transport_start); - sentry_transport_set_flush_func(transport, sentry__winhttp_transport_flush); - sentry_transport_set_shutdown_func( - transport, sentry__winhttp_transport_shutdown); - sentry__transport_set_dump_func(transport, sentry__winhttp_dump_queue); - - return transport; -} diff --git a/tests/unit/test_basic.c b/tests/unit/test_basic.c index 919a52616..7c6ce7306 100644 --- a/tests/unit/test_basic.c +++ b/tests/unit/test_basic.c @@ -5,6 +5,7 @@ #include "sentry_sync.h" #include "sentry_testsupport.h" #include "sentry_transport.h" +#include "transports/sentry_http_transport.h" static void send_envelope_test_basic(sentry_envelope_t *envelope, void *data) @@ -328,7 +329,7 @@ SENTRY_TEST(basic_transport_thread_name) // Get the bgworker from the transport (for HTTP transports) sentry_bgworker_t *bgworker - = (sentry_bgworker_t *)sentry__transport_get_bgworker( + = (sentry_bgworker_t *)sentry__http_transport_get_bgworker( runtime_options->transport); TEST_ASSERT(!!bgworker); diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 5acfea790..06e686398 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -2,9 +2,9 @@ #include "sentry_json.h" #include "sentry_path.h" #include "sentry_testsupport.h" -#include "sentry_transport.h" #include "sentry_utils.h" #include "sentry_value.h" +#include "transports/sentry_http_transport.h" static char *const SERIALIZED_ENVELOPE_STR = "{\"dsn\":\"https://foo@sentry.invalid/42\","