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
- ff83ba776194ed4d6904eaa195dcdfcf0283757a
- author
- bamen · · 2025-09-10 19:55 UTC
- committer
- bamen · · 2025-09-10 17:59 UTC
- parent
- 00f6868
- signature
- unsigned
3 files · +459 -89
M CHANGELOG.md +13 -0
| @@ -2,6 +2,19 @@ | |||
| 2 | 2 | ||
| 3 | 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 | 18 | ## [0.4.0] - 2025-09-10 | |
| 6 | 19 | ||
| 7 | 20 | ### 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-10 | 18 | ## [0.4.0] - 2025-09-10 | ||
| 6 | 19 | ||||
| 7 | ### Added | 20 | ### 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 | |||
| 13 | 13 | #include <jsc/jsc.h> | |
| 14 | 14 | #include <glib.h> | |
| 15 | 15 | #include <gio/gio.h> | |
| 16 | + | #include "webview_callbacks.h" | |
| 16 | 17 | ||
| 17 | 18 | static GtkWidget* new_window() { return GTK_WIDGET(gtk_window_new()); } | |
| 18 | 19 | ||
| 19 | 20 | // Forward declaration | |
| 20 | 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 | 23 | // Connect window close signal to quit main loop | |
| 30 | 24 | static void connect_destroy_quit(GtkWidget* w) { | |
| 31 | 25 | if (!w) return; | |
| @@ -37,6 +31,25 @@ static WebKitWebView* as_webview(GtkWidget* w) { return WEBKIT_WEB_VIEW(w); } | |||
| 37 | 31 | // Forward declare helpers used below | |
| 38 | 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 | 53 | // Construct a WebKitWebView via g_object_new with a fresh UserContentManager. | |
| 41 | 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 | 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 | 76 | // Persist credentials (HTTP auth, etc.) | |
| 64 | 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 | 80 | GtkWidget* w = GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW, | |
| 66 | 81 | "user-content-manager", u, | |
| 67 | 82 | "network-session", sess, | |
| 68 | 83 | NULL)); | |
| 69 | 84 | // WebView holds refs to provided objects; drop our temporary refs | |
| 70 | 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 | 92 | if (out_ucm) { *out_ucm = u; } | |
| 72 | 93 | return w; | |
| 73 | 94 | } | |
| @@ -85,87 +106,57 @@ static gboolean gtk_prefers_dark() { | |||
| 85 | 106 | } | |
| 86 | 107 | g_object_unref(desktop_settings); | |
| 87 | 108 | } | |
| 88 | - | ||
| 109 | + | ||
| 89 | 110 | // Method 2: Check GTK theme name for dark variants | |
| 90 | 111 | GtkSettings* settings = gtk_settings_get_default(); | |
| 91 | 112 | if (settings) { | |
| 92 | 113 | gchar* theme_name = NULL; | |
| 93 | 114 | g_object_get(settings, "gtk-theme-name", &theme_name, NULL); | |
| 94 | 115 | if (theme_name) { | |
| 95 | - | gboolean is_dark = (strstr(theme_name, "-dark") != NULL || | |
| 116 | + | gboolean is_dark = (strstr(theme_name, "-dark") != NULL || | |
| 96 | 117 | strstr(theme_name, "-Dark") != NULL); | |
| 97 | 118 | g_free(theme_name); | |
| 98 | 119 | if (is_dark) return TRUE; | |
| 99 | 120 | } | |
| 100 | - | ||
| 121 | + | ||
| 101 | 122 | // Method 3: Check gtk-application-prefer-dark-theme (fallback) | |
| 102 | 123 | gboolean prefer = FALSE; | |
| 103 | 124 | g_object_get(settings, "gtk-application-prefer-dark-theme", &prefer, NULL); | |
| 104 | 125 | if (prefer) return TRUE; | |
| 105 | 126 | } | |
| 106 | - | ||
| 127 | + | ||
| 107 | 128 | return FALSE; | |
| 108 | 129 | } | |
| 109 | 130 | ||
| 110 | 131 | // Note: preferred color scheme is handled via a user script injection to support | |
| 111 | 132 | // older WebKitGTK versions without the color-scheme API. See enableUserContentManager. | |
| 112 | 133 | ||
| 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); | |
| 127 | 140 | } | |
| 128 | 141 | ||
| 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); | |
| 133 | 145 | } | |
| 134 | 146 | ||
| 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); | |
| 139 | 150 | } | |
| 140 | 151 | ||
| 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) { | |
| 145 | 153 | 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); | |
| 149 | 155 | } | |
| 150 | 156 | ||
| 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); | |
| 169 | 160 | } | |
| 170 | 161 | ||
| 171 | 162 | // Script message handler wiring for WebKitGTK 6 | |
| @@ -357,6 +348,195 @@ static void maybe_collect_js(WebKitWebContext* context) { | |||
| 357 | 348 | // Disabled by default due to API availability variance across 6.0 builds. | |
| 358 | 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", ¬_valid_before, | |
| 436 | + | "not-valid-after", ¬_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 | 541 | import "C" | |
| 362 | 542 | ||
| @@ -378,7 +558,6 @@ func boolToInt(b bool) int { | |||
| 378 | 558 | return 0 | |
| 379 | 559 | } | |
| 380 | 560 | ||
| 381 | - | ||
| 382 | 561 | type nativeView struct { | |
| 383 | 562 | win *C.GtkWidget | |
| 384 | 563 | view *C.GtkWidget | |
| @@ -394,20 +573,21 @@ type memoryStats struct { | |||
| 394 | 573 | ||
| 395 | 574 | // WebView represents a browser view powered by WebKit2GTK. | |
| 396 | 575 | 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 | |
| 411 | 591 | } | |
| 412 | 592 | ||
| 413 | 593 | // NewWebView constructs a new WebView instance with native WebKit2GTK widgets. | |
| @@ -488,7 +668,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 488 | 668 | // Register custom URI scheme handler for "dumb://" | |
| 489 | 669 | { | |
| 490 | 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 | 672 | C.free(unsafe.Pointer(sch)) | |
| 493 | 673 | } | |
| 494 | 674 | // 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 | 702 | lastGCTime: time.Now(), | |
| 523 | 703 | memoryPressureSettings: pressureSettings, | |
| 524 | 704 | }, | |
| 525 | - | gcDone: make(chan struct{}), | |
| 705 | + | gcDone: make(chan struct{}), | |
| 706 | + | tlsExceptions: make(map[string]bool), | |
| 526 | 707 | } | |
| 527 | 708 | // Assign an ID for accelerator dispatch | |
| 528 | 709 | v.id = nextViewID() | |
| @@ -546,9 +727,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 546 | 727 | ||
| 547 | 728 | // Watch for GTK theme changes and propagate to page at runtime | |
| 548 | 729 | 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)) | |
| 552 | 731 | } | |
| 553 | 732 | ||
| 554 | 733 | // Native zoom shortcuts (independent of app services) | |
| @@ -602,17 +781,11 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 602 | 781 | _ = v.RegisterKeyboardShortcut("alt+ArrowLeft", func() { _ = v.GoBack() }) | |
| 603 | 782 | _ = v.RegisterKeyboardShortcut("alt+ArrowRight", func() { _ = v.GoForward() }) | |
| 604 | 783 | // 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))) | |
| 608 | 785 | // 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)) | |
| 612 | 787 | // 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)) | |
| 616 | 789 | // Apply hardware acceleration and related settings based on cfg.Rendering | |
| 617 | 790 | if settings := C.webkit_web_view_get_settings(v.native.wv); settings != nil { | |
| 618 | 791 | // Hardware acceleration policy (guarded by version) | |
| @@ -887,8 +1060,8 @@ func (w *WebView) enableUserContentManager() { | |||
| 887 | 1060 | C.webkit_user_script_unref(schemeScript) | |
| 888 | 1061 | } | |
| 889 | 1062 | ||
| 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()) | |
| 892 | 1065 | defer C.free(unsafe.Pointer(src)) | |
| 893 | 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 | 1067 | if script != nil { | |
| @@ -1007,3 +1180,115 @@ func (w *WebView) TriggerMemoryCleanup() { | |||
| 1007 | 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 | + | } | |
| @@ -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 declaration | 20 | // 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 loop | 23 | // 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 below | 31 | // 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 refs | 84 | // 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 variants | 110 | // 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 support | 131 | // 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 runtime | 152 | + | 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 6 | 162 | // 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", ¬_valid_before, | |||
| 436 | + | "not-valid-after", ¬_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 0 | 558 | return 0 | ||
| 379 | } | 559 | } | ||
| 380 | 560 | ||||
| 381 | - | ||||
| 382 | type nativeView struct { | 561 | type nativeView struct { | ||
| 383 | win *C.GtkWidget | 562 | win *C.GtkWidget | ||
| 384 | view *C.GtkWidget | 563 | 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 *Config | 576 | + | config *Config |
| 398 | - | zoom float64 | 577 | + | zoom float64 |
| 399 | - | url string | 578 | + | url string |
| 400 | - | destroyed bool | 579 | + | destroyed bool |
| 401 | - | native *nativeView | 580 | + | native *nativeView |
| 402 | - | window *Window | 581 | + | window *Window |
| 403 | - | id uintptr | 582 | + | 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 *memoryStats | 587 | + | memStats *memoryStats |
| 409 | - | gcTicker *time.Ticker | 588 | + | 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_session | 674 | // 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 dispatch | 708 | // 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 runtime | 728 | // 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 changes | 783 | // 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 id | 785 | // 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 sync | 787 | // 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.Rendering | 789 | // 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 | + | } | |||