detached 4cda051

feat(webkit): add TLS certificate error handling with user dialog

- Implement comprehensive certificate information display (subject, issuer, validity dates)
- Add interactive warning dialog for untrusted certificate authorities
- Integrate certificate exception management with WebKitGTK 6.0 network session API
- Fix webpage loading after user accepts certificate exceptions via manual reload
- Set WebView background to black for better user experience during page loads
- Support per-host certificate exception persistence to avoid repeated prompts

Resolves issues with websites using certificate chains not trusted by WebKitGTK
while providing users with informed choice about security exceptions.
commit
4cda05117694fc9376f4e4c501e41cec9b7a4e8d
author
bamen · · 2025-09-11 08:18 UTC
parent
f47c81f
signature
unsigned
3 files · +459 -89
M CHANGELOG.md +13 -0
@@ -2,6 +2,19 @@
22
33 All notable changes to this project will be documented in this file.
44
5+## [Unreleased]
6+
7+### Added
8+- TLS certificate error handling with user confirmation dialog:
9+ - Comprehensive certificate information display (subject, issuer, validity dates)
10+ - Interactive warning dialog for untrusted certificate authorities
11+ - Certificate exception management with per-host persistence
12+ - Manual load triggering after user accepts certificate exceptions
13+ - Proper integration with WebKitGTK 6.0 network session API
14+
15+### Fixed
16+- WebView background set to black to improve user experience during page loads
17+
518 ## [0.4.0] - 2025-09-10
619
720 ### Added
@@ -2,6 +2,19 @@
2 2
3 All notable changes to this project will be documented in this file.3 All notable changes to this project will be documented in this file.
4 4
5+## [Unreleased]
6+
7+### Added
8+- TLS certificate error handling with user confirmation dialog:
9+ - Comprehensive certificate information display (subject, issuer, validity dates)
10+ - Interactive warning dialog for untrusted certificate authorities
11+ - Certificate exception management with per-host persistence
12+ - Manual load triggering after user accepts certificate exceptions
13+ - Proper integration with WebKitGTK 6.0 network session API
14+
15+### Fixed
16+- WebView background set to black to improve user experience during page loads
17+
5 ## [0.4.0] - 2025-09-1018 ## [0.4.0] - 2025-09-10
6 19
7 ### Added20 ### Added
A pkg/webkit/webview_callbacks.h +72 -0
@@ -0,0 +1,72 @@
1+#ifndef WEBVIEW_CALLBACKS_H
2+#define WEBVIEW_CALLBACKS_H
3+
4+#include <gtk/gtk.h>
5+#include <webkit/webkit.h>
6+#include <glib-object.h>
7+
8+// Forward declarations for Go callbacks
9+extern void goOnTitleChanged(unsigned long id, char* title);
10+extern void goOnURIChanged(unsigned long id, char* uri);
11+extern void goOnThemeChanged(unsigned long id, int prefer_dark);
12+extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
13+extern void goQuitMainLoop();
14+
15+// Static inline functions to avoid multiple definition errors
16+static inline void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
17+ (void)pspec;
18+ WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
19+ const gchar* title = webkit_web_view_get_title(view);
20+ GtkWindow* win = GTK_WINDOW(user_data);
21+ if (title && win) {
22+ gtk_window_set_title(win, title);
23+ }
24+}
25+
26+static inline void on_title_notify_id(GObject* obj, GParamSpec* pspec, gpointer user_data) {
27+ (void)pspec; (void)obj;
28+ const gchar* title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(obj));
29+ if (title) { goOnTitleChanged((unsigned long)user_data, (char*)title); }
30+}
31+
32+static inline void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
33+ (void)pspec; (void)obj;
34+ const gchar* uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(obj));
35+ if (uri) { goOnURIChanged((unsigned long)user_data, (char*)uri); }
36+}
37+
38+static inline void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
39+ (void)pspec;
40+ GtkSettings* settings = GTK_SETTINGS(obj);
41+ if (!settings) return;
42+ gboolean prefer = FALSE;
43+ g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
44+ goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
45+}
46+
47+static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
48+ (void)user_data;
49+ const gchar* uri = webkit_uri_scheme_request_get_uri(request);
50+ size_t n = 0;
51+ char* mime = NULL;
52+ void* buf = goResolveURIScheme((char*)uri, &n, &mime);
53+ if (buf && n > 0 && mime) {
54+ GInputStream* stream = g_memory_input_stream_new_from_data(buf, (gssize)n, g_free);
55+ webkit_uri_scheme_request_finish(request, stream, (gint64)n, mime);
56+ g_object_unref(stream);
57+ g_free(mime);
58+ } else {
59+ GError* err = g_error_new_literal(g_quark_from_string("dumber"), 404, "Not found");
60+ webkit_uri_scheme_request_finish_error(request, err);
61+ g_error_free(err);
62+ }
63+}
64+
65+// GTK4 close-request signal handler
66+static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) {
67+ (void)window; (void)user_data;
68+ goQuitMainLoop();
69+ return FALSE; // Allow the window to close
70+}
71+
72+#endif // WEBVIEW_CALLBACKS_H⏎̸
@@ -0,0 +1,72 @@
1+#ifndef WEBVIEW_CALLBACKS_H
2+#define WEBVIEW_CALLBACKS_H
3+
4+#include <gtk/gtk.h>
5+#include <webkit/webkit.h>
6+#include <glib-object.h>
7+
8+// Forward declarations for Go callbacks
9+extern void goOnTitleChanged(unsigned long id, char* title);
10+extern void goOnURIChanged(unsigned long id, char* uri);
11+extern void goOnThemeChanged(unsigned long id, int prefer_dark);
12+extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
13+extern void goQuitMainLoop();
14+
15+// Static inline functions to avoid multiple definition errors
16+static inline void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
17+ (void)pspec;
18+ WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
19+ const gchar* title = webkit_web_view_get_title(view);
20+ GtkWindow* win = GTK_WINDOW(user_data);
21+ if (title && win) {
22+ gtk_window_set_title(win, title);
23+ }
24+}
25+
26+static inline void on_title_notify_id(GObject* obj, GParamSpec* pspec, gpointer user_data) {
27+ (void)pspec; (void)obj;
28+ const gchar* title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(obj));
29+ if (title) { goOnTitleChanged((unsigned long)user_data, (char*)title); }
30+}
31+
32+static inline void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
33+ (void)pspec; (void)obj;
34+ const gchar* uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(obj));
35+ if (uri) { goOnURIChanged((unsigned long)user_data, (char*)uri); }
36+}
37+
38+static inline void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
39+ (void)pspec;
40+ GtkSettings* settings = GTK_SETTINGS(obj);
41+ if (!settings) return;
42+ gboolean prefer = FALSE;
43+ g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
44+ goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
45+}
46+
47+static inline void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
48+ (void)user_data;
49+ const gchar* uri = webkit_uri_scheme_request_get_uri(request);
50+ size_t n = 0;
51+ char* mime = NULL;
52+ void* buf = goResolveURIScheme((char*)uri, &n, &mime);
53+ if (buf && n > 0 && mime) {
54+ GInputStream* stream = g_memory_input_stream_new_from_data(buf, (gssize)n, g_free);
55+ webkit_uri_scheme_request_finish(request, stream, (gint64)n, mime);
56+ g_object_unref(stream);
57+ g_free(mime);
58+ } else {
59+ GError* err = g_error_new_literal(g_quark_from_string("dumber"), 404, "Not found");
60+ webkit_uri_scheme_request_finish_error(request, err);
61+ g_error_free(err);
62+ }
63+}
64+
65+// GTK4 close-request signal handler
66+static inline gboolean on_close_request(GtkWindow* window, gpointer user_data) {
67+ (void)window; (void)user_data;
68+ goQuitMainLoop();
69+ return FALSE; // Allow the window to close
70+}
71+
72+#endif // WEBVIEW_CALLBACKS_H⏎̸
M pkg/webkit/webview_cgo.go +374 -89
@@ -13,19 +13,13 @@ package webkit
1313 #include <jsc/jsc.h>
1414 #include <glib.h>
1515 #include <gio/gio.h>
16+#include "webview_callbacks.h"
1617
1718 static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
1819
1920 // Forward declaration
2021 extern void goQuitMainLoop();
2122
22-// GTK4 close-request signal handler
23-static gboolean on_close_request(GtkWindow* window, gpointer user_data) {
24- (void)window; (void)user_data;
25- goQuitMainLoop();
26- return FALSE; // Allow the window to close
27-}
28-
2923 // Connect window close signal to quit main loop
3024 static void connect_destroy_quit(GtkWidget* w) {
3125 if (!w) return;
@@ -37,6 +31,25 @@ static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
3731 // Forward declare helpers used below
3832 static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
3933
34+// TLS error handling forward declarations
35+extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
36+static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
37+ const char *failing_uri,
38+ GTlsCertificate *certificate,
39+ GTlsCertificateFlags errors,
40+ gpointer user_data);
41+static void connect_tls_error_handler(WebKitWebView* wv);
42+
43+// Dialog callback structure for TLS warnings
44+typedef struct {
45+ gboolean user_accepted;
46+ gboolean dialog_completed;
47+} TLSDialogData;
48+
49+static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data);
50+static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
51+static char* extract_certificate_info(GTlsCertificate* certificate);
52+
4053 // Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
4154 static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm, WebKitMemoryPressureSettings* pressure_settings) {
4255 WebKitUserContentManager* u = webkit_user_content_manager_new();
@@ -62,12 +75,20 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c
6275 }
6376 // Persist credentials (HTTP auth, etc.)
6477 webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
78+ // Configure TLS errors to emit signals instead of failing silently
79+ webkit_network_session_set_tls_errors_policy(sess, WEBKIT_TLS_ERRORS_POLICY_FAIL);
6580 GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
6681 "user-content-manager", u,
6782 "network-session", sess,
6883 NULL));
6984 // WebView holds refs to provided objects; drop our temporary refs
7085 g_object_unref(sess);
86+ // Set WebView background to black (easier on eyes when pages are loading)
87+ GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
88+ webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
89+
90+ // Connect TLS error handler
91+ connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
7192 if (out_ucm) { *out_ucm = u; }
7293 return w;
7394 }
@@ -85,87 +106,57 @@ static gboolean gtk_prefers_dark() {
85106 }
86107 g_object_unref(desktop_settings);
87108 }
88-
109+
89110 // Method 2: Check GTK theme name for dark variants
90111 GtkSettings* settings = gtk_settings_get_default();
91112 if (settings) {
92113 gchar* theme_name = NULL;
93114 g_object_get(settings, "gtk-theme-name", &theme_name, NULL);
94115 if (theme_name) {
95- gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
116+ gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
96117 strstr(theme_name, "-Dark") != NULL);
97118 g_free(theme_name);
98119 if (is_dark) return TRUE;
99120 }
100-
121+
101122 // Method 3: Check gtk-application-prefer-dark-theme (fallback)
102123 gboolean prefer = FALSE;
103124 g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
104125 if (prefer) return TRUE;
105126 }
106-
127+
107128 return FALSE;
108129 }
109130
110131 // Note: preferred color scheme is handled via a user script injection to support
111132 // older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
112133
113-extern void goOnUcmMessage(unsigned long id, const char* json);
114-extern void goOnTitleChanged(unsigned long id, const char* title);
115-extern void goOnURIChanged(unsigned long id, const char* uri);
116-extern void goOnThemeChanged(unsigned long id, int prefer_dark);
117-extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);
118-
119-void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
120- (void)pspec;
121- WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
122- const gchar* title = webkit_web_view_get_title(view);
123- GtkWindow* win = GTK_WINDOW(user_data);
124- if (title && win) {
125- gtk_window_set_title(win, title);
126- }
134+extern void goOnUcmMessage(unsigned long id, char* json);
135+
136+// Helper functions to connect signals using static callbacks
137+static void connect_title_notify(GtkWidget* widget, GtkWindow* window) {
138+ if (!widget) return;
139+ g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify), G_OBJECT(window), NULL, 0);
127140 }
128141
129-void on_title_notify_id(GObject* obj, GParamSpec* pspec, gpointer user_data) {
130- (void)pspec; (void)obj;
131- const gchar* title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(obj));
132- if (title) { goOnTitleChanged((unsigned long)user_data, title); }
142+static void connect_title_notify_with_id(GtkWidget* widget, unsigned long id) {
143+ if (!widget) return;
144+ g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify_id), (gpointer)id, NULL, 0);
133145 }
134146
135-void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
136- (void)pspec; (void)obj;
137- const gchar* uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(obj));
138- if (uri) { goOnURIChanged((unsigned long)user_data, uri); }
147+static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
148+ if (!widget) return;
149+ g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
139150 }
140151
141-// React to GTK theme preference changes at runtime
142-void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
143- (void)pspec;
144- GtkSettings* settings = GTK_SETTINGS(obj);
152+static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
145153 if (!settings) return;
146- gboolean prefer = FALSE;
147- g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
148- goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
154+ g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
149155 }
150156
151-// NOTE: UCM message callback signature changed in WebKitGTK 6; will be reimplemented later.
152-
153-void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {
154- (void)user_data;
155- const gchar* uri = webkit_uri_scheme_request_get_uri(request);
156- size_t n = 0;
157- char* mime = NULL;
158- void* buf = goResolveURIScheme((char*)uri, &n, &mime);
159- if (buf && n > 0 && mime) {
160- GInputStream* stream = g_memory_input_stream_new_from_data(buf, (gssize)n, g_free);
161- webkit_uri_scheme_request_finish(request, stream, (gint64)n, mime);
162- g_object_unref(stream);
163- g_free(mime);
164- } else {
165- GError* err = g_error_new_literal(g_quark_from_string("dumber"), 404, "Not found");
166- webkit_uri_scheme_request_finish_error(request, err);
167- g_error_free(err);
168- }
157+static void register_uri_scheme_handler(WebKitWebContext* context, const char* scheme) {
158+ if (!context || !scheme) return;
159+ webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
169160 }
170161
171162 // Script message handler wiring for WebKitGTK 6
@@ -357,6 +348,195 @@ static void maybe_collect_js(WebKitWebContext* context) {
357348 // Disabled by default due to API availability variance across 6.0 builds.
358349 (void)context;
359350 }
351+
352+// TLS error handling implementation
353+static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
354+ const char *failing_uri,
355+ GTlsCertificate *certificate,
356+ GTlsCertificateFlags errors,
357+ gpointer user_data) {
358+ (void)user_data;
359+
360+ // Extract host from URI for the Go callback
361+ const char* host = failing_uri;
362+ char* host_copy = NULL;
363+
364+ if (strstr(failing_uri, "://")) {
365+ host = strstr(failing_uri, "://") + 3;
366+ char* slash = strchr(host, '/');
367+ if (slash) {
368+ // Create a temporary null-terminated host string
369+ size_t host_len = slash - host;
370+ host_copy = malloc(host_len + 1);
371+ if (host_copy) {
372+ strncpy(host_copy, host, host_len);
373+ host_copy[host_len] = '\0';
374+ host = host_copy;
375+ }
376+ }
377+ }
378+
379+ // Extract certificate information
380+ char* cert_info = extract_certificate_info(certificate);
381+
382+ gboolean should_proceed = goHandleTLSError((char*)failing_uri, (char*)host, (int)errors, cert_info);
383+
384+ // If user accepted, allow the certificate for this host
385+ if (should_proceed && certificate) {
386+ printf("[dumber] User accepted certificate - adding exception and triggering new load\n");
387+ fflush(stdout);
388+ // Get the network session from the web view
389+ WebKitNetworkSession* session = webkit_web_view_get_network_session(web_view);
390+ if (session) {
391+ webkit_network_session_allow_tls_certificate_for_host(session, certificate, host);
392+ printf("[dumber] Certificate exception added for host: %s\n", host);
393+ fflush(stdout);
394+
395+ // Try loading the URL again after adding the exception
396+ webkit_web_view_load_uri(web_view, failing_uri);
397+ printf("[dumber] Triggered new load of %s with certificate exception\n", failing_uri);
398+ fflush(stdout);
399+ }
400+ }
401+
402+ // Free the certificate info string
403+ if (cert_info) {
404+ g_free(cert_info);
405+ }
406+
407+ if (host_copy) {
408+ free(host_copy);
409+ }
410+
411+ return should_proceed;
412+}
413+
414+// Extract certificate information for display
415+static char* extract_certificate_info(GTlsCertificate* certificate) {
416+ if (!certificate) {
417+ return g_strdup("Certificate information not available");
418+ }
419+
420+ printf("[dumber] Extracting certificate information...\n");
421+ fflush(stdout);
422+
423+ GString* info = g_string_new("Certificate Information:\n");
424+
425+ // Try to get certificate properties - but these might not be available in all GTK versions
426+ gchar* subject = NULL;
427+ gchar* issuer = NULL;
428+ GDateTime* not_valid_before = NULL;
429+ GDateTime* not_valid_after = NULL;
430+
431+ // Try property access but handle failures gracefully
432+ g_object_get(certificate,
433+ "subject-name", &subject,
434+ "issuer-name", &issuer,
435+ "not-valid-before", &not_valid_before,
436+ "not-valid-after", &not_valid_after,
437+ NULL);
438+
439+ if (subject) {
440+ g_string_append_printf(info, "Subject: %s\n", subject);
441+ g_free(subject);
442+ printf("[dumber] Found subject info\n");
443+ } else {
444+ g_string_append(info, "Subject: Information not available\n");
445+ }
446+
447+ if (issuer) {
448+ g_string_append_printf(info, "Issued by: %s\n", issuer);
449+ g_free(issuer);
450+ printf("[dumber] Found issuer info\n");
451+ } else {
452+ g_string_append(info, "Issued by: Information not available\n");
453+ }
454+
455+ if (not_valid_before) {
456+ gchar* date_str = g_date_time_format(not_valid_before, "%Y-%m-%d %H:%M:%S UTC");
457+ g_string_append_printf(info, "Valid from: %s\n", date_str);
458+ g_free(date_str);
459+ g_date_time_unref(not_valid_before);
460+ } else {
461+ g_string_append(info, "Valid from: Information not available\n");
462+ }
463+
464+ if (not_valid_after) {
465+ gchar* date_str = g_date_time_format(not_valid_after, "%Y-%m-%d %H:%M:%S UTC");
466+ g_string_append_printf(info, "Valid until: %s\n", date_str);
467+ g_free(date_str);
468+ g_date_time_unref(not_valid_after);
469+ } else {
470+ g_string_append(info, "Valid until: Information not available\n");
471+ }
472+
473+ char* result = g_string_free(info, FALSE);
474+ printf("[dumber] Certificate info: %s\n", result);
475+ fflush(stdout);
476+ return result;
477+}
478+
479+static void connect_tls_error_handler(WebKitWebView* wv) {
480+ if (!wv) return;
481+ g_signal_connect(G_OBJECT(wv), "load-failed-with-tls-errors",
482+ G_CALLBACK(on_load_failed_with_tls_errors), NULL);
483+}
484+
485+// TLS dialog response callback
486+static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data) {
487+ TLSDialogData *data = (TLSDialogData*)user_data;
488+ data->user_accepted = (response_id == GTK_RESPONSE_ACCEPT);
489+ data->dialog_completed = TRUE;
490+ printf("[dumber] TLS dialog response: %s (response_id=%d)\n",
491+ data->user_accepted ? "ACCEPTED" : "REJECTED", response_id);
492+ fflush(stdout);
493+ gtk_window_destroy(GTK_WINDOW(dialog));
494+}
495+
496+// Show TLS warning dialog synchronously using GTK4 patterns
497+static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
498+ printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
499+ printf("[dumber] Error message: %s\n", error_msg);
500+ fflush(stdout);
501+
502+ // Create the dialog
503+ GtkWidget *dialog = gtk_message_dialog_new(
504+ parent,
505+ GTK_DIALOG_MODAL,
506+ GTK_MESSAGE_WARNING,
507+ GTK_BUTTONS_NONE,
508+ "Certificate Error for %s", hostname
509+ );
510+
511+ // Set secondary text with error details
512+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error_msg);
513+
514+ // Add buttons
515+ gtk_dialog_add_button(GTK_DIALOG(dialog), "Go Back", GTK_RESPONSE_CANCEL);
516+ GtkWidget *proceed_btn = gtk_dialog_add_button(GTK_DIALOG(dialog), "Proceed Anyway (Unsafe)", GTK_RESPONSE_ACCEPT);
517+
518+ // Style the proceed button as destructive (red)
519+ gtk_widget_add_css_class(proceed_btn, "destructive-action");
520+
521+ // Set default response to cancel (safer option)
522+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
523+
524+ // Set up dialog data
525+ TLSDialogData dialog_data = { FALSE, FALSE };
526+
527+ // Connect response signal
528+ g_signal_connect(dialog, "response", G_CALLBACK(on_tls_dialog_response), &dialog_data);
529+
530+ // Show the dialog
531+ gtk_window_present(GTK_WINDOW(dialog));
532+
533+ // Run a nested main loop until dialog is completed
534+ while (!dialog_data.dialog_completed) {
535+ g_main_context_iteration(NULL, TRUE);
536+ }
537+
538+ return dialog_data.user_accepted;
539+}
360540 */
361541 import "C"
362542
@@ -378,7 +558,6 @@ func boolToInt(b bool) int {
378558 return 0
379559 }
380560
381-
382561 type nativeView struct {
383562 win *C.GtkWidget
384563 view *C.GtkWidget
@@ -394,20 +573,21 @@ type memoryStats struct {
394573
395574 // WebView represents a browser view powered by WebKit2GTK.
396575 type WebView struct {
397- config *Config
398- zoom float64
399- url string
400- destroyed bool
401- native *nativeView
402- window *Window
403- id uintptr
404- msgHandler func(payload string)
405- titleHandler func(title string)
406- uriHandler func(uri string)
407- zoomHandler func(level float64)
408- memStats *memoryStats
409- gcTicker *time.Ticker
410- gcDone chan struct{}
576+ config *Config
577+ zoom float64
578+ url string
579+ destroyed bool
580+ native *nativeView
581+ window *Window
582+ id uintptr
583+ msgHandler func(payload string)
584+ titleHandler func(title string)
585+ uriHandler func(uri string)
586+ zoomHandler func(level float64)
587+ memStats *memoryStats
588+ gcTicker *time.Ticker
589+ gcDone chan struct{}
590+ tlsExceptions map[string]bool // host -> whether user allowed certificate exception
411591 }
412592
413593 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
@@ -488,7 +668,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
488668 // Register custom URI scheme handler for "dumb://"
489669 {
490670 sch := C.CString("dumb")
491- C.webkit_web_context_register_uri_scheme(ctx, sch, (C.WebKitURISchemeRequestCallback)(C.on_uri_scheme), nil, nil)
671+ C.register_uri_scheme_handler(ctx, sch)
492672 C.free(unsafe.Pointer(sch))
493673 }
494674 // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
@@ -522,7 +702,8 @@ func NewWebView(cfg *Config) (*WebView, error) {
522702 lastGCTime: time.Now(),
523703 memoryPressureSettings: pressureSettings,
524704 },
525- gcDone: make(chan struct{}),
705+ gcDone: make(chan struct{}),
706+ tlsExceptions: make(map[string]bool),
526707 }
527708 // Assign an ID for accelerator dispatch
528709 v.id = nextViewID()
@@ -546,9 +727,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
546727
547728 // Watch for GTK theme changes and propagate to page at runtime
548729 if settings := C.gtk_settings_get_default(); settings != nil {
549- cprop := C.CString("notify::gtk-application-prefer-dark-theme")
550- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(settings)), cprop, C.GCallback(C.on_theme_changed), C.gpointer(v.id), nil, 0)
551- C.free(unsafe.Pointer(cprop))
730+ C.connect_theme_changed_with_id(settings, C.ulong(v.id))
552731 }
553732
554733 // Native zoom shortcuts (independent of app services)
@@ -602,17 +781,11 @@ func NewWebView(cfg *Config) (*WebView, error) {
602781 _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })
603782 _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })
604783 // Update window title when page title changes
605- cNotifyTitle1 := C.CString("notify::title")
606- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
607- C.free(unsafe.Pointer(cNotifyTitle1))
784+ C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
608785 // Also dispatch title change to Go with view id
609- cNotifyTitle2 := C.CString("notify::title")
610- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle2, C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)
611- C.free(unsafe.Pointer(cNotifyTitle2))
786+ C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
612787 // Notify URI changes to Go to keep current URL in sync
613- cNotifyURI := C.CString("notify::uri")
614- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyURI, C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)
615- C.free(unsafe.Pointer(cNotifyURI))
788+ C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
616789 // Apply hardware acceleration and related settings based on cfg.Rendering
617790 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
618791 // Hardware acceleration policy (guarded by version)
@@ -887,8 +1060,8 @@ func (w *WebView) enableUserContentManager() {
8871060 C.webkit_user_script_unref(schemeScript)
8881061 }
8891062
890- // Add user script at document-start (omnibox/find reusable component)
891- src := C.CString(getOmniboxScript())
1063+ // Add user script at document-start (omnibox/find reusable component)
1064+ src := C.CString(getOmniboxScript())
8921065 defer C.free(unsafe.Pointer(src))
8931066 script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
8941067 if script != nil {
@@ -1007,3 +1180,115 @@ func (w *WebView) TriggerMemoryCleanup() {
10071180 log.Printf("[webkit] Manual memory cleanup triggered")
10081181 }
10091182 }
1183+
1184+//export goHandleTLSError
1185+func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certInfo *C.char) C.gboolean {
1186+ uri := C.GoString(failingURI)
1187+ hostname := C.GoString(host)
1188+ certificateInfo := C.GoString(certInfo)
1189+
1190+ log.Printf("[tls] Certificate error for %s (URI: %s, flags: %d)", hostname, uri, int(errorFlags))
1191+
1192+ // Find the WebView that triggered this error
1193+ // For now, we'll get the most recently created view
1194+ // In a more sophisticated implementation, we'd pass the view ID through user_data
1195+ var webView *WebView
1196+ regMu.RLock()
1197+ for _, v := range viewByID {
1198+ if v != nil {
1199+ webView = v
1200+ break
1201+ }
1202+ }
1203+ regMu.RUnlock()
1204+
1205+ if webView == nil {
1206+ log.Printf("[tls] No WebView found to handle TLS error")
1207+ return C.FALSE // Don't proceed
1208+ }
1209+
1210+ // Check if we've already allowed this host
1211+ if allowed, exists := webView.tlsExceptions[hostname]; exists && allowed {
1212+ log.Printf("[tls] Certificate exception already granted for %s", hostname)
1213+ return C.TRUE // Proceed with the load
1214+ }
1215+
1216+ // Show warning dialog and get user decision
1217+ if webView.showTLSWarningDialog(hostname, uri, int(errorFlags), certificateInfo) {
1218+ webView.tlsExceptions[hostname] = true
1219+ log.Printf("[tls] User accepted certificate exception for %s", hostname)
1220+ return C.TRUE // Proceed with the load
1221+ }
1222+
1223+ log.Printf("[tls] User rejected certificate for %s", hostname)
1224+ return C.FALSE // Don't proceed
1225+}
1226+
1227+// showTLSWarningDialog displays a warning dialog for TLS certificate errors
1228+func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
1229+ if w == nil || w.native == nil || w.native.win == nil {
1230+ log.Printf("[tls] Cannot show dialog: WebView or window is nil")
1231+ return false
1232+ }
1233+
1234+ log.Printf("[tls] Showing certificate error dialog for %s", hostname)
1235+
1236+ // Format the error message with certificate information
1237+ errorMsg := formatTLSErrorMessage(hostname, uri, errorFlags, certificateInfo)
1238+
1239+ // Convert strings to C strings
1240+ cHostname := C.CString(hostname)
1241+ defer C.free(unsafe.Pointer(cHostname))
1242+
1243+ cErrorMsg := C.CString(errorMsg)
1244+ defer C.free(unsafe.Pointer(cErrorMsg))
1245+
1246+ // Show the dialog and get user response
1247+ result := C.show_tls_warning_dialog_sync(
1248+ (*C.GtkWindow)(unsafe.Pointer(w.native.win)),
1249+ cHostname,
1250+ cErrorMsg,
1251+ )
1252+
1253+ userAccepted := result != 0
1254+ log.Printf("[tls] User %s certificate for %s",
1255+ map[bool]string{true: "accepted", false: "rejected"}[userAccepted],
1256+ hostname)
1257+
1258+ return userAccepted
1259+}
1260+
1261+// formatTLSErrorMessage creates a detailed error message based on the error flags
1262+func formatTLSErrorMessage(hostname, uri string, errorFlags int, certificateInfo string) string {
1263+ msg := "Website: " + hostname + "\n\n"
1264+
1265+ // Add certificate information
1266+ if certificateInfo != "" {
1267+ msg += "Certificate Details:\n" + certificateInfo + "\n"
1268+ }
1269+
1270+ msg += "Security Issues:\n"
1271+ // Decode GTlsCertificateFlags (common values)
1272+ if errorFlags&1 != 0 { // G_TLS_CERTIFICATE_UNKNOWN_CA
1273+ msg += "• Certificate authority is not trusted\n"
1274+ }
1275+ if errorFlags&2 != 0 { // G_TLS_CERTIFICATE_BAD_IDENTITY
1276+ msg += "• Certificate does not match the website identity\n"
1277+ }
1278+ if errorFlags&4 != 0 { // G_TLS_CERTIFICATE_NOT_ACTIVATED
1279+ msg += "• Certificate is not yet valid\n"
1280+ }
1281+ if errorFlags&8 != 0 { // G_TLS_CERTIFICATE_EXPIRED
1282+ msg += "• Certificate has expired\n"
1283+ }
1284+ if errorFlags&16 != 0 { // G_TLS_CERTIFICATE_REVOKED
1285+ msg += "• Certificate has been revoked\n"
1286+ }
1287+ if errorFlags&32 != 0 { // G_TLS_CERTIFICATE_INSECURE
1288+ msg += "• Certificate uses weak cryptography\n"
1289+ }
1290+
1291+ msg += "\nProceeding may expose your data to attackers.\nOnly continue if you trust this website."
1292+
1293+ return msg
1294+}
@@ -13,19 +13,13 @@ package webkit
13 #include <jsc/jsc.h>13 #include <jsc/jsc.h>
14 #include <glib.h>14 #include <glib.h>
15 #include <gio/gio.h>15 #include <gio/gio.h>
16+#include "webview_callbacks.h"
16 17
17 static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }18 static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); }
18 19
19 // Forward declaration20 // Forward declaration
20 extern void goQuitMainLoop();21 extern void goQuitMainLoop();
21 22
22-// GTK4 close-request signal handler
23-static gboolean on_close_request(GtkWindow* window, gpointer user_data) {
24- (void)window; (void)user_data;
25- goQuitMainLoop();
26- return FALSE; // Allow the window to close
27-}
28-
29 // Connect window close signal to quit main loop23 // Connect window close signal to quit main loop
30 static void connect_destroy_quit(GtkWidget* w) {24 static void connect_destroy_quit(GtkWidget* w) {
31 if (!w) return;25 if (!w) return;
@@ -37,6 +31,25 @@ static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); }
37 // Forward declare helpers used below31 // Forward declare helpers used below
38 static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);32 static void maybe_set_cookie_policy(WebKitCookieManager* cm, int policy);
39 33
34+// TLS error handling forward declarations
35+extern gboolean goHandleTLSError(char* failing_uri, char* host, int error_flags, char* cert_info);
36+static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
37+ const char *failing_uri,
38+ GTlsCertificate *certificate,
39+ GTlsCertificateFlags errors,
40+ gpointer user_data);
41+static void connect_tls_error_handler(WebKitWebView* wv);
42+
43+// Dialog callback structure for TLS warnings
44+typedef struct {
45+ gboolean user_accepted;
46+ gboolean dialog_completed;
47+} TLSDialogData;
48+
49+static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data);
50+static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
51+static char* extract_certificate_info(GTlsCertificate* certificate);
52+
40 // Construct a WebKitWebView via g_object_new with a fresh UserContentManager.53 // Construct a WebKitWebView via g_object_new with a fresh UserContentManager.
41 static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm, WebKitMemoryPressureSettings* pressure_settings) {54 static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const char* cache_dir, const char* cookie_path, WebKitUserContentManager** out_ucm, WebKitMemoryPressureSettings* pressure_settings) {
42 WebKitUserContentManager* u = webkit_user_content_manager_new();55 WebKitUserContentManager* u = webkit_user_content_manager_new();
@@ -62,12 +75,20 @@ static GtkWidget* new_webview_with_ucm_and_session(const char* data_dir, const c
62 }75 }
63 // Persist credentials (HTTP auth, etc.)76 // Persist credentials (HTTP auth, etc.)
64 webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);77 webkit_network_session_set_persistent_credential_storage_enabled(sess, TRUE);
78+ // Configure TLS errors to emit signals instead of failing silently
79+ webkit_network_session_set_tls_errors_policy(sess, WEBKIT_TLS_ERRORS_POLICY_FAIL);
65 GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,80 GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
66 "user-content-manager", u,81 "user-content-manager", u,
67 "network-session", sess,82 "network-session", sess,
68 NULL));83 NULL));
69 // WebView holds refs to provided objects; drop our temporary refs84 // WebView holds refs to provided objects; drop our temporary refs
70 g_object_unref(sess);85 g_object_unref(sess);
86+ // Set WebView background to black (easier on eyes when pages are loading)
87+ GdkRGBA black_color = { 0.0, 0.0, 0.0, 1.0 };
88+ webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w), &black_color);
89+
90+ // Connect TLS error handler
91+ connect_tls_error_handler(WEBKIT_WEB_VIEW(w));
71 if (out_ucm) { *out_ucm = u; }92 if (out_ucm) { *out_ucm = u; }
72 return w;93 return w;
73 }94 }
@@ -85,87 +106,57 @@ static gboolean gtk_prefers_dark() {
85 }106 }
86 g_object_unref(desktop_settings);107 g_object_unref(desktop_settings);
87 }108 }
88- 109+
89 // Method 2: Check GTK theme name for dark variants110 // Method 2: Check GTK theme name for dark variants
90 GtkSettings* settings = gtk_settings_get_default();111 GtkSettings* settings = gtk_settings_get_default();
91 if (settings) {112 if (settings) {
92 gchar* theme_name = NULL;113 gchar* theme_name = NULL;
93 g_object_get(settings, "gtk-theme-name", &theme_name, NULL);114 g_object_get(settings, "gtk-theme-name", &theme_name, NULL);
94 if (theme_name) {115 if (theme_name) {
95- gboolean is_dark = (strstr(theme_name, "-dark") != NULL || 116+ gboolean is_dark = (strstr(theme_name, "-dark") != NULL ||
96 strstr(theme_name, "-Dark") != NULL);117 strstr(theme_name, "-Dark") != NULL);
97 g_free(theme_name);118 g_free(theme_name);
98 if (is_dark) return TRUE;119 if (is_dark) return TRUE;
99 }120 }
100- 121+
101 // Method 3: Check gtk-application-prefer-dark-theme (fallback)122 // Method 3: Check gtk-application-prefer-dark-theme (fallback)
102 gboolean prefer = FALSE;123 gboolean prefer = FALSE;
103 g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);124 g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
104 if (prefer) return TRUE;125 if (prefer) return TRUE;
105 }126 }
106- 127+
107 return FALSE;128 return FALSE;
108 }129 }
109 130
110 // Note: preferred color scheme is handled via a user script injection to support131 // Note: preferred color scheme is handled via a user script injection to support
111 // older WebKitGTK versions without the color-scheme API. See enableUserContentManager.132 // older WebKitGTK versions without the color-scheme API. See enableUserContentManager.
112 133
113-extern void goOnUcmMessage(unsigned long id, const char* json);134+extern void goOnUcmMessage(unsigned long id, char* json);
114-extern void goOnTitleChanged(unsigned long id, const char* title);135+
115-extern void goOnURIChanged(unsigned long id, const char* uri);136+// Helper functions to connect signals using static callbacks
116-extern void goOnThemeChanged(unsigned long id, int prefer_dark);137+static void connect_title_notify(GtkWidget* widget, GtkWindow* window) {
117-extern void* goResolveURIScheme(char* uri, size_t* out_len, char** out_mime);138+ if (!widget) return;
118-139+ g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify), G_OBJECT(window), NULL, 0);
119-void on_title_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {
120- (void)pspec;
121- WebKitWebView* view = WEBKIT_WEB_VIEW(obj);
122- const gchar* title = webkit_web_view_get_title(view);
123- GtkWindow* win = GTK_WINDOW(user_data);
124- if (title && win) {
125- gtk_window_set_title(win, title);
126- }
127 }140 }
128 141
129-void on_title_notify_id(GObject* obj, GParamSpec* pspec, gpointer user_data) {142+static void connect_title_notify_with_id(GtkWidget* widget, unsigned long id) {
130- (void)pspec; (void)obj;143+ if (!widget) return;
131- const gchar* title = webkit_web_view_get_title(WEBKIT_WEB_VIEW(obj));144+ g_signal_connect_data(G_OBJECT(widget), "notify::title", G_CALLBACK(on_title_notify_id), (gpointer)id, NULL, 0);
132- if (title) { goOnTitleChanged((unsigned long)user_data, title); }
133 }145 }
134 146
135-void on_uri_notify(GObject* obj, GParamSpec* pspec, gpointer user_data) {147+static void connect_uri_notify_with_id(GtkWidget* widget, unsigned long id) {
136- (void)pspec; (void)obj;148+ if (!widget) return;
137- const gchar* uri = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(obj));149+ g_signal_connect_data(G_OBJECT(widget), "notify::uri", G_CALLBACK(on_uri_notify), (gpointer)id, NULL, 0);
138- if (uri) { goOnURIChanged((unsigned long)user_data, uri); }
139 }150 }
140 151
141-// React to GTK theme preference changes at runtime152+static void connect_theme_changed_with_id(GtkSettings* settings, unsigned long id) {
142-void on_theme_changed(GObject* obj, GParamSpec* pspec, gpointer user_data) {
143- (void)pspec;
144- GtkSettings* settings = GTK_SETTINGS(obj);
145 if (!settings) return;153 if (!settings) return;
146- gboolean prefer = FALSE;154+ g_signal_connect_data(G_OBJECT(settings), "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_theme_changed), (gpointer)id, NULL, 0);
147- g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL);
148- goOnThemeChanged((unsigned long)user_data, prefer ? 1 : 0);
149 }155 }
150 156
151-// NOTE: UCM message callback signature changed in WebKitGTK 6; will be reimplemented later.157+static void register_uri_scheme_handler(WebKitWebContext* context, const char* scheme) {
152-158+ if (!context || !scheme) return;
153-void on_uri_scheme(WebKitURISchemeRequest* request, gpointer user_data) {159+ webkit_web_context_register_uri_scheme(context, scheme, (WebKitURISchemeRequestCallback)on_uri_scheme, NULL, NULL);
154- (void)user_data;
155- const gchar* uri = webkit_uri_scheme_request_get_uri(request);
156- size_t n = 0;
157- char* mime = NULL;
158- void* buf = goResolveURIScheme((char*)uri, &n, &mime);
159- if (buf && n > 0 && mime) {
160- GInputStream* stream = g_memory_input_stream_new_from_data(buf, (gssize)n, g_free);
161- webkit_uri_scheme_request_finish(request, stream, (gint64)n, mime);
162- g_object_unref(stream);
163- g_free(mime);
164- } else {
165- GError* err = g_error_new_literal(g_quark_from_string("dumber"), 404, "Not found");
166- webkit_uri_scheme_request_finish_error(request, err);
167- g_error_free(err);
168- }
169 }160 }
170 161
171 // Script message handler wiring for WebKitGTK 6162 // Script message handler wiring for WebKitGTK 6
@@ -357,6 +348,195 @@ static void maybe_collect_js(WebKitWebContext* context) {
357 // Disabled by default due to API availability variance across 6.0 builds.348 // Disabled by default due to API availability variance across 6.0 builds.
358 (void)context;349 (void)context;
359 }350 }
351+
352+// TLS error handling implementation
353+static gboolean on_load_failed_with_tls_errors(WebKitWebView *web_view,
354+ const char *failing_uri,
355+ GTlsCertificate *certificate,
356+ GTlsCertificateFlags errors,
357+ gpointer user_data) {
358+ (void)user_data;
359+
360+ // Extract host from URI for the Go callback
361+ const char* host = failing_uri;
362+ char* host_copy = NULL;
363+
364+ if (strstr(failing_uri, "://")) {
365+ host = strstr(failing_uri, "://") + 3;
366+ char* slash = strchr(host, '/');
367+ if (slash) {
368+ // Create a temporary null-terminated host string
369+ size_t host_len = slash - host;
370+ host_copy = malloc(host_len + 1);
371+ if (host_copy) {
372+ strncpy(host_copy, host, host_len);
373+ host_copy[host_len] = '\0';
374+ host = host_copy;
375+ }
376+ }
377+ }
378+
379+ // Extract certificate information
380+ char* cert_info = extract_certificate_info(certificate);
381+
382+ gboolean should_proceed = goHandleTLSError((char*)failing_uri, (char*)host, (int)errors, cert_info);
383+
384+ // If user accepted, allow the certificate for this host
385+ if (should_proceed && certificate) {
386+ printf("[dumber] User accepted certificate - adding exception and triggering new load\n");
387+ fflush(stdout);
388+ // Get the network session from the web view
389+ WebKitNetworkSession* session = webkit_web_view_get_network_session(web_view);
390+ if (session) {
391+ webkit_network_session_allow_tls_certificate_for_host(session, certificate, host);
392+ printf("[dumber] Certificate exception added for host: %s\n", host);
393+ fflush(stdout);
394+
395+ // Try loading the URL again after adding the exception
396+ webkit_web_view_load_uri(web_view, failing_uri);
397+ printf("[dumber] Triggered new load of %s with certificate exception\n", failing_uri);
398+ fflush(stdout);
399+ }
400+ }
401+
402+ // Free the certificate info string
403+ if (cert_info) {
404+ g_free(cert_info);
405+ }
406+
407+ if (host_copy) {
408+ free(host_copy);
409+ }
410+
411+ return should_proceed;
412+}
413+
414+// Extract certificate information for display
415+static char* extract_certificate_info(GTlsCertificate* certificate) {
416+ if (!certificate) {
417+ return g_strdup("Certificate information not available");
418+ }
419+
420+ printf("[dumber] Extracting certificate information...\n");
421+ fflush(stdout);
422+
423+ GString* info = g_string_new("Certificate Information:\n");
424+
425+ // Try to get certificate properties - but these might not be available in all GTK versions
426+ gchar* subject = NULL;
427+ gchar* issuer = NULL;
428+ GDateTime* not_valid_before = NULL;
429+ GDateTime* not_valid_after = NULL;
430+
431+ // Try property access but handle failures gracefully
432+ g_object_get(certificate,
433+ "subject-name", &subject,
434+ "issuer-name", &issuer,
435+ "not-valid-before", &not_valid_before,
436+ "not-valid-after", &not_valid_after,
437+ NULL);
438+
439+ if (subject) {
440+ g_string_append_printf(info, "Subject: %s\n", subject);
441+ g_free(subject);
442+ printf("[dumber] Found subject info\n");
443+ } else {
444+ g_string_append(info, "Subject: Information not available\n");
445+ }
446+
447+ if (issuer) {
448+ g_string_append_printf(info, "Issued by: %s\n", issuer);
449+ g_free(issuer);
450+ printf("[dumber] Found issuer info\n");
451+ } else {
452+ g_string_append(info, "Issued by: Information not available\n");
453+ }
454+
455+ if (not_valid_before) {
456+ gchar* date_str = g_date_time_format(not_valid_before, "%Y-%m-%d %H:%M:%S UTC");
457+ g_string_append_printf(info, "Valid from: %s\n", date_str);
458+ g_free(date_str);
459+ g_date_time_unref(not_valid_before);
460+ } else {
461+ g_string_append(info, "Valid from: Information not available\n");
462+ }
463+
464+ if (not_valid_after) {
465+ gchar* date_str = g_date_time_format(not_valid_after, "%Y-%m-%d %H:%M:%S UTC");
466+ g_string_append_printf(info, "Valid until: %s\n", date_str);
467+ g_free(date_str);
468+ g_date_time_unref(not_valid_after);
469+ } else {
470+ g_string_append(info, "Valid until: Information not available\n");
471+ }
472+
473+ char* result = g_string_free(info, FALSE);
474+ printf("[dumber] Certificate info: %s\n", result);
475+ fflush(stdout);
476+ return result;
477+}
478+
479+static void connect_tls_error_handler(WebKitWebView* wv) {
480+ if (!wv) return;
481+ g_signal_connect(G_OBJECT(wv), "load-failed-with-tls-errors",
482+ G_CALLBACK(on_load_failed_with_tls_errors), NULL);
483+}
484+
485+// TLS dialog response callback
486+static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data) {
487+ TLSDialogData *data = (TLSDialogData*)user_data;
488+ data->user_accepted = (response_id == GTK_RESPONSE_ACCEPT);
489+ data->dialog_completed = TRUE;
490+ printf("[dumber] TLS dialog response: %s (response_id=%d)\n",
491+ data->user_accepted ? "ACCEPTED" : "REJECTED", response_id);
492+ fflush(stdout);
493+ gtk_window_destroy(GTK_WINDOW(dialog));
494+}
495+
496+// Show TLS warning dialog synchronously using GTK4 patterns
497+static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
498+ printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
499+ printf("[dumber] Error message: %s\n", error_msg);
500+ fflush(stdout);
501+
502+ // Create the dialog
503+ GtkWidget *dialog = gtk_message_dialog_new(
504+ parent,
505+ GTK_DIALOG_MODAL,
506+ GTK_MESSAGE_WARNING,
507+ GTK_BUTTONS_NONE,
508+ "Certificate Error for %s", hostname
509+ );
510+
511+ // Set secondary text with error details
512+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error_msg);
513+
514+ // Add buttons
515+ gtk_dialog_add_button(GTK_DIALOG(dialog), "Go Back", GTK_RESPONSE_CANCEL);
516+ GtkWidget *proceed_btn = gtk_dialog_add_button(GTK_DIALOG(dialog), "Proceed Anyway (Unsafe)", GTK_RESPONSE_ACCEPT);
517+
518+ // Style the proceed button as destructive (red)
519+ gtk_widget_add_css_class(proceed_btn, "destructive-action");
520+
521+ // Set default response to cancel (safer option)
522+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
523+
524+ // Set up dialog data
525+ TLSDialogData dialog_data = { FALSE, FALSE };
526+
527+ // Connect response signal
528+ g_signal_connect(dialog, "response", G_CALLBACK(on_tls_dialog_response), &dialog_data);
529+
530+ // Show the dialog
531+ gtk_window_present(GTK_WINDOW(dialog));
532+
533+ // Run a nested main loop until dialog is completed
534+ while (!dialog_data.dialog_completed) {
535+ g_main_context_iteration(NULL, TRUE);
536+ }
537+
538+ return dialog_data.user_accepted;
539+}
360 */540 */
361 import "C"541 import "C"
362 542
@@ -378,7 +558,6 @@ func boolToInt(b bool) int {
378 return 0558 return 0
379 }559 }
380 560
381-
382 type nativeView struct {561 type nativeView struct {
383 win *C.GtkWidget562 win *C.GtkWidget
384 view *C.GtkWidget563 view *C.GtkWidget
@@ -394,20 +573,21 @@ type memoryStats struct {
394 573
395 // WebView represents a browser view powered by WebKit2GTK.574 // WebView represents a browser view powered by WebKit2GTK.
396 type WebView struct {575 type WebView struct {
397- config *Config576+ config *Config
398- zoom float64577+ zoom float64
399- url string578+ url string
400- destroyed bool579+ destroyed bool
401- native *nativeView580+ native *nativeView
402- window *Window581+ window *Window
403- id uintptr582+ id uintptr
404- msgHandler func(payload string)583+ msgHandler func(payload string)
405- titleHandler func(title string)584+ titleHandler func(title string)
406- uriHandler func(uri string)585+ uriHandler func(uri string)
407- zoomHandler func(level float64)586+ zoomHandler func(level float64)
408- memStats *memoryStats587+ memStats *memoryStats
409- gcTicker *time.Ticker588+ gcTicker *time.Ticker
410- gcDone chan struct{}589+ gcDone chan struct{}
590+ tlsExceptions map[string]bool // host -> whether user allowed certificate exception
411 }591 }
412 592
413 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.593 // NewWebView constructs a new WebView instance with native WebKit2GTK widgets.
@@ -488,7 +668,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
488 // Register custom URI scheme handler for "dumb://"668 // Register custom URI scheme handler for "dumb://"
489 {669 {
490 sch := C.CString("dumb")670 sch := C.CString("dumb")
491- C.webkit_web_context_register_uri_scheme(ctx, sch, (C.WebKitURISchemeRequestCallback)(C.on_uri_scheme), nil, nil)671+ C.register_uri_scheme_handler(ctx, sch)
492 C.free(unsafe.Pointer(sch))672 C.free(unsafe.Pointer(sch))
493 }673 }
494 // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session674 // Cookie manager persistent storage handled via NetworkSession in new_webview_with_ucm_and_session
@@ -522,7 +702,8 @@ func NewWebView(cfg *Config) (*WebView, error) {
522 lastGCTime: time.Now(),702 lastGCTime: time.Now(),
523 memoryPressureSettings: pressureSettings,703 memoryPressureSettings: pressureSettings,
524 },704 },
525- gcDone: make(chan struct{}),705+ gcDone: make(chan struct{}),
706+ tlsExceptions: make(map[string]bool),
526 }707 }
527 // Assign an ID for accelerator dispatch708 // Assign an ID for accelerator dispatch
528 v.id = nextViewID()709 v.id = nextViewID()
@@ -546,9 +727,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
546 727
547 // Watch for GTK theme changes and propagate to page at runtime728 // Watch for GTK theme changes and propagate to page at runtime
548 if settings := C.gtk_settings_get_default(); settings != nil {729 if settings := C.gtk_settings_get_default(); settings != nil {
549- cprop := C.CString("notify::gtk-application-prefer-dark-theme")730+ C.connect_theme_changed_with_id(settings, C.ulong(v.id))
550- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(settings)), cprop, C.GCallback(C.on_theme_changed), C.gpointer(v.id), nil, 0)
551- C.free(unsafe.Pointer(cprop))
552 }731 }
553 732
554 // Native zoom shortcuts (independent of app services)733 // Native zoom shortcuts (independent of app services)
@@ -602,17 +781,11 @@ func NewWebView(cfg *Config) (*WebView, error) {
602 _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })781 _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() })
603 _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })782 _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() })
604 // Update window title when page title changes783 // Update window title when page title changes
605- cNotifyTitle1 := C.CString("notify::title")784+ C.connect_title_notify(viewWidget, (*C.GtkWindow)(unsafe.Pointer(win)))
606- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle1, C.GCallback(C.on_title_notify), C.gpointer(unsafe.Pointer(win)), nil, 0)
607- C.free(unsafe.Pointer(cNotifyTitle1))
608 // Also dispatch title change to Go with view id785 // Also dispatch title change to Go with view id
609- cNotifyTitle2 := C.CString("notify::title")786+ C.connect_title_notify_with_id(viewWidget, C.ulong(v.id))
610- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyTitle2, C.GCallback(C.on_title_notify_id), C.gpointer(v.id), nil, 0)
611- C.free(unsafe.Pointer(cNotifyTitle2))
612 // Notify URI changes to Go to keep current URL in sync787 // Notify URI changes to Go to keep current URL in sync
613- cNotifyURI := C.CString("notify::uri")788+ C.connect_uri_notify_with_id(viewWidget, C.ulong(v.id))
614- C.g_signal_connect_data(C.gpointer(unsafe.Pointer(viewWidget)), cNotifyURI, C.GCallback(C.on_uri_notify), C.gpointer(v.id), nil, 0)
615- C.free(unsafe.Pointer(cNotifyURI))
616 // Apply hardware acceleration and related settings based on cfg.Rendering789 // Apply hardware acceleration and related settings based on cfg.Rendering
617 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {790 if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil {
618 // Hardware acceleration policy (guarded by version)791 // Hardware acceleration policy (guarded by version)
@@ -887,8 +1060,8 @@ func (w *WebView) enableUserContentManager() {
887 C.webkit_user_script_unref(schemeScript)1060 C.webkit_user_script_unref(schemeScript)
888 }1061 }
889 1062
890- // Add user script at document-start (omnibox/find reusable component)1063+ // Add user script at document-start (omnibox/find reusable component)
891- src := C.CString(getOmniboxScript())1064+ src := C.CString(getOmniboxScript())
892 defer C.free(unsafe.Pointer(src))1065 defer C.free(unsafe.Pointer(src))
893 script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)1066 script := C.webkit_user_script_new((*C.gchar)(src), C.WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
894 if script != nil {1067 if script != nil {
@@ -1007,3 +1180,115 @@ func (w *WebView) TriggerMemoryCleanup() {
1007 log.Printf("[webkit] Manual memory cleanup triggered")1180 log.Printf("[webkit] Manual memory cleanup triggered")
1008 }1181 }
1009 }1182 }
1183+
1184+//export goHandleTLSError
1185+func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certInfo *C.char) C.gboolean {
1186+ uri := C.GoString(failingURI)
1187+ hostname := C.GoString(host)
1188+ certificateInfo := C.GoString(certInfo)
1189+
1190+ log.Printf("[tls] Certificate error for %s (URI: %s, flags: %d)", hostname, uri, int(errorFlags))
1191+
1192+ // Find the WebView that triggered this error
1193+ // For now, we'll get the most recently created view
1194+ // In a more sophisticated implementation, we'd pass the view ID through user_data
1195+ var webView *WebView
1196+ regMu.RLock()
1197+ for _, v := range viewByID {
1198+ if v != nil {
1199+ webView = v
1200+ break
1201+ }
1202+ }
1203+ regMu.RUnlock()
1204+
1205+ if webView == nil {
1206+ log.Printf("[tls] No WebView found to handle TLS error")
1207+ return C.FALSE // Don't proceed
1208+ }
1209+
1210+ // Check if we've already allowed this host
1211+ if allowed, exists := webView.tlsExceptions[hostname]; exists && allowed {
1212+ log.Printf("[tls] Certificate exception already granted for %s", hostname)
1213+ return C.TRUE // Proceed with the load
1214+ }
1215+
1216+ // Show warning dialog and get user decision
1217+ if webView.showTLSWarningDialog(hostname, uri, int(errorFlags), certificateInfo) {
1218+ webView.tlsExceptions[hostname] = true
1219+ log.Printf("[tls] User accepted certificate exception for %s", hostname)
1220+ return C.TRUE // Proceed with the load
1221+ }
1222+
1223+ log.Printf("[tls] User rejected certificate for %s", hostname)
1224+ return C.FALSE // Don't proceed
1225+}
1226+
1227+// showTLSWarningDialog displays a warning dialog for TLS certificate errors
1228+func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
1229+ if w == nil || w.native == nil || w.native.win == nil {
1230+ log.Printf("[tls] Cannot show dialog: WebView or window is nil")
1231+ return false
1232+ }
1233+
1234+ log.Printf("[tls] Showing certificate error dialog for %s", hostname)
1235+
1236+ // Format the error message with certificate information
1237+ errorMsg := formatTLSErrorMessage(hostname, uri, errorFlags, certificateInfo)
1238+
1239+ // Convert strings to C strings
1240+ cHostname := C.CString(hostname)
1241+ defer C.free(unsafe.Pointer(cHostname))
1242+
1243+ cErrorMsg := C.CString(errorMsg)
1244+ defer C.free(unsafe.Pointer(cErrorMsg))
1245+
1246+ // Show the dialog and get user response
1247+ result := C.show_tls_warning_dialog_sync(
1248+ (*C.GtkWindow)(unsafe.Pointer(w.native.win)),
1249+ cHostname,
1250+ cErrorMsg,
1251+ )
1252+
1253+ userAccepted := result != 0
1254+ log.Printf("[tls] User %s certificate for %s",
1255+ map[bool]string{true: "accepted", false: "rejected"}[userAccepted],
1256+ hostname)
1257+
1258+ return userAccepted
1259+}
1260+
1261+// formatTLSErrorMessage creates a detailed error message based on the error flags
1262+func formatTLSErrorMessage(hostname, uri string, errorFlags int, certificateInfo string) string {
1263+ msg := "Website: " + hostname + "\n\n"
1264+
1265+ // Add certificate information
1266+ if certificateInfo != "" {
1267+ msg += "Certificate Details:\n" + certificateInfo + "\n"
1268+ }
1269+
1270+ msg += "Security Issues:\n"
1271+ // Decode GTlsCertificateFlags (common values)
1272+ if errorFlags&1 != 0 { // G_TLS_CERTIFICATE_UNKNOWN_CA
1273+ msg += "• Certificate authority is not trusted\n"
1274+ }
1275+ if errorFlags&2 != 0 { // G_TLS_CERTIFICATE_BAD_IDENTITY
1276+ msg += "• Certificate does not match the website identity\n"
1277+ }
1278+ if errorFlags&4 != 0 { // G_TLS_CERTIFICATE_NOT_ACTIVATED
1279+ msg += "• Certificate is not yet valid\n"
1280+ }
1281+ if errorFlags&8 != 0 { // G_TLS_CERTIFICATE_EXPIRED
1282+ msg += "• Certificate has expired\n"
1283+ }
1284+ if errorFlags&16 != 0 { // G_TLS_CERTIFICATE_REVOKED
1285+ msg += "• Certificate has been revoked\n"
1286+ }
1287+ if errorFlags&32 != 0 { // G_TLS_CERTIFICATE_INSECURE
1288+ msg += "• Certificate uses weak cryptography\n"
1289+ }
1290+
1291+ msg += "\nProceeding may expose your data to attackers.\nOnly continue if you trust this website."
1292+
1293+ return msg
1294+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close