diff options
Diffstat (limited to 'aclk/https_client.c')
-rw-r--r-- | aclk/https_client.c | 869 |
1 files changed, 869 insertions, 0 deletions
diff --git a/aclk/https_client.c b/aclk/https_client.c new file mode 100644 index 00000000..5385786b --- /dev/null +++ b/aclk/https_client.c @@ -0,0 +1,869 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "libnetdata/libnetdata.h" + +#include "https_client.h" + +#include "aclk_util.h" + +#include "daemon/global_statistics.h" + +#define DEFAULT_CHUNKED_RESPONSE_BUFFER_SIZE (4096) + +static const char *http_req_type_to_str(http_req_type_t req) { + switch (req) { + case HTTP_REQ_GET: + return "GET"; + case HTTP_REQ_POST: + return "POST"; + case HTTP_REQ_CONNECT: + return "CONNECT"; + default: + return "unknown"; + } +} + +#define TRANSFER_ENCODING_CHUNKED (-2) + +#define HTTP_PARSE_CTX_INITIALIZER { .state = HTTP_PARSE_INITIAL, .content_length = -1, .http_code = 0 } +void http_parse_ctx_create(http_parse_ctx *ctx) +{ + ctx->state = HTTP_PARSE_INITIAL; + ctx->content_length = -1; + ctx->http_code = 0; + ctx->headers = c_rhash_new(0); + ctx->flags = HTTP_PARSE_FLAGS_DEFAULT; +} + +void http_parse_ctx_destroy(http_parse_ctx *ctx) +{ + c_rhash_iter_t iter; + const char *key; + + c_rhash_iter_t_initialize(&iter); + while ( !c_rhash_iter_str_keys(ctx->headers, &iter, &key) ) { + void *val; + c_rhash_get_ptr_by_str(ctx->headers, key, &val); + freez(val); + } + + c_rhash_destroy(ctx->headers); +} + +#define POLL_TO_MS 100 + +#define HTTP_LINE_TERM "\x0D\x0A" +#define RESP_PROTO "HTTP/1.1 " +#define HTTP_KEYVAL_SEPARATOR ": " +#define HTTP_HDR_BUFFER_SIZE 1024 +#define PORT_STR_MAX_BYTES 12 + +static int process_http_hdr(http_parse_ctx *parse_ctx, const char *key, const char *val) +{ + // currently we care only about specific headers + // we can skip the rest + if (parse_ctx->content_length < 0 && !strcmp("content-length", key)) { + if (parse_ctx->content_length == TRANSFER_ENCODING_CHUNKED) { + netdata_log_error("Content-length and transfer-encoding: chunked headers are mutually exclusive"); + return 1; + } + if (parse_ctx->content_length != -1) { + netdata_log_error("Duplicate content-length header"); + return 1; + } + parse_ctx->content_length = str2u(val); + if (parse_ctx->content_length < 0) { + netdata_log_error("Invalid content-length %d", parse_ctx->content_length); + return 1; + } + return 0; + } + if (!strcmp("transfer-encoding", key)) { + if (!strcmp("chunked", val)) { + if (parse_ctx->content_length != -1) { + netdata_log_error("Content-length and transfer-encoding: chunked headers are mutually exclusive"); + return 1; + } + parse_ctx->content_length = TRANSFER_ENCODING_CHUNKED; + } + return 0; + } + char *val_cpy = strdupz(val); + c_rhash_insert_str_ptr(parse_ctx->headers, key, val_cpy); + return 0; +} + +const char *get_http_header_by_name(http_parse_ctx *ctx, const char *name) +{ + const char *ret; + if (c_rhash_get_ptr_by_str(ctx->headers, name, (void**)&ret)) + return NULL; + + return ret; +} + +static int parse_http_hdr(rbuf_t buf, http_parse_ctx *parse_ctx) +{ + int idx, idx_end; + char buf_key[HTTP_HDR_BUFFER_SIZE]; + char buf_val[HTTP_HDR_BUFFER_SIZE]; + char *ptr = buf_key; + if (!rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx_end)) { + netdata_log_error("CRLF expected"); + return 1; + } + + char *separator = rbuf_find_bytes(buf, HTTP_KEYVAL_SEPARATOR, strlen(HTTP_KEYVAL_SEPARATOR), &idx); + if (!separator) { + netdata_log_error("Missing Key/Value separator"); + return 1; + } + if (idx >= HTTP_HDR_BUFFER_SIZE) { + netdata_log_error("Key name is too long"); + return 1; + } + + rbuf_pop(buf, buf_key, idx); + buf_key[idx] = 0; + + rbuf_bump_tail(buf, strlen(HTTP_KEYVAL_SEPARATOR)); + idx_end -= strlen(HTTP_KEYVAL_SEPARATOR) + idx; + if (idx_end >= HTTP_HDR_BUFFER_SIZE) { + netdata_log_error("Value of key \"%s\" too long", buf_key); + return 1; + } + + rbuf_pop(buf, buf_val, idx_end); + buf_val[idx_end] = 0; + + for (ptr = buf_key; *ptr; ptr++) + *ptr = tolower(*ptr); + + if (process_http_hdr(parse_ctx, buf_key, buf_val)) + return 1; + + return 0; +} + +static inline void chunked_response_buffer_grow_by(http_parse_ctx *parse_ctx, size_t size) +{ + if (unlikely(parse_ctx->chunked_response_size == 0)) { + parse_ctx->chunked_response = mallocz(size); + parse_ctx->chunked_response_size = size; + return; + } + parse_ctx->chunked_response = reallocz((void *)parse_ctx->chunked_response, parse_ctx->chunked_response_size + size); + parse_ctx->chunked_response_size += size; +} + +static int process_chunked_content(rbuf_t buf, http_parse_ctx *parse_ctx) +{ + int idx; + size_t bytes_to_copy; + + do { + switch (parse_ctx->chunked_content_state) { + case CHUNKED_CONTENT_CHUNK_SIZE: + if (!rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx)) { + if (rbuf_bytes_available(buf) >= rbuf_get_capacity(buf)) + return HTTP_PARSE_ERROR; + return HTTP_PARSE_NEED_MORE_DATA; + } + if (idx == 0) { + parse_ctx->chunked_content_state = CHUNKED_CONTENT_FINAL_CRLF; + continue; + } + if (idx >= HTTP_HDR_BUFFER_SIZE) { + netdata_log_error("Chunk size is too long"); + return HTTP_PARSE_ERROR; + } + char buf_size[HTTP_HDR_BUFFER_SIZE]; + rbuf_pop(buf, buf_size, idx); + buf_size[idx] = 0; + long chunk_size = strtol(buf_size, NULL, 16); + if (chunk_size < 0 || chunk_size == LONG_MAX) { + netdata_log_error("Chunk size out of range"); + return HTTP_PARSE_ERROR; + } + parse_ctx->chunk_size = chunk_size; + if (parse_ctx->chunk_size == 0) { + if (errno == EINVAL) { + netdata_log_error("Invalid chunk size"); + return HTTP_PARSE_ERROR; + } + parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_END_CRLF; + continue; + } + parse_ctx->chunk_got = 0; + chunked_response_buffer_grow_by(parse_ctx, parse_ctx->chunk_size); + rbuf_bump_tail(buf, strlen(HTTP_LINE_TERM)); + parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_DATA; + // fallthrough + case CHUNKED_CONTENT_CHUNK_DATA: + if (!(bytes_to_copy = rbuf_bytes_available(buf))) + return HTTP_PARSE_NEED_MORE_DATA; + if (bytes_to_copy > parse_ctx->chunk_size - parse_ctx->chunk_got) + bytes_to_copy = parse_ctx->chunk_size - parse_ctx->chunk_got; + rbuf_pop(buf, parse_ctx->chunked_response + parse_ctx->chunked_response_written, bytes_to_copy); + parse_ctx->chunk_got += bytes_to_copy; + parse_ctx->chunked_response_written += bytes_to_copy; + if (parse_ctx->chunk_got != parse_ctx->chunk_size) + continue; + parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_END_CRLF; + // fallthrough + case CHUNKED_CONTENT_FINAL_CRLF: + case CHUNKED_CONTENT_CHUNK_END_CRLF: + if (rbuf_bytes_available(buf) < strlen(HTTP_LINE_TERM)) + return HTTP_PARSE_NEED_MORE_DATA; + char buf_crlf[strlen(HTTP_LINE_TERM)]; + rbuf_pop(buf, buf_crlf, strlen(HTTP_LINE_TERM)); + if (memcmp(buf_crlf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM))) { + netdata_log_error("CRLF expected"); + return HTTP_PARSE_ERROR; + } + if (parse_ctx->chunked_content_state == CHUNKED_CONTENT_FINAL_CRLF) { + if (parse_ctx->chunked_response_size != parse_ctx->chunked_response_written) + netdata_log_error("Chunked response size mismatch"); + chunked_response_buffer_grow_by(parse_ctx, 1); + parse_ctx->chunked_response[parse_ctx->chunked_response_written] = 0; + return HTTP_PARSE_SUCCESS; + } + if (parse_ctx->chunk_size == 0) { + parse_ctx->chunked_content_state = CHUNKED_CONTENT_FINAL_CRLF; + continue; + } + parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_SIZE; + continue; + } + } while(1); +} + +http_parse_rc parse_http_response(rbuf_t buf, http_parse_ctx *parse_ctx) +{ + int idx; + char rc[4]; + + do { + if (parse_ctx->state != HTTP_PARSE_CONTENT && !rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx)) + return HTTP_PARSE_NEED_MORE_DATA; + switch (parse_ctx->state) { + case HTTP_PARSE_INITIAL: + if (rbuf_memcmp_n(buf, RESP_PROTO, strlen(RESP_PROTO))) { + netdata_log_error("Expected response to start with \"%s\"", RESP_PROTO); + return HTTP_PARSE_ERROR; + } + rbuf_bump_tail(buf, strlen(RESP_PROTO)); + if (rbuf_pop(buf, rc, 4) != 4) { + netdata_log_error("Expected HTTP status code"); + return HTTP_PARSE_ERROR; + } + if (rc[3] != ' ') { + netdata_log_error("Expected space after HTTP return code"); + return HTTP_PARSE_ERROR; + } + rc[3] = 0; + parse_ctx->http_code = atoi(rc); + if (parse_ctx->http_code < 100 || parse_ctx->http_code >= 600) { + netdata_log_error("HTTP code not in range 100 to 599"); + return HTTP_PARSE_ERROR; + } + + rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx); + + rbuf_bump_tail(buf, idx + strlen(HTTP_LINE_TERM)); + + parse_ctx->state = HTTP_PARSE_HEADERS; + break; + case HTTP_PARSE_HEADERS: + if (!idx) { + parse_ctx->state = HTTP_PARSE_CONTENT; + rbuf_bump_tail(buf, strlen(HTTP_LINE_TERM)); + break; + } + if (parse_http_hdr(buf, parse_ctx)) + return HTTP_PARSE_ERROR; + rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx); + rbuf_bump_tail(buf, idx + strlen(HTTP_LINE_TERM)); + break; + case HTTP_PARSE_CONTENT: + // replies like CONNECT etc. do not have content + if (parse_ctx->content_length == TRANSFER_ENCODING_CHUNKED) + return process_chunked_content(buf, parse_ctx); + + if (parse_ctx->content_length < 0) + return HTTP_PARSE_SUCCESS; + + if (parse_ctx->flags & HTTP_PARSE_FLAG_DONT_WAIT_FOR_CONTENT) + return HTTP_PARSE_SUCCESS; + + if (rbuf_bytes_available(buf) >= (size_t)parse_ctx->content_length) + return HTTP_PARSE_SUCCESS; + return HTTP_PARSE_NEED_MORE_DATA; + } + } while(1); +} + +typedef struct https_req_ctx { + https_req_t *request; + + int sock; + rbuf_t buf_rx; + + struct pollfd poll_fd; + + SSL_CTX *ssl_ctx; + SSL *ssl; + + size_t written; + + int self_signed_allowed; + + http_parse_ctx parse_ctx; + + time_t req_start_time; +} https_req_ctx_t; + +static int https_req_check_timedout(https_req_ctx_t *ctx) { + if (now_realtime_sec() > ctx->req_start_time + ctx->request->timeout_s) { + netdata_log_error("request timed out"); + return 1; + } + return 0; +} + +static char *_ssl_err_tos(int err) +{ + switch(err){ + case SSL_ERROR_SSL: + return "SSL_ERROR_SSL"; + case SSL_ERROR_WANT_READ: + return "SSL_ERROR_WANT_READ"; + case SSL_ERROR_WANT_WRITE: + return "SSL_ERROR_WANT_WRITE"; + case SSL_ERROR_NONE: + return "SSL_ERROR_NONE"; + case SSL_ERROR_ZERO_RETURN: + return "SSL_ERROR_ZERO_RETURN"; + case SSL_ERROR_WANT_CONNECT: + return "SSL_ERROR_WANT_CONNECT"; + case SSL_ERROR_WANT_ACCEPT: + return "SSL_ERROR_WANT_ACCEPT"; + } + return "Unknown!!!"; +} + +static int socket_write_all(https_req_ctx_t *ctx, char *data, size_t data_len) { + ctx->written = 0; + ctx->poll_fd.events = POLLOUT; + + do { + int ret = poll(&ctx->poll_fd, 1, POLL_TO_MS); + if (ret < 0) { + netdata_log_error("poll error"); + return 1; + } + if (ret == 0) { + if (https_req_check_timedout(ctx)) { + netdata_log_error("Poll timed out"); + return 2; + } + continue; + } + + ret = write(ctx->sock, &data[ctx->written], data_len - ctx->written); + if (ret > 0) { + ctx->written += ret; + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + netdata_log_error("Error writing to socket"); + return 3; + } + } while (ctx->written < data_len); + + return 0; +} + +static int ssl_write_all(https_req_ctx_t *ctx, char *data, size_t data_len) { + ctx->written = 0; + ctx->poll_fd.events |= POLLOUT; + + do { + int ret = poll(&ctx->poll_fd, 1, POLL_TO_MS); + if (ret < 0) { + netdata_log_error("poll error"); + return 1; + } + if (ret == 0) { + if (https_req_check_timedout(ctx)) { + netdata_log_error("Poll timed out"); + return 2; + } + continue; + } + ctx->poll_fd.events = 0; + + ret = SSL_write(ctx->ssl, &data[ctx->written], data_len - ctx->written); + if (ret > 0) { + ctx->written += ret; + } else { + ret = SSL_get_error(ctx->ssl, ret); + switch (ret) { + case SSL_ERROR_WANT_READ: + ctx->poll_fd.events |= POLLIN; + break; + case SSL_ERROR_WANT_WRITE: + ctx->poll_fd.events |= POLLOUT; + break; + default: + netdata_log_error("SSL_write Err: %s", _ssl_err_tos(ret)); + return 3; + } + } + } while (ctx->written < data_len); + + return 0; +} + +static inline int https_client_write_all(https_req_ctx_t *ctx, char *data, size_t data_len) { + if (ctx->ssl_ctx) + return ssl_write_all(ctx, data, data_len); + return socket_write_all(ctx, data, data_len); +} + +static int read_parse_response(https_req_ctx_t *ctx) { + int ret; + char *ptr; + size_t size; + + ctx->poll_fd.events = POLLIN; + do { + ret = poll(&ctx->poll_fd, 1, POLL_TO_MS); + if (ret < 0) { + netdata_log_error("poll error"); + return 1; + } + if (ret == 0) { + if (https_req_check_timedout(ctx)) { + netdata_log_error("Poll timed out"); + return 2; + } + if (!ctx->ssl_ctx) + continue; + } + ctx->poll_fd.events = 0; + + do { + ptr = rbuf_get_linear_insert_range(ctx->buf_rx, &size); + + if (ctx->ssl_ctx) + ret = SSL_read(ctx->ssl, ptr, size); + else + ret = read(ctx->sock, ptr, size); + + if (ret > 0) { + rbuf_bump_head(ctx->buf_rx, ret); + } else { + if (ctx->ssl_ctx) { + ret = SSL_get_error(ctx->ssl, ret); + switch (ret) { + case SSL_ERROR_WANT_READ: + ctx->poll_fd.events |= POLLIN; + break; + case SSL_ERROR_WANT_WRITE: + ctx->poll_fd.events |= POLLOUT; + break; + default: + netdata_log_error("SSL_read Err: %s", _ssl_err_tos(ret)); + return 3; + } + } else { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + netdata_log_error("write error"); + return 3; + } + ctx->poll_fd.events |= POLLIN; + } + } + } while (ctx->poll_fd.events == 0 && rbuf_bytes_free(ctx->buf_rx) > 0); + } while (!(ret = parse_http_response(ctx->buf_rx, &ctx->parse_ctx))); + + if (ret != HTTP_PARSE_SUCCESS) { + netdata_log_error("Error parsing HTTP response"); + return 1; + } + + return 0; +} + +#define TX_BUFFER_SIZE 8192 +#define RX_BUFFER_SIZE (TX_BUFFER_SIZE*2) +static int handle_http_request(https_req_ctx_t *ctx) { + BUFFER *hdr = buffer_create(TX_BUFFER_SIZE, &netdata_buffers_statistics.buffers_aclk); + int rc = 0; + + http_parse_ctx_create(&ctx->parse_ctx); + + // Prepare data to send + switch (ctx->request->request_type) { + case HTTP_REQ_CONNECT: + buffer_strcat(hdr, "CONNECT "); + break; + case HTTP_REQ_GET: + buffer_strcat(hdr, "GET "); + break; + case HTTP_REQ_POST: + buffer_strcat(hdr, "POST "); + break; + default: + netdata_log_error("Unknown HTTPS request type!"); + rc = 1; + goto err_exit; + } + + if (ctx->request->request_type == HTTP_REQ_CONNECT) { + buffer_strcat(hdr, ctx->request->host); + buffer_sprintf(hdr, ":%d", ctx->request->port); + } else { + buffer_strcat(hdr, ctx->request->url); + } + + buffer_strcat(hdr, HTTP_1_1 HTTP_ENDL); + + //TODO Headers! + if (ctx->request->request_type != HTTP_REQ_CONNECT) { + buffer_sprintf(hdr, "Host: %s\x0D\x0A", ctx->request->host); + } + buffer_strcat(hdr, "User-Agent: Netdata/rocks newhttpclient\x0D\x0A"); + + if (ctx->request->request_type == HTTP_REQ_POST && ctx->request->payload && ctx->request->payload_size) { + buffer_sprintf(hdr, "Content-Length: %zu\x0D\x0A", ctx->request->payload_size); + } + if (ctx->request->proxy_username) { + size_t creds_plain_len = strlen(ctx->request->proxy_username) + strlen(ctx->request->proxy_password) + 1 /* ':' */; + char *creds_plain = callocz(1, creds_plain_len + 1); + char *ptr = creds_plain; + strcpy(ptr, ctx->request->proxy_username); + ptr += strlen(ctx->request->proxy_username); + *ptr++ = ':'; + strcpy(ptr, ctx->request->proxy_password); + + int creds_base64_len = (((4 * creds_plain_len / 3) + 3) & ~3); + // OpenSSL encoder puts newline every 64 output bytes + // we remove those but during encoding we need that space in the buffer + creds_base64_len += (1+(creds_base64_len/64)) * strlen("\n"); + char *creds_base64 = callocz(1, creds_base64_len + 1); + base64_encode_helper((unsigned char*)creds_base64, &creds_base64_len, (unsigned char*)creds_plain, creds_plain_len); + buffer_sprintf(hdr, "Proxy-Authorization: Basic %s\x0D\x0A", creds_base64); + freez(creds_plain); + } + + buffer_strcat(hdr, "\x0D\x0A"); + + // Send the request + if (https_client_write_all(ctx, hdr->buffer, hdr->len)) { + netdata_log_error("Couldn't write HTTP request header into SSL connection"); + rc = 2; + goto err_exit; + } + + if (ctx->request->request_type == HTTP_REQ_POST && ctx->request->payload && ctx->request->payload_size) { + if (https_client_write_all(ctx, ctx->request->payload, ctx->request->payload_size)) { + netdata_log_error("Couldn't write payload into SSL connection"); + rc = 3; + goto err_exit; + } + } + + // Read The Response + if (read_parse_response(ctx)) { + netdata_log_error("Error reading or parsing response from server"); + if (ctx->parse_ctx.chunked_response) + freez(ctx->parse_ctx.chunked_response); + rc = 4; + goto err_exit; + } + +err_exit: + buffer_free(hdr); + return rc; +} + +static int cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) +{ + X509 *err_cert; + int err, depth; + char *err_str; + + if (!preverify_ok) { + err = X509_STORE_CTX_get_error(ctx); + depth = X509_STORE_CTX_get_error_depth(ctx); + err_cert = X509_STORE_CTX_get_current_cert(ctx); + err_str = X509_NAME_oneline(X509_get_subject_name(err_cert), NULL, 0); + + netdata_log_error("Cert Chain verify error:num=%d:%s:depth=%d:%s", err, + X509_verify_cert_error_string(err), depth, err_str); + + free(err_str); + } + +#ifdef ACLK_SSL_ALLOW_SELF_SIGNED + if (!preverify_ok && err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) + { + preverify_ok = 1; + netdata_log_error("Self Signed Certificate Accepted as the agent was built with ACLK_SSL_ALLOW_SELF_SIGNED"); + } +#endif + + return preverify_ok; +} + +int https_request(https_req_t *request, https_req_response_t *response) { + int rc = 1, ret; + char connect_port_str[PORT_STR_MAX_BYTES]; + + const char *connect_host = request->proxy_host ? request->proxy_host : request->host; + int connect_port = request->proxy_host ? request->proxy_port : request->port; + struct timeval timeout = { .tv_sec = request->timeout_s, .tv_usec = 0 }; + + https_req_ctx_t *ctx = callocz(1, sizeof(https_req_ctx_t)); + ctx->req_start_time = now_realtime_sec(); + + ctx->buf_rx = rbuf_create(RX_BUFFER_SIZE); + if (!ctx->buf_rx) { + netdata_log_error("Couldn't allocate buffer for RX data"); + goto exit_req_ctx; + } + + snprintfz(connect_port_str, PORT_STR_MAX_BYTES, "%d", connect_port); + + ctx->sock = connect_to_this_ip46(IPPROTO_TCP, SOCK_STREAM, connect_host, 0, connect_port_str, &timeout); + if (ctx->sock < 0) { + netdata_log_error("Error connecting TCP socket to \"%s\"", connect_host); + goto exit_buf_rx; + } + + if (fcntl(ctx->sock, F_SETFL, fcntl(ctx->sock, F_GETFL, 0) | O_NONBLOCK) == -1) { + netdata_log_error("Error setting O_NONBLOCK to TCP socket."); + goto exit_sock; + } + + ctx->poll_fd.fd = ctx->sock; + + // Do the CONNECT if proxy is used + if (request->proxy_host) { + https_req_t req = HTTPS_REQ_T_INITIALIZER; + req.request_type = HTTP_REQ_CONNECT; + req.timeout_s = request->timeout_s; + req.host = request->host; + req.port = request->port; + req.url = request->url; + req.proxy_username = request->proxy_username; + req.proxy_password = request->proxy_password; + ctx->request = &req; + if (handle_http_request(ctx)) { + netdata_log_error("Failed to CONNECT with proxy"); + http_parse_ctx_destroy(&ctx->parse_ctx); + goto exit_sock; + } + if (ctx->parse_ctx.http_code != 200) { + netdata_log_error("Proxy didn't return 200 OK (got %d)", ctx->parse_ctx.http_code); + http_parse_ctx_destroy(&ctx->parse_ctx); + goto exit_sock; + } + http_parse_ctx_destroy(&ctx->parse_ctx); + netdata_log_info("Proxy accepted CONNECT upgrade"); + } + ctx->request = request; + + ctx->ssl_ctx = netdata_ssl_create_client_ctx(0); + if (ctx->ssl_ctx==NULL) { + netdata_log_error("Cannot allocate SSL context"); + goto exit_sock; + } + + if (!SSL_CTX_set_default_verify_paths(ctx->ssl_ctx)) { + netdata_log_error("Error setting default verify paths"); + goto exit_CTX; + } + SSL_CTX_set_verify(ctx->ssl_ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, cert_verify_callback); + + ctx->ssl = SSL_new(ctx->ssl_ctx); + if (ctx->ssl==NULL) { + netdata_log_error("Cannot allocate SSL"); + goto exit_CTX; + } + + if (!SSL_set_tlsext_host_name(ctx->ssl, connect_host)) { + netdata_log_error("Error setting TLS SNI host"); + goto exit_CTX; + } + + SSL_set_fd(ctx->ssl, ctx->sock); + ret = SSL_connect(ctx->ssl); + if (ret != -1 && ret != 1) { + netdata_log_error("SSL could not connect"); + goto exit_SSL; + } + if (ret == -1) { + // expected as underlying socket is non blocking! + // consult SSL_connect documentation for details + int ec = SSL_get_error(ctx->ssl, ret); + if (ec != SSL_ERROR_WANT_READ && ec != SSL_ERROR_WANT_WRITE) { + netdata_log_error("Failed to start SSL connection"); + goto exit_SSL; + } + } + + // The actual request here + if (handle_http_request(ctx)) { + netdata_log_error("Couldn't process request"); + http_parse_ctx_destroy(&ctx->parse_ctx); + goto exit_SSL; + } + http_parse_ctx_destroy(&ctx->parse_ctx); + response->http_code = ctx->parse_ctx.http_code; + if (ctx->parse_ctx.content_length == TRANSFER_ENCODING_CHUNKED) { + response->payload_size = ctx->parse_ctx.chunked_response_size; + response->payload = ctx->parse_ctx.chunked_response; + } + if (ctx->parse_ctx.content_length > 0) { + response->payload_size = ctx->parse_ctx.content_length; + response->payload = mallocz(response->payload_size + 1); + ret = rbuf_pop(ctx->buf_rx, response->payload, response->payload_size); + if (ret != (int)response->payload_size) { + netdata_log_error("Payload size doesn't match remaining data on the buffer!"); + response->payload_size = ret; + } + // normally we take payload as it is and copy it + // but for convenience in cases where payload is sth. like + // json we add terminating zero so that user of the data + // doesn't have to convert to C string (0 terminated) + // other uses still have correct payload_size and can copy + // only exact data without affixed 0x00 + ((char*)response->payload)[response->payload_size] = 0; // mallocz(response->payload_size + 1); + } + netdata_log_info("HTTPS \"%s\" request to \"%s\" finished with HTTP code: %d", http_req_type_to_str(ctx->request->request_type), ctx->request->host, response->http_code); + + rc = 0; + +exit_SSL: + SSL_free(ctx->ssl); +exit_CTX: + SSL_CTX_free(ctx->ssl_ctx); +exit_sock: + close(ctx->sock); +exit_buf_rx: + rbuf_free(ctx->buf_rx); +exit_req_ctx: + freez(ctx); + return rc; +} + +void https_req_response_free(https_req_response_t *res) { + freez(res->payload); +} + +void https_req_response_init(https_req_response_t *res) { + res->http_code = 0; + res->payload = NULL; + res->payload_size = 0; +} + +static inline char *UNUSED_FUNCTION(min_non_null)(char *a, char *b) { + if (!a) + return b; + if (!b) + return a; + return (a < b ? a : b); +} + +#define URI_PROTO_SEPARATOR "://" +#define URL_PARSER_LOG_PREFIX "url_parser " + +static int parse_host_port(url_t *url) { + char *ptr = strrchr(url->host, ':'); + if (ptr) { + size_t port_len = strlen(ptr + 1); + if (!port_len) { + netdata_log_error(URL_PARSER_LOG_PREFIX ": specified but no port number"); + return 1; + } + if (port_len > 5 /* MAX port length is 5digit long in decimal */) { + netdata_log_error(URL_PARSER_LOG_PREFIX "port # is too long"); + return 1; + } + *ptr = 0; + if (!strlen(url->host)) { + netdata_log_error(URL_PARSER_LOG_PREFIX "host empty after removing port"); + return 1; + } + url->port = atoi (ptr + 1); + } + return 0; +} + +static inline void port_by_proto(url_t *url) { + if (url->port) + return; + if (!url->proto) + return; + if (!strcmp(url->proto, "http")) { + url->port = 80; + return; + } + if (!strcmp(url->proto, "https")) { + url->port = 443; + return; + } +} + +#define STRDUPZ_2PTR(dest, start, end) \ + { \ + dest = mallocz(1 + end - start); \ + memcpy(dest, start, end - start); \ + dest[end - start] = 0; \ + } + +int url_parse(const char *url, url_t *parsed) { + const char *start = url; + const char *end = strstr(url, URI_PROTO_SEPARATOR); + + if (end) { + if (end == start) { + netdata_log_error(URL_PARSER_LOG_PREFIX "found " URI_PROTO_SEPARATOR " without protocol specified"); + return 1; + } + + STRDUPZ_2PTR(parsed->proto, start, end) + start = end + strlen(URI_PROTO_SEPARATOR); + } + + end = strchr(start, '/'); + if (!end) + end = start + strlen(start); + + if (start == end) { + netdata_log_error(URL_PARSER_LOG_PREFIX "Host empty"); + return 1; + } + + STRDUPZ_2PTR(parsed->host, start, end); + + if (parse_host_port(parsed)) + return 1; + + if (!*end) { + parsed->path = strdupz("/"); + port_by_proto(parsed); + return 0; + } + + parsed->path = strdupz(end); + port_by_proto(parsed); + return 0; +} + +void url_t_destroy(url_t *url) { + freez(url->host); + freez(url->path); + freez(url->proto); +} |