Files
sovereign_browser/src/tor_scheme.c
T

292 lines
9.8 KiB
C

/*
* tor_scheme.c — asynchronous Tor-backed WebKitGTK handler for tor://
*/
#include "tor_scheme.h"
#include "net_services.h"
#include <curl/curl.h>
#include <glib.h>
#include <webkit2/webkit2.h>
#include <ctype.h>
#include <string.h>
#define TOR_FETCH_MAX_BYTES (16 * 1024 * 1024)
#define TOR_FETCH_TIMEOUT_SECONDS 30L
#define TOR_CONNECT_TIMEOUT_SECONDS 15L
typedef struct {
WebKitURISchemeRequest *request;
char *target_url;
char *proxy_url;
GString *response_body;
char *content_type;
char *error_html;
} tor_job_t;
typedef struct {
GString *body;
gboolean overflow;
} tor_body_t;
static char *html_escape(const char *input) {
if (!input) return g_strdup("");
return g_markup_escape_text(input, -1);
}
static void respond_html(WebKitURISchemeRequest *request, char *html) {
gsize len = strlen(html);
GInputStream *stream = g_memory_input_stream_new_from_data(html, len, g_free);
webkit_uri_scheme_request_finish(request, stream, len, "text/html");
g_object_unref(stream);
}
static void respond_error_page(WebKitURISchemeRequest *request,
const char *title,
const char *message) {
char *safe_title = html_escape(title ? title : "Error");
char *safe_message = html_escape(message ? message : "Request failed.");
char *html = g_strdup_printf(
"<!doctype html><html><head><meta charset=\"utf-8\">"
"<title>%s</title></head><body>"
"<h1>%s</h1><p>%s</p></body></html>",
safe_title, safe_title, safe_message);
respond_html(request, html);
g_free(safe_title);
g_free(safe_message);
}
static gboolean host_is_onion(const char *host) {
return host && *host && g_str_has_suffix(host, ".onion");
}
static char *proxy_to_socks5h(const char *endpoint) {
if (!endpoint || endpoint[0] == '\0') return NULL;
if (g_str_has_prefix(endpoint, "socks5h://")) return g_strdup(endpoint);
if (!g_str_has_prefix(endpoint, "socks5://")) return NULL;
return g_strdup_printf("socks5h://%s", endpoint + strlen("socks5://"));
}
static char *build_target_http_url(const char *uri, char **error_message) {
GError *error = NULL;
GUri *parsed = g_uri_parse(uri, G_URI_FLAGS_PARSE_RELAXED, &error);
if (!parsed) {
if (error_message)
*error_message = g_strdup(error ? error->message : "Invalid tor:// URI");
g_clear_error(&error);
return NULL;
}
const char *host = g_uri_get_host(parsed);
gint port = g_uri_get_port(parsed);
const char *path = g_uri_get_path(parsed);
const char *query = g_uri_get_query(parsed);
if (!host_is_onion(host)) {
if (error_message) {
*error_message = g_strdup("tor:// only supports .onion hostnames in Phase 1");
}
g_uri_unref(parsed);
return NULL;
}
if (!path || path[0] == '\0') path = "/";
/* Phase 1: always map tor:// to http:// over Tor SOCKS. HTTPS-over-onion
* support can be added in a future phase with an explicit signal. */
char *target = NULL;
if (port > 0) {
target = query
? g_strdup_printf("http://%s:%d%s?%s", host, port, path, query)
: g_strdup_printf("http://%s:%d%s", host, port, path);
} else {
target = query
? g_strdup_printf("http://%s%s?%s", host, path, query)
: g_strdup_printf("http://%s%s", host, path);
}
g_uri_unref(parsed);
return target;
}
static size_t tor_write_bounded(void *data, size_t size, size_t nmemb, void *user_data) {
tor_body_t *body = user_data;
size_t bytes = size * nmemb;
if (body->body->len + bytes > TOR_FETCH_MAX_BYTES) {
body->overflow = TRUE;
return 0;
}
g_string_append_len(body->body, data, bytes);
return bytes;
}
static size_t tor_header_capture(void *data, size_t size, size_t nmemb, void *user_data) {
tor_job_t *job = user_data;
size_t bytes = size * nmemb;
const char *line = (const char *)data;
if (job->content_type != NULL || bytes < strlen("Content-Type:")) return bytes;
if (g_ascii_strncasecmp(line, "Content-Type:", strlen("Content-Type:")) == 0) {
const char *value = line + strlen("Content-Type:");
while (*value == ' ' || *value == '\t') value++;
const char *end = line + bytes;
while (end > value && (end[-1] == '\r' || end[-1] == '\n' ||
end[-1] == ' ' || end[-1] == '\t')) {
end--;
}
if (end > value) job->content_type = g_strndup(value, (gsize)(end - value));
}
return bytes;
}
static gpointer tor_scheme_worker(gpointer data) {
tor_job_t *job = data;
CURL *curl = curl_easy_init();
tor_body_t body = { g_string_new(NULL), FALSE };
if (!curl) {
job->error_html = g_strdup(
"Failed to load .onion site: Could not initialize HTTP client.");
g_string_free(body.body, TRUE);
return job;
}
curl_easy_setopt(curl, CURLOPT_URL, job->target_url);
curl_easy_setopt(curl, CURLOPT_PROXY, job->proxy_url);
curl_easy_setopt(curl, CURLOPT_PROXYTYPE, (long)CURLPROXY_SOCKS5_HOSTNAME);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, TOR_FETCH_TIMEOUT_SECONDS);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, TOR_CONNECT_TIMEOUT_SECONDS);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "sovereign_browser/tor-scheme");
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, tor_write_bounded);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, tor_header_capture);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, job);
CURLcode rc = curl_easy_perform(curl);
if (rc != CURLE_OK) {
const char *err = body.overflow
? "response exceeded maximum size"
: curl_easy_strerror(rc);
char *safe_err = html_escape(err);
job->error_html = g_strdup_printf("Failed to load .onion site: %s", safe_err);
g_free(safe_err);
g_string_free(body.body, TRUE);
curl_easy_cleanup(curl);
return job;
}
job->response_body = body.body;
if (!job->content_type || job->content_type[0] == '\0') {
g_free(job->content_type);
job->content_type = g_strdup("text/html");
}
curl_easy_cleanup(curl);
return job;
}
static void tor_job_free(tor_job_t *job) {
if (!job) return;
g_clear_object(&job->request);
g_free(job->target_url);
g_free(job->proxy_url);
if (job->response_body) g_string_free(job->response_body, TRUE);
g_free(job->content_type);
g_free(job->error_html);
g_free(job);
}
static void tor_scheme_worker_done(GObject *source_object,
GAsyncResult *result,
gpointer user_data) {
(void)source_object;
(void)user_data;
GTask *task = G_TASK(result);
tor_job_t *job = g_task_propagate_pointer(task, NULL);
if (job->error_html) {
respond_error_page(job->request, "Tor request failed", job->error_html);
tor_job_free(job);
return;
}
gsize len = job->response_body ? job->response_body->len : 0;
const char *data = (job->response_body && len > 0) ? job->response_body->str : "";
GInputStream *stream = g_memory_input_stream_new_from_data(g_memdup2(data, len),
len,
g_free);
webkit_uri_scheme_request_finish(job->request,
stream,
len,
job->content_type ? job->content_type : "text/html");
g_object_unref(stream);
tor_job_free(job);
}
static void tor_task_thread(GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable) {
(void)source_object;
(void)cancellable;
g_task_return_pointer(task, tor_scheme_worker(task_data), NULL);
}
static void on_tor_scheme(WebKitURISchemeRequest *request, gpointer user_data) {
(void)user_data;
const char *uri = webkit_uri_scheme_request_get_uri(request);
char *url_error = NULL;
char *target_url = build_target_http_url(uri, &url_error);
if (!target_url) {
respond_error_page(request,
"Invalid .onion URL",
url_error ? url_error : "Invalid tor:// URL.");
g_free(url_error);
return;
}
if (!net_service_is_ready(NET_SERVICE_TOR)) {
respond_error_page(request,
"Tor unavailable",
"Tor is not ready. Please wait for Tor to bootstrap and try again.");
g_free(target_url);
return;
}
const net_service_t *tor = net_service_get_status(NET_SERVICE_TOR);
const char *endpoint = tor ? tor->status.tor.socks_endpoint : NULL;
char *proxy_url = proxy_to_socks5h(endpoint);
if (!proxy_url) {
respond_error_page(request,
"Tor unavailable",
"Tor SOCKS endpoint is not available yet. Please try again shortly.");
g_free(target_url);
return;
}
tor_job_t *job = g_new0(tor_job_t, 1);
job->request = g_object_ref(request);
job->target_url = target_url;
job->proxy_url = proxy_url;
GTask *task = g_task_new(NULL, NULL, tor_scheme_worker_done, NULL);
g_task_set_task_data(task, job, NULL);
g_task_run_in_thread(task, tor_task_thread);
g_object_unref(task);
}
void tor_scheme_register(WebKitWebContext *ctx) {
g_return_if_fail(WEBKIT_IS_WEB_CONTEXT(ctx));
webkit_web_context_register_uri_scheme(ctx, "tor", on_tor_scheme, NULL, NULL);
}