summaryrefslogtreecommitdiffstats
path: root/libfreerdp/utils/http.c
diff options
context:
space:
mode:
Diffstat (limited to 'libfreerdp/utils/http.c')
-rw-r--r--libfreerdp/utils/http.c386
1 files changed, 386 insertions, 0 deletions
diff --git a/libfreerdp/utils/http.c b/libfreerdp/utils/http.c
new file mode 100644
index 0000000..70963f0
--- /dev/null
+++ b/libfreerdp/utils/http.c
@@ -0,0 +1,386 @@
+/**
+ * FreeRDP: A Remote Desktop Protocol Implementation
+ * Simple HTTP client request utility
+ *
+ * Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <freerdp/config.h>
+#include <freerdp/utils/http.h>
+
+#include <winpr/assert.h>
+#include <winpr/string.h>
+
+#include <openssl/bio.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+
+#include <freerdp/log.h>
+#define TAG FREERDP_TAG("utils.http")
+
+static const char get_header_fmt[] = "GET %s HTTP/1.1\r\n"
+ "Host: %s\r\n"
+ "\r\n";
+
+static const char post_header_fmt[] = "POST %s HTTP/1.1\r\n"
+ "Host: %s\r\n"
+ "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Content-Length: %lu\r\n"
+ "\r\n";
+
+#define log_errors(log, msg) log_errors_(log, msg, __FILE__, __func__, __LINE__)
+static void log_errors_(wLog* log, const char* msg, const char* file, const char* fkt, size_t line)
+{
+ const DWORD level = WLOG_ERROR;
+ unsigned long ec = 0;
+
+ if (!WLog_IsLevelActive(log, level))
+ return;
+
+ BOOL error_logged = FALSE;
+ while ((ec = ERR_get_error()))
+ {
+ error_logged = TRUE;
+ WLog_PrintMessage(log, WLOG_MESSAGE_TEXT, level, line, file, fkt, "%s: %s", msg,
+ ERR_error_string(ec, NULL));
+ }
+ if (!error_logged)
+ WLog_PrintMessage(log, WLOG_MESSAGE_TEXT, level, line, file, fkt,
+ "%s (no details available)", msg);
+}
+
+static int get_line(BIO* bio, char* buffer, size_t size)
+{
+#if !defined(OPENSSL_VERSION_MAJOR) || (OPENSSL_VERSION_MAJOR < 3)
+ if (size <= 1)
+ return -1;
+
+ size_t pos = 0;
+ do
+ {
+ int rc = BIO_read(bio, &buffer[pos], 1);
+ if (rc <= 0)
+ return rc;
+ char cur = buffer[pos];
+ pos += rc;
+ if ((cur == '\n') || (pos >= size - 1))
+ {
+ buffer[pos] = '\0';
+ return (int)pos;
+ }
+ } while (1);
+#else
+ return BIO_get_line(bio, buffer, size);
+#endif
+}
+
+BOOL freerdp_http_request(const char* url, const char* body, long* status_code, BYTE** response,
+ size_t* response_length)
+{
+ BOOL ret = FALSE;
+ char* hostname = NULL;
+ const char* path = NULL;
+ char* headers = NULL;
+ size_t size = 0;
+ int status = 0;
+ char buffer[1024] = { 0 };
+ BIO* bio = NULL;
+ SSL_CTX* ssl_ctx = NULL;
+ SSL* ssl = NULL;
+
+ WINPR_ASSERT(status_code);
+ WINPR_ASSERT(response);
+ WINPR_ASSERT(response_length);
+
+ wLog* log = WLog_Get(TAG);
+ WINPR_ASSERT(log);
+
+ *response = NULL;
+
+ if (!url || strnlen(url, 8) < 8 || strncmp(url, "https://", 8) != 0 ||
+ !(path = strchr(url + 8, '/')))
+ {
+ WLog_Print(log, WLOG_ERROR, "invalid url provided");
+ goto out;
+ }
+
+ const size_t len = path - (url + 8);
+ hostname = strndup(&url[8], len);
+ if (!hostname)
+ return FALSE;
+
+ size_t blen = 0;
+ if (body)
+ {
+ blen = strlen(body);
+ if (winpr_asprintf(&headers, &size, post_header_fmt, path, hostname, blen) < 0)
+ return FALSE;
+ }
+ else
+ {
+ if (winpr_asprintf(&headers, &size, get_header_fmt, path, hostname) < 0)
+ return FALSE;
+ }
+
+ ssl_ctx = SSL_CTX_new(TLS_client_method());
+
+ if (!ssl_ctx)
+ {
+ log_errors(log, "could not set up ssl context");
+ goto out;
+ }
+
+ if (!SSL_CTX_set_default_verify_paths(ssl_ctx))
+ {
+ log_errors(log, "could not set ssl context verify paths");
+ goto out;
+ }
+
+ SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY);
+
+ bio = BIO_new_ssl_connect(ssl_ctx);
+ if (!bio)
+ {
+ log_errors(log, "could not set up connection");
+ goto out;
+ }
+
+ if (BIO_set_conn_port(bio, "https") <= 0)
+ {
+ log_errors(log, "could not set port");
+ goto out;
+ }
+
+ if (!BIO_set_conn_hostname(bio, hostname))
+ {
+ log_errors(log, "could not set hostname");
+ goto out;
+ }
+
+ BIO_get_ssl(bio, &ssl);
+ if (!ssl)
+ {
+ log_errors(log, "could not get ssl");
+ goto out;
+ }
+
+ if (!SSL_set_tlsext_host_name(ssl, hostname))
+ {
+ log_errors(log, "could not set sni hostname");
+ goto out;
+ }
+
+ WLog_Print(log, WLOG_DEBUG, "headers:\n%s", headers);
+ ERR_clear_error();
+ if (BIO_write(bio, headers, strnlen(headers, size)) < 0)
+ {
+ log_errors(log, "could not write headers");
+ goto out;
+ }
+
+ if (body)
+ {
+ WLog_Print(log, WLOG_DEBUG, "body:\n%s", body);
+
+ if (blen > INT_MAX)
+ {
+ WLog_Print(log, WLOG_ERROR, "body too long!");
+ goto out;
+ }
+
+ ERR_clear_error();
+ if (BIO_write(bio, body, blen) < 0)
+ {
+ log_errors(log, "could not write body");
+ goto out;
+ }
+ }
+
+ status = get_line(bio, buffer, sizeof(buffer));
+ if (status <= 0)
+ {
+ log_errors(log, "could not read response");
+ goto out;
+ }
+
+ if (sscanf(buffer, "HTTP/1.1 %li %*[^\r\n]\r\n", status_code) < 1)
+ {
+ WLog_Print(log, WLOG_ERROR, "invalid HTTP status line");
+ goto out;
+ }
+
+ do
+ {
+ status = get_line(bio, buffer, sizeof(buffer));
+ if (status <= 0)
+ {
+ log_errors(log, "could not read response");
+ goto out;
+ }
+
+ char* val = NULL;
+ char* name = strtok_s(buffer, ":", &val);
+ if (name && (_stricmp(name, "content-length") == 0))
+ {
+ errno = 0;
+ *response_length = strtoul(val, NULL, 10);
+ if (errno)
+ {
+ char ebuffer[256] = { 0 };
+ WLog_Print(log, WLOG_ERROR, "could not parse content length (%s): %s [%d]", val,
+ winpr_strerror(errno, ebuffer, sizeof(ebuffer)), errno);
+ goto out;
+ }
+ }
+ } while (strcmp(buffer, "\r\n") != 0);
+
+ if (*response_length > 0)
+ {
+ if (*response_length > INT_MAX)
+ {
+ WLog_Print(log, WLOG_ERROR, "response too long!");
+ goto out;
+ }
+
+ *response = calloc(1, *response_length + 1);
+ if (!*response)
+ goto out;
+
+ BYTE* p = *response;
+ int left = *response_length;
+ while (left > 0)
+ {
+ status = BIO_read(bio, p, left);
+ if (status <= 0)
+ {
+ log_errors(log, "could not read response");
+ goto out;
+ }
+ p += status;
+ left -= status;
+ }
+ }
+
+ ret = TRUE;
+
+out:
+ if (!ret)
+ {
+ free(*response);
+ *response = NULL;
+ *response_length = 0;
+ }
+ free(hostname);
+ free(headers);
+ BIO_free_all(bio);
+ SSL_CTX_free(ssl_ctx);
+ return ret;
+}
+
+const char* freerdp_http_status_string(long status)
+{
+ switch (status)
+ {
+ case HTTP_STATUS_CONTINUE:
+ return "HTTP_STATUS_CONTINUE";
+ case HTTP_STATUS_SWITCH_PROTOCOLS:
+ return "HTTP_STATUS_SWITCH_PROTOCOLS";
+ case HTTP_STATUS_OK:
+ return "HTTP_STATUS_OK";
+ case HTTP_STATUS_CREATED:
+ return "HTTP_STATUS_CREATED";
+ case HTTP_STATUS_ACCEPTED:
+ return "HTTP_STATUS_ACCEPTED";
+ case HTTP_STATUS_PARTIAL:
+ return "HTTP_STATUS_PARTIAL";
+ case HTTP_STATUS_NO_CONTENT:
+ return "HTTP_STATUS_NO_CONTENT";
+ case HTTP_STATUS_RESET_CONTENT:
+ return "HTTP_STATUS_RESET_CONTENT";
+ case HTTP_STATUS_PARTIAL_CONTENT:
+ return "HTTP_STATUS_PARTIAL_CONTENT";
+ case HTTP_STATUS_WEBDAV_MULTI_STATUS:
+ return "HTTP_STATUS_WEBDAV_MULTI_STATUS";
+ case HTTP_STATUS_AMBIGUOUS:
+ return "HTTP_STATUS_AMBIGUOUS";
+ case HTTP_STATUS_MOVED:
+ return "HTTP_STATUS_MOVED";
+ case HTTP_STATUS_REDIRECT:
+ return "HTTP_STATUS_REDIRECT";
+ case HTTP_STATUS_REDIRECT_METHOD:
+ return "HTTP_STATUS_REDIRECT_METHOD";
+ case HTTP_STATUS_NOT_MODIFIED:
+ return "HTTP_STATUS_NOT_MODIFIED";
+ case HTTP_STATUS_USE_PROXY:
+ return "HTTP_STATUS_USE_PROXY";
+ case HTTP_STATUS_REDIRECT_KEEP_VERB:
+ return "HTTP_STATUS_REDIRECT_KEEP_VERB";
+ case HTTP_STATUS_BAD_REQUEST:
+ return "HTTP_STATUS_BAD_REQUEST";
+ case HTTP_STATUS_DENIED:
+ return "HTTP_STATUS_DENIED";
+ case HTTP_STATUS_PAYMENT_REQ:
+ return "HTTP_STATUS_PAYMENT_REQ";
+ case HTTP_STATUS_FORBIDDEN:
+ return "HTTP_STATUS_FORBIDDEN";
+ case HTTP_STATUS_NOT_FOUND:
+ return "HTTP_STATUS_NOT_FOUND";
+ case HTTP_STATUS_BAD_METHOD:
+ return "HTTP_STATUS_BAD_METHOD";
+ case HTTP_STATUS_NONE_ACCEPTABLE:
+ return "HTTP_STATUS_NONE_ACCEPTABLE";
+ case HTTP_STATUS_PROXY_AUTH_REQ:
+ return "HTTP_STATUS_PROXY_AUTH_REQ";
+ case HTTP_STATUS_REQUEST_TIMEOUT:
+ return "HTTP_STATUS_REQUEST_TIMEOUT";
+ case HTTP_STATUS_CONFLICT:
+ return "HTTP_STATUS_CONFLICT";
+ case HTTP_STATUS_GONE:
+ return "HTTP_STATUS_GONE";
+ case HTTP_STATUS_LENGTH_REQUIRED:
+ return "HTTP_STATUS_LENGTH_REQUIRED";
+ case HTTP_STATUS_PRECOND_FAILED:
+ return "HTTP_STATUS_PRECOND_FAILED";
+ case HTTP_STATUS_REQUEST_TOO_LARGE:
+ return "HTTP_STATUS_REQUEST_TOO_LARGE";
+ case HTTP_STATUS_URI_TOO_LONG:
+ return "HTTP_STATUS_URI_TOO_LONG";
+ case HTTP_STATUS_UNSUPPORTED_MEDIA:
+ return "HTTP_STATUS_UNSUPPORTED_MEDIA";
+ case HTTP_STATUS_RETRY_WITH:
+ return "HTTP_STATUS_RETRY_WITH";
+ case HTTP_STATUS_SERVER_ERROR:
+ return "HTTP_STATUS_SERVER_ERROR";
+ case HTTP_STATUS_NOT_SUPPORTED:
+ return "HTTP_STATUS_NOT_SUPPORTED";
+ case HTTP_STATUS_BAD_GATEWAY:
+ return "HTTP_STATUS_BAD_GATEWAY";
+ case HTTP_STATUS_SERVICE_UNAVAIL:
+ return "HTTP_STATUS_SERVICE_UNAVAIL";
+ case HTTP_STATUS_GATEWAY_TIMEOUT:
+ return "HTTP_STATUS_GATEWAY_TIMEOUT";
+ case HTTP_STATUS_VERSION_NOT_SUP:
+ return "HTTP_STATUS_VERSION_NOT_SUP";
+ default:
+ return "HTTP_STATUS_UNKNOWN";
+ }
+}
+
+char* freerdp_http_status_string_format(long status, char* buffer, size_t size)
+{
+ const char* code = freerdp_http_status_string(status);
+ _snprintf(buffer, size, "%s [%ld]", code, status);
+ return buffer;
+}