292 lines
9.8 KiB
C
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);
|
|
}
|