detached 8772236

feat(tls): modernize GTK dialogs with persistent certificate validation

- Migrate from deprecated GTK 4.10 dialog functions to GtkAlertDialog API
- Add three-option certificate dialog: Go Back, Proceed Once, Always Accept
- Implement database-backed certificate storage with SHA256 hashing
- Add smart decision handling with temporary (24h) and permanent validation
- Eliminate deprecation warnings and improve dialog accessibility
- Clean up migration directories and update SQLC configuration
commit
8772236c38b6ee3b2ccdda1074799455e3c47385
author
bamen · · 2025-09-11 20:46 UTC
committer
bamen · · 2025-09-11 17:48 UTC
parent
915715d
signature
unsigned
14 files · +317 -120
M CHANGELOG.md +10 -0
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55 ## [Unreleased]
66
77 ### Added
8+- **Persistent TLS certificate validation system**:
9+ - **Three-option certificate dialog**: "Go Back", "Proceed Once (Unsafe)", and "Always Accept This Site"
10+ - **Database-backed certificate storage**: SQLite-based persistence for user certificate decisions with SHA256 certificate hashing
11+ - **Smart decision handling**: Automatic application of stored decisions without repeated prompts
12+ - **Temporary vs permanent choices**: "Proceed Once" expires after 24 hours, "Always Accept" persists indefinitely
13+ - **Seamless user experience**: No more repetitive certificate warnings for trusted sites
814 - **Performance-optimized WebKit memory configuration system**:
915 - **New `WebkitMemoryConfig`** configuration section with performance-focused defaults for faster page loading
1016 - **Aggressive caching strategy**: `web_browser` cache model with page cache enabled for instant back/forward navigation
@@ -15,6 +21,10 @@ All notable changes to this project will be documented in this file.
1521 - **Early crash handling initialization**: Moved crash handler setup to application entry point for better crash recovery
1622
1723 ### Changed
24+- **Modernized GTK dialog system**: Migrated from deprecated GTK 4.10 dialog functions to modern `GtkAlertDialog` API
25+ - **Future-proof implementation**: Eliminated all deprecation warnings by replacing `gtk_message_dialog_*` and `gtk_dialog_*` functions
26+ - **Async-first architecture**: Native async dialog handling with proper event loop integration
27+ - **Improved accessibility**: Better screen reader support and GTK4 theme integration
1828 - **Major main.go refactoring**: Massive code reduction from 1071 lines to 61 lines (~95% reduction)
1929 - **Extracted browser application logic** to dedicated `internal/app/browser` package for better separation of concerns
2030 - **Streamlined entry point**: Clean main function focusing only on CLI vs GUI mode detection and application bootstrapping
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5 ## [Unreleased]5 ## [Unreleased]
6 6
7 ### Added7 ### Added
8+- **Persistent TLS certificate validation system**:
9+ - **Three-option certificate dialog**: "Go Back", "Proceed Once (Unsafe)", and "Always Accept This Site"
10+ - **Database-backed certificate storage**: SQLite-based persistence for user certificate decisions with SHA256 certificate hashing
11+ - **Smart decision handling**: Automatic application of stored decisions without repeated prompts
12+ - **Temporary vs permanent choices**: "Proceed Once" expires after 24 hours, "Always Accept" persists indefinitely
13+ - **Seamless user experience**: No more repetitive certificate warnings for trusted sites
8 - **Performance-optimized WebKit memory configuration system**:14 - **Performance-optimized WebKit memory configuration system**:
9 - **New `WebkitMemoryConfig`** configuration section with performance-focused defaults for faster page loading15 - **New `WebkitMemoryConfig`** configuration section with performance-focused defaults for faster page loading
10 - **Aggressive caching strategy**: `web_browser` cache model with page cache enabled for instant back/forward navigation16 - **Aggressive caching strategy**: `web_browser` cache model with page cache enabled for instant back/forward navigation
@@ -15,6 +21,10 @@ All notable changes to this project will be documented in this file.
15 - **Early crash handling initialization**: Moved crash handler setup to application entry point for better crash recovery21 - **Early crash handling initialization**: Moved crash handler setup to application entry point for better crash recovery
16 22
17 ### Changed23 ### Changed
24+- **Modernized GTK dialog system**: Migrated from deprecated GTK 4.10 dialog functions to modern `GtkAlertDialog` API
25+ - **Future-proof implementation**: Eliminated all deprecation warnings by replacing `gtk_message_dialog_*` and `gtk_dialog_*` functions
26+ - **Async-first architecture**: Native async dialog handling with proper event loop integration
27+ - **Improved accessibility**: Better screen reader support and GTK4 theme integration
18 - **Major main.go refactoring**: Massive code reduction from 1071 lines to 61 lines (~95% reduction)28 - **Major main.go refactoring**: Massive code reduction from 1071 lines to 61 lines (~95% reduction)
19 - **Extracted browser application logic** to dedicated `internal/app/browser` package for better separation of concerns29 - **Extracted browser application logic** to dedicated `internal/app/browser` package for better separation of concerns
20 - **Streamlined entry point**: Clean main function focusing only on CLI vs GUI mode detection and application bootstrapping 30 - **Streamlined entry point**: Clean main function focusing only on CLI vs GUI mode detection and application bootstrapping
M go.mod +1 -0
@@ -14,6 +14,7 @@ require (
1414 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
1515 github.com/google/go-cmp v0.7.0 // indirect
1616 github.com/inconshreveable/mousetrap v1.1.0 // indirect
17+ github.com/mattn/go-sqlite3 v1.14.32 // indirect
1718 github.com/ncruces/julianday v1.0.0 // indirect
1819 github.com/pelletier/go-toml/v2 v2.2.4 // indirect
1920 github.com/rogpeppe/go-internal v1.12.0 // indirect
@@ -14,6 +14,7 @@ require (
14 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect14 github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
15 github.com/google/go-cmp v0.7.0 // indirect15 github.com/google/go-cmp v0.7.0 // indirect
16 github.com/inconshreveable/mousetrap v1.1.0 // indirect16 github.com/inconshreveable/mousetrap v1.1.0 // indirect
17+ github.com/mattn/go-sqlite3 v1.14.32 // indirect
17 github.com/ncruces/julianday v1.0.0 // indirect18 github.com/ncruces/julianday v1.0.0 // indirect
18 github.com/pelletier/go-toml/v2 v2.2.4 // indirect19 github.com/pelletier/go-toml/v2 v2.2.4 // indirect
19 github.com/rogpeppe/go-internal v1.12.0 // indirect20 github.com/rogpeppe/go-internal v1.12.0 // indirect
M go.sum +2 -0
@@ -22,6 +22,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
2222 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
2323 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
2424 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25+github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
26+github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
2527 github.com/ncruces/go-sqlite3 v0.29.0 h1:1tsLiagCoqZEfcHDeKsNSv5jvrY/Iu393pAnw2wLNJU=
2628 github.com/ncruces/go-sqlite3 v0.29.0/go.mod h1:r1hSvYKPNJ+OlUA1O3r8o9LAawzPAlqeZiIdxTBBBJ0=
2729 github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
@@ -22,6 +22,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
22 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=22 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
23 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=23 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=24 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25+github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
26+github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
25 github.com/ncruces/go-sqlite3 v0.29.0 h1:1tsLiagCoqZEfcHDeKsNSv5jvrY/Iu393pAnw2wLNJU=27 github.com/ncruces/go-sqlite3 v0.29.0 h1:1tsLiagCoqZEfcHDeKsNSv5jvrY/Iu393pAnw2wLNJU=
26 github.com/ncruces/go-sqlite3 v0.29.0/go.mod h1:r1hSvYKPNJ+OlUA1O3r8o9LAawzPAlqeZiIdxTBBBJ0=28 github.com/ncruces/go-sqlite3 v0.29.0/go.mod h1:r1hSvYKPNJ+OlUA1O3r8o9LAawzPAlqeZiIdxTBBBJ0=
27 github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=29 github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
A internal/db/certificate_validations.sql.go +67 -0
@@ -0,0 +1,67 @@
1+// Code generated by sqlc. DO NOT EDIT.
2+// versions:
3+// sqlc v1.30.0
4+// source: certificate_validations.sql
5+
6+package db
7+
8+import (
9+ "context"
10+ "database/sql"
11+)
12+
13+const DeleteCertificateValidation = `-- name: DeleteCertificateValidation :exec
14+DELETE FROM certificate_validations
15+WHERE hostname = ? AND certificate_hash = ?
16+`
17+
18+func (q *Queries) DeleteCertificateValidation(ctx context.Context, hostname string, certificateHash string) error {
19+ _, err := q.db.ExecContext(ctx, DeleteCertificateValidation, hostname, certificateHash)
20+ return err
21+}
22+
23+const DeleteExpiredCertificateValidations = `-- name: DeleteExpiredCertificateValidations :exec
24+DELETE FROM certificate_validations
25+WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')
26+`
27+
28+func (q *Queries) DeleteExpiredCertificateValidations(ctx context.Context) error {
29+ _, err := q.db.ExecContext(ctx, DeleteExpiredCertificateValidations)
30+ return err
31+}
32+
33+const GetCertificateValidation = `-- name: GetCertificateValidation :one
34+SELECT id, hostname, certificate_hash, user_decision, created_at, expires_at FROM certificate_validations
35+WHERE hostname = ? AND certificate_hash = ?
36+AND (expires_at IS NULL OR expires_at > datetime('now'))
37+LIMIT 1
38+`
39+
40+func (q *Queries) GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error) {
41+ row := q.db.QueryRowContext(ctx, GetCertificateValidation, hostname, certificateHash)
42+ var i CertificateValidation
43+ err := row.Scan(
44+ &i.ID,
45+ &i.Hostname,
46+ &i.CertificateHash,
47+ &i.UserDecision,
48+ &i.CreatedAt,
49+ &i.ExpiresAt,
50+ )
51+ return i, err
52+}
53+
54+const StoreCertificateValidation = `-- name: StoreCertificateValidation :exec
55+INSERT OR REPLACE INTO certificate_validations (hostname, certificate_hash, user_decision, expires_at)
56+VALUES (?, ?, ?, ?)
57+`
58+
59+func (q *Queries) StoreCertificateValidation(ctx context.Context, hostname string, certificateHash string, userDecision string, expiresAt sql.NullTime) error {
60+ _, err := q.db.ExecContext(ctx, StoreCertificateValidation,
61+ hostname,
62+ certificateHash,
63+ userDecision,
64+ expiresAt,
65+ )
66+ return err
67+}
@@ -0,0 +1,67 @@
1+// Code generated by sqlc. DO NOT EDIT.
2+// versions:
3+// sqlc v1.30.0
4+// source: certificate_validations.sql
5+
6+package db
7+
8+import (
9+ "context"
10+ "database/sql"
11+)
12+
13+const DeleteCertificateValidation = `-- name: DeleteCertificateValidation :exec
14+DELETE FROM certificate_validations
15+WHERE hostname = ? AND certificate_hash = ?
16+`
17+
18+func (q *Queries) DeleteCertificateValidation(ctx context.Context, hostname string, certificateHash string) error {
19+ _, err := q.db.ExecContext(ctx, DeleteCertificateValidation, hostname, certificateHash)
20+ return err
21+}
22+
23+const DeleteExpiredCertificateValidations = `-- name: DeleteExpiredCertificateValidations :exec
24+DELETE FROM certificate_validations
25+WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')
26+`
27+
28+func (q *Queries) DeleteExpiredCertificateValidations(ctx context.Context) error {
29+ _, err := q.db.ExecContext(ctx, DeleteExpiredCertificateValidations)
30+ return err
31+}
32+
33+const GetCertificateValidation = `-- name: GetCertificateValidation :one
34+SELECT id, hostname, certificate_hash, user_decision, created_at, expires_at FROM certificate_validations
35+WHERE hostname = ? AND certificate_hash = ?
36+AND (expires_at IS NULL OR expires_at > datetime('now'))
37+LIMIT 1
38+`
39+
40+func (q *Queries) GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error) {
41+ row := q.db.QueryRowContext(ctx, GetCertificateValidation, hostname, certificateHash)
42+ var i CertificateValidation
43+ err := row.Scan(
44+ &i.ID,
45+ &i.Hostname,
46+ &i.CertificateHash,
47+ &i.UserDecision,
48+ &i.CreatedAt,
49+ &i.ExpiresAt,
50+ )
51+ return i, err
52+}
53+
54+const StoreCertificateValidation = `-- name: StoreCertificateValidation :exec
55+INSERT OR REPLACE INTO certificate_validations (hostname, certificate_hash, user_decision, expires_at)
56+VALUES (?, ?, ?, ?)
57+`
58+
59+func (q *Queries) StoreCertificateValidation(ctx context.Context, hostname string, certificateHash string, userDecision string, expiresAt sql.NullTime) error {
60+ _, err := q.db.ExecContext(ctx, StoreCertificateValidation,
61+ hostname,
62+ certificateHash,
63+ userDecision,
64+ expiresAt,
65+ )
66+ return err
67+}
M internal/db/models.go +9 -0
@@ -8,6 +8,15 @@ import (
88 "database/sql"
99 )
1010
11+type CertificateValidation struct {
12+ ID int64 `json:"id"`
13+ Hostname string `json:"hostname"`
14+ CertificateHash string `json:"certificate_hash"`
15+ UserDecision string `json:"user_decision"`
16+ CreatedAt sql.NullTime `json:"created_at"`
17+ ExpiresAt sql.NullTime `json:"expires_at"`
18+}
19+
1120 type History struct {
1221 ID int64 `json:"id"`
1322 Url string `json:"url"`
@@ -8,6 +8,15 @@ import (
8 "database/sql"8 "database/sql"
9 )9 )
10 10
11+type CertificateValidation struct {
12+ ID int64 `json:"id"`
13+ Hostname string `json:"hostname"`
14+ CertificateHash string `json:"certificate_hash"`
15+ UserDecision string `json:"user_decision"`
16+ CreatedAt sql.NullTime `json:"created_at"`
17+ ExpiresAt sql.NullTime `json:"expires_at"`
18+}
19+
11 type History struct {20 type History struct {
12 ID int64 `json:"id"`21 ID int64 `json:"id"`
13 Url string `json:"url"`22 Url string `json:"url"`
M internal/db/querier.go +4 -0
@@ -13,8 +13,11 @@ type Querier interface {
1313 AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error
1414 // Cleanup zoom level entries older than specified days
1515 CleanupOldZoomLevels(ctx context.Context, dollar_1 sql.NullString) error
16+ DeleteCertificateValidation(ctx context.Context, hostname string, certificateHash string) error
17+ DeleteExpiredCertificateValidations(ctx context.Context) error
1618 // Delete zoom level setting for a domain
1719 DeleteZoomLevel(ctx context.Context, domain string) error
20+ GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error)
1821 GetHistory(ctx context.Context, limit int64) ([]History, error)
1922 GetShortcuts(ctx context.Context) ([]Shortcut, error)
2023 // Get zoom level for a specific domain
@@ -26,6 +29,7 @@ type Querier interface {
2629 SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error)
2730 // Set or update zoom level for a domain with validation
2831 SetZoomLevel(ctx context.Context, domain string, zoomFactor float64) error
32+ StoreCertificateValidation(ctx context.Context, hostname string, certificateHash string, userDecision string, expiresAt sql.NullTime) error
2933 UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error
3034 }
3135
@@ -13,8 +13,11 @@ type Querier interface {
13 AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error13 AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error
14 // Cleanup zoom level entries older than specified days14 // Cleanup zoom level entries older than specified days
15 CleanupOldZoomLevels(ctx context.Context, dollar_1 sql.NullString) error15 CleanupOldZoomLevels(ctx context.Context, dollar_1 sql.NullString) error
16+ DeleteCertificateValidation(ctx context.Context, hostname string, certificateHash string) error
17+ DeleteExpiredCertificateValidations(ctx context.Context) error
16 // Delete zoom level setting for a domain18 // Delete zoom level setting for a domain
17 DeleteZoomLevel(ctx context.Context, domain string) error19 DeleteZoomLevel(ctx context.Context, domain string) error
20+ GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error)
18 GetHistory(ctx context.Context, limit int64) ([]History, error)21 GetHistory(ctx context.Context, limit int64) ([]History, error)
19 GetShortcuts(ctx context.Context) ([]Shortcut, error)22 GetShortcuts(ctx context.Context) ([]Shortcut, error)
20 // Get zoom level for a specific domain23 // Get zoom level for a specific domain
@@ -26,6 +29,7 @@ type Querier interface {
26 SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error)29 SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error)
27 // Set or update zoom level for a domain with validation30 // Set or update zoom level for a domain with validation
28 SetZoomLevel(ctx context.Context, domain string, zoomFactor float64) error31 SetZoomLevel(ctx context.Context, domain string, zoomFactor float64) error
32+ StoreCertificateValidation(ctx context.Context, hostname string, certificateHash string, userDecision string, expiresAt sql.NullTime) error
29 UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error33 UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error
30 }34 }
31 35
A internal/db/queries/certificate_validations.sql +17 -0
@@ -0,0 +1,17 @@
1+-- name: GetCertificateValidation :one
2+SELECT * FROM certificate_validations
3+WHERE hostname = ? AND certificate_hash = ?
4+AND (expires_at IS NULL OR expires_at > datetime('now'))
5+LIMIT 1;
6+
7+-- name: StoreCertificateValidation :exec
8+INSERT OR REPLACE INTO certificate_validations (hostname, certificate_hash, user_decision, expires_at)
9+VALUES (?, ?, ?, ?);
10+
11+-- name: DeleteExpiredCertificateValidations :exec
12+DELETE FROM certificate_validations
13+WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');
14+
15+-- name: DeleteCertificateValidation :exec
16+DELETE FROM certificate_validations
17+WHERE hostname = ? AND certificate_hash = ?;⏎̸
@@ -0,0 +1,17 @@
1+-- name: GetCertificateValidation :one
2+SELECT * FROM certificate_validations
3+WHERE hostname = ? AND certificate_hash = ?
4+AND (expires_at IS NULL OR expires_at > datetime('now'))
5+LIMIT 1;
6+
7+-- name: StoreCertificateValidation :exec
8+INSERT OR REPLACE INTO certificate_validations (hostname, certificate_hash, user_decision, expires_at)
9+VALUES (?, ?, ?, ?);
10+
11+-- name: DeleteExpiredCertificateValidations :exec
12+DELETE FROM certificate_validations
13+WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');
14+
15+-- name: DeleteCertificateValidation :exec
16+DELETE FROM certificate_validations
17+WHERE hostname = ? AND certificate_hash = ?;⏎̸
A internal/migrations/005_add_certificate_validations.sql +16 -0
@@ -0,0 +1,16 @@
1+-- Add certificate validation storage for permanent TLS decisions
2+-- Store user decisions for certificate errors to avoid repeated prompts
3+
4+CREATE TABLE IF NOT EXISTS certificate_validations (
5+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6+ hostname TEXT NOT NULL,
7+ certificate_hash TEXT NOT NULL,
8+ user_decision TEXT NOT NULL CHECK (user_decision IN ('accepted', 'rejected')),
9+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
10+ expires_at DATETIME, -- Optional expiration for temporary decisions
11+ UNIQUE(hostname, certificate_hash)
12+);
13+
14+CREATE INDEX idx_cert_validations_hostname ON certificate_validations(hostname);
15+CREATE INDEX idx_cert_validations_hash ON certificate_validations(certificate_hash);
16+CREATE INDEX idx_cert_validations_expires ON certificate_validations(expires_at);⏎̸
@@ -0,0 +1,16 @@
1+-- Add certificate validation storage for permanent TLS decisions
2+-- Store user decisions for certificate errors to avoid repeated prompts
3+
4+CREATE TABLE IF NOT EXISTS certificate_validations (
5+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6+ hostname TEXT NOT NULL,
7+ certificate_hash TEXT NOT NULL,
8+ user_decision TEXT NOT NULL CHECK (user_decision IN ('accepted', 'rejected')),
9+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
10+ expires_at DATETIME, -- Optional expiration for temporary decisions
11+ UNIQUE(hostname, certificate_hash)
12+);
13+
14+CREATE INDEX idx_cert_validations_hostname ON certificate_validations(hostname);
15+CREATE INDEX idx_cert_validations_hash ON certificate_validations(certificate_hash);
16+CREATE INDEX idx_cert_validations_expires ON certificate_validations(expires_at);⏎̸
D migrations/001_initial.sql +0 -29
@@ -1,29 +0,0 @@
1--- Initial database schema for dumber
2--- History tracking for visited URLs
3-
4-CREATE TABLE IF NOT EXISTS history (
5- id INTEGER PRIMARY KEY AUTOINCREMENT,
6- url TEXT NOT NULL UNIQUE,
7- title TEXT,
8- visit_count INTEGER DEFAULT 1,
9- last_visited DATETIME DEFAULT CURRENT_TIMESTAMP,
10- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
11-);
12-
13-CREATE INDEX idx_history_url ON history(url);
14-CREATE INDEX idx_history_last_visited ON history(last_visited);
15-
16--- URL shortcuts configuration
17-CREATE TABLE IF NOT EXISTS shortcuts (
18- id INTEGER PRIMARY KEY AUTOINCREMENT,
19- shortcut TEXT NOT NULL UNIQUE,
20- url_template TEXT NOT NULL,
21- description TEXT,
22- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
23-);
24-
25-INSERT OR IGNORE INTO shortcuts (shortcut, url_template, description) VALUES
26- ('g', 'https://www.google.com/search?q=%s', 'Google search'),
27- ('gh', 'https://github.com/search?q=%s', 'GitHub search'),
28- ('yt', 'https://www.youtube.com/results?search_query=%s', 'YouTube search'),
29- ('w', 'https://en.wikipedia.org/wiki/%s', 'Wikipedia search');⏎̸
@@ -1,29 +0,0 @@
1--- Initial database schema for dumber
2--- History tracking for visited URLs
3-
4-CREATE TABLE IF NOT EXISTS history (
5- id INTEGER PRIMARY KEY AUTOINCREMENT,
6- url TEXT NOT NULL UNIQUE,
7- title TEXT,
8- visit_count INTEGER DEFAULT 1,
9- last_visited DATETIME DEFAULT CURRENT_TIMESTAMP,
10- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
11-);
12-
13-CREATE INDEX idx_history_url ON history(url);
14-CREATE INDEX idx_history_last_visited ON history(last_visited);
15-
16--- URL shortcuts configuration
17-CREATE TABLE IF NOT EXISTS shortcuts (
18- id INTEGER PRIMARY KEY AUTOINCREMENT,
19- shortcut TEXT NOT NULL UNIQUE,
20- url_template TEXT NOT NULL,
21- description TEXT,
22- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
23-);
24-
25-INSERT OR IGNORE INTO shortcuts (shortcut, url_template, description) VALUES
26- ('g', 'https://www.google.com/search?q=%s', 'Google search'),
27- ('gh', 'https://github.com/search?q=%s', 'GitHub search'),
28- ('yt', 'https://www.youtube.com/results?search_query=%s', 'YouTube search'),
29- ('w', 'https://en.wikipedia.org/wiki/%s', 'Wikipedia search');⏎̸
D migrations/002_zoom_persistence.sql +0 -11
@@ -1,11 +0,0 @@
1--- Add zoom persistence table
2--- Store zoom levels per URL for restoration between sessions
3-
4-CREATE TABLE IF NOT EXISTS zoom_settings (
5- id INTEGER PRIMARY KEY AUTOINCREMENT,
6- url TEXT NOT NULL UNIQUE,
7- zoom_level REAL NOT NULL DEFAULT 1.0,
8- last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
9-);
10-
11-CREATE INDEX idx_zoom_settings_url ON zoom_settings(url);⏎̸
@@ -1,11 +0,0 @@
1--- Add zoom persistence table
2--- Store zoom levels per URL for restoration between sessions
3-
4-CREATE TABLE IF NOT EXISTS zoom_settings (
5- id INTEGER PRIMARY KEY AUTOINCREMENT,
6- url TEXT NOT NULL UNIQUE,
7- zoom_level REAL NOT NULL DEFAULT 1.0,
8- last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
9-);
10-
11-CREATE INDEX idx_zoom_settings_url ON zoom_settings(url);⏎̸
D migrations/003_add_zoom_levels.sql +0 -15
@@ -1,15 +0,0 @@
1--- Add zoom_levels table to replace zoom_settings with spec requirements
2--- Store zoom levels per domain (not full URL) with Firefox zoom constraints
3-
4--- Drop existing zoom_settings table
5-DROP TABLE IF EXISTS zoom_settings;
6-
7--- Create new zoom_levels table following spec requirements
8-CREATE TABLE zoom_levels (
9- domain TEXT PRIMARY KEY,
10- zoom_factor REAL NOT NULL DEFAULT 1.0 CHECK(zoom_factor >= 0.3 AND zoom_factor <= 5.0),
11- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
12-);
13-
14--- Create index for performance on updated_at for cleanup operations
15-CREATE INDEX idx_zoom_levels_updated_at ON zoom_levels(updated_at);⏎̸
@@ -1,15 +0,0 @@
1--- Add zoom_levels table to replace zoom_settings with spec requirements
2--- Store zoom levels per domain (not full URL) with Firefox zoom constraints
3-
4--- Drop existing zoom_settings table
5-DROP TABLE IF EXISTS zoom_settings;
6-
7--- Create new zoom_levels table following spec requirements
8-CREATE TABLE zoom_levels (
9- domain TEXT PRIMARY KEY,
10- zoom_factor REAL NOT NULL DEFAULT 1.0 CHECK(zoom_factor >= 0.3 AND zoom_factor <= 5.0),
11- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
12-);
13-
14--- Create index for performance on updated_at for cleanup operations
15-CREATE INDEX idx_zoom_levels_updated_at ON zoom_levels(updated_at);⏎̸
D migrations/004_add_favicon_url.sql +0 -5
@@ -1,5 +0,0 @@
1--- Add favicon_url column to history table
2--- Store favicon URLs for better UI display in dmenu
3-
4-ALTER TABLE history ADD COLUMN favicon_url TEXT DEFAULT NULL;
5-CREATE INDEX IF NOT EXISTS idx_history_favicon ON history(favicon_url);⏎̸
@@ -1,5 +0,0 @@
1--- Add favicon_url column to history table
2--- Store favicon URLs for better UI display in dmenu
3-
4-ALTER TABLE history ADD COLUMN favicon_url TEXT DEFAULT NULL;
5-CREATE INDEX IF NOT EXISTS idx_history_favicon ON history(favicon_url);⏎̸
M pkg/webkit/webview_cgo.go +190 -59
@@ -60,13 +60,14 @@ static WebKitWebView* on_create_new_window(WebKitWebView* web_view,
6060 }
6161 static void connect_tls_error_handler(WebKitWebView* wv);
6262
63-// Dialog callback structure for TLS warnings
63+// Dialog callback structure for modern AlertDialog async handling
6464 typedef struct {
65- gboolean user_accepted;
66- gboolean dialog_completed;
67-} TLSDialogData;
65+ int response;
66+ gboolean completed;
67+ GMainLoop *loop;
68+} AlertDialogResponse;
6869
69-static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data);
70+static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data);
7071 static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
7172 static char* extract_certificate_info(GTlsCertificate* certificate);
7273
@@ -534,72 +535,82 @@ static void connect_tls_error_handler(WebKitWebView* wv) {
534535 G_CALLBACK(on_load_failed_with_tls_errors), NULL);
535536 }
536537
537-// TLS dialog response callback
538-static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data) {
539- TLSDialogData *data = (TLSDialogData*)user_data;
540- data->user_accepted = (response_id == GTK_RESPONSE_ACCEPT);
541- data->dialog_completed = TRUE;
542- printf("[dumber] TLS dialog response: %s (response_id=%d)\n",
543- data->user_accepted ? "ACCEPTED" : "REJECTED", response_id);
538+// Modern AlertDialog async callback
539+static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data) {
540+ AlertDialogResponse *data = (AlertDialogResponse*)user_data;
541+ GError *error = NULL;
542+ data->response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
543+ if (error) {
544+ printf("[dumber] AlertDialog error: %s\n", error->message);
545+ g_error_free(error);
546+ data->response = 0;
547+ }
548+ data->completed = TRUE;
549+ const char *response_names[] = {"GO_BACK", "PROCEED_ONCE", "ALWAYS_ACCEPT"};
550+ const char *response_name = (data->response >= 0 && data->response <= 2) ?
551+ response_names[data->response] : "UNKNOWN";
552+ printf("[dumber] TLS dialog response: %s (button_index=%d)\n", response_name, data->response);
544553 fflush(stdout);
545- gtk_window_destroy(GTK_WINDOW(dialog));
554+ if (data->loop) {
555+ g_main_loop_quit(data->loop);
556+ }
546557 }
547558
548-// Show TLS warning dialog synchronously using GTK4 patterns
559+// Show TLS warning dialog synchronously using modern GtkAlertDialog API
549560 static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
550561 printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
551562 printf("[dumber] Error message: %s\n", error_msg);
552563 fflush(stdout);
553564
554- // Create the dialog
555- GtkWidget *dialog = gtk_message_dialog_new(
556- parent,
557- GTK_DIALOG_MODAL,
558- GTK_MESSAGE_WARNING,
559- GTK_BUTTONS_NONE,
560- "Certificate Error for %s", hostname
561- );
562-
563- // Set secondary text with error details
564- gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error_msg);
565-
566- // Add buttons
567- gtk_dialog_add_button(GTK_DIALOG(dialog), "Go Back", GTK_RESPONSE_CANCEL);
568- GtkWidget *proceed_btn = gtk_dialog_add_button(GTK_DIALOG(dialog), "Proceed Anyway (Unsafe)", GTK_RESPONSE_ACCEPT);
569-
570- // Style the proceed button as destructive (red)
571- gtk_widget_add_css_class(proceed_btn, "destructive-action");
572-
573- // Set default response to cancel (safer option)
574- gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
575-
576- // Set up dialog data
577- TLSDialogData dialog_data = { FALSE, FALSE };
578-
579- // Connect response signal
580- g_signal_connect(dialog, "response", G_CALLBACK(on_tls_dialog_response), &dialog_data);
581-
582- // Show the dialog
583- gtk_window_present(GTK_WINDOW(dialog));
584-
585- // Run a nested main loop until dialog is completed
586- while (!dialog_data.dialog_completed) {
587- g_main_context_iteration(NULL, TRUE);
588- }
589-
590- return dialog_data.user_accepted;
565+ // Create the modern AlertDialog
566+ GtkAlertDialog *dialog = gtk_alert_dialog_new("Certificate Error for %s", hostname);
567+
568+ // Set modal behavior and error details
569+ gtk_alert_dialog_set_modal(dialog, TRUE);
570+ gtk_alert_dialog_set_detail(dialog, error_msg);
571+
572+ // Configure buttons (Go Back = 0, Proceed Once = 1, Always Accept = 2)
573+ const char *buttons[] = {"Go Back", "Proceed Once (Unsafe)", "Always Accept This Site", NULL};
574+ gtk_alert_dialog_set_buttons(dialog, buttons);
575+ gtk_alert_dialog_set_default_button(dialog, 0);
576+ gtk_alert_dialog_set_cancel_button(dialog, 0);
577+
578+ // Set up response data with main loop for sync behavior
579+ AlertDialogResponse response_data = {0, FALSE, g_main_loop_new(NULL, FALSE)};
580+
581+ // Start async operation
582+ gtk_alert_dialog_choose(dialog, parent, NULL,
583+ on_alert_dialog_choose_done, &response_data);
584+
585+ // Run event loop until dialog completes
586+ g_main_loop_run(response_data.loop);
587+ g_main_loop_unref(response_data.loop);
588+
589+ // Clean up
590+ g_object_unref(dialog);
591+
592+ // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept
593+ return response_data.response;
591594 }
592595 */
593596 import "C"
594597
595598 import (
599+ "context"
600+ "crypto/sha256"
601+ "database/sql"
596602 "errors"
603+ "fmt"
597604 "log"
598605 "os"
599606 "path/filepath"
600607 "runtime"
601608 "time"
602609 "unsafe"
610+
611+ "github.com/bnema/dumber/internal/config"
612+ "github.com/bnema/dumber/internal/db"
613+ _ "github.com/ncruces/go-sqlite3"
603614 )
604615
605616 // Helper function to convert bool to int for C interop
@@ -610,6 +621,91 @@ func boolToInt(b bool) int {
610621 return 0
611622 }
612623
624+// getCertificateHash generates a SHA256 hash of the certificate info for storage
625+func getCertificateHash(certificateInfo string) string {
626+ hash := sha256.Sum256([]byte(certificateInfo))
627+ return fmt.Sprintf("%x", hash)
628+}
629+
630+// getDBConnection opens a database connection and returns queries
631+func getDBConnection() (*db.Queries, *sql.DB, error) {
632+ cfg := config.Get()
633+ if cfg == nil {
634+ return nil, nil, fmt.Errorf("config not available")
635+ }
636+
637+ dbPath := cfg.Database.Path
638+ if dbPath == "" {
639+ var err error
640+ dbPath, err = config.GetDatabaseFile()
641+ if err != nil {
642+ return nil, nil, fmt.Errorf("failed to get database path: %w", err)
643+ }
644+ }
645+
646+ database, err := sql.Open("sqlite3", dbPath)
647+ if err != nil {
648+ return nil, nil, fmt.Errorf("failed to open database: %w", err)
649+ }
650+
651+ queries := db.New(database)
652+ return queries, database, nil
653+}
654+
655+// checkStoredCertificateDecision checks if user has previously decided on this certificate
656+func checkStoredCertificateDecision(hostname, certificateInfo string) (string, bool) {
657+ queries, database, err := getDBConnection()
658+ if err != nil {
659+ log.Printf("[tls] Failed to connect to database: %v", err)
660+ return "", false
661+ }
662+ defer database.Close()
663+
664+ certHash := getCertificateHash(certificateInfo)
665+ ctx := context.Background()
666+
667+ validation, err := queries.GetCertificateValidation(ctx, hostname, certHash)
668+ if err != nil {
669+ if err == sql.ErrNoRows {
670+ return "", false // No previous decision
671+ }
672+ log.Printf("[tls] Failed to query certificate validation: %v", err)
673+ return "", false
674+ }
675+
676+ log.Printf("[tls] Found stored certificate decision for %s: %s", hostname, validation.UserDecision)
677+ return validation.UserDecision, true
678+}
679+
680+// storeCertificateDecision stores the user's decision about a certificate
681+func storeCertificateDecision(hostname, certificateInfo, decision string, permanent bool) error {
682+ queries, database, err := getDBConnection()
683+ if err != nil {
684+ return fmt.Errorf("failed to connect to database: %w", err)
685+ }
686+ defer database.Close()
687+
688+ certHash := getCertificateHash(certificateInfo)
689+ ctx := context.Background()
690+
691+ var expiresAt sql.NullTime
692+ if !permanent {
693+ // For temporary decisions, expire after 24 hours
694+ expiresAt = sql.NullTime{
695+ Time: time.Now().Add(24 * time.Hour),
696+ Valid: true,
697+ }
698+ }
699+
700+ err = queries.StoreCertificateValidation(ctx, hostname, certHash, decision, expiresAt)
701+ if err != nil {
702+ return fmt.Errorf("failed to store certificate validation: %w", err)
703+ }
704+
705+ log.Printf("[tls] Stored certificate decision for %s: %s (permanent: %t)", hostname, decision, permanent)
706+ return nil
707+}
708+
613709 type nativeView struct {
614710 win *C.GtkWidget
615711 view *C.GtkWidget
@@ -1344,13 +1440,27 @@ func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certIn
13441440 return C.FALSE // Don't proceed
13451441 }
13461442
1347-// showTLSWarningDialog displays a warning dialog for TLS certificate errors
1443+// showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
13481444 func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
13491445 if w == nil || w.native == nil || w.native.win == nil {
13501446 log.Printf("[tls] Cannot show dialog: WebView or window is nil")
13511447 return false
13521448 }
13531449
1450+ // Check if we have a stored decision for this certificate
1451+ if storedDecision, hasStored := checkStoredCertificateDecision(hostname, certificateInfo); hasStored {
1452+ switch storedDecision {
1453+ case "accepted":
1454+ log.Printf("[tls] Using stored ACCEPT decision for %s", hostname)
1455+ return true
1456+ case "rejected":
1457+ log.Printf("[tls] Using stored REJECT decision for %s", hostname)
1458+ return false
1459+ default:
1460+ log.Printf("[tls] Unknown stored decision '%s' for %s, showing dialog", storedDecision, hostname)
1461+ }
1462+ }
1463+
13541464 log.Printf("[tls] Showing certificate error dialog for %s", hostname)
13551465
13561466 // Format the error message with certificate information
@@ -1363,19 +1473,40 @@ func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, cer
13631473 cErrorMsg := C.CString(errorMsg)
13641474 defer C.free(unsafe.Pointer(cErrorMsg))
13651475
1366- // Show the dialog and get user response
1476+ // Show the dialog and get user response (0=Go Back, 1=Proceed Once, 2=Always Accept)
13671477 result := C.show_tls_warning_dialog_sync(
13681478 (*C.GtkWindow)(unsafe.Pointer(w.native.win)),
13691479 cHostname,
13701480 cErrorMsg,
13711481 )
13721482
1373- userAccepted := result != 0
1374- log.Printf("[tls] User %s certificate for %s",
1375- map[bool]string{true: "accepted", false: "rejected"}[userAccepted],
1376- hostname)
1377-
1378- return userAccepted
1483+ responseCode := int(result)
1484+
1485+ switch responseCode {
1486+ case 0: // Go Back
1487+ log.Printf("[tls] User chose to GO BACK for %s", hostname)
1488+ return false
1489+
1490+ case 1: // Proceed Once
1491+ log.Printf("[tls] User chose to PROCEED ONCE for %s", hostname)
1492+ // Store temporary decision (expires in 24 hours)
1493+ if err := storeCertificateDecision(hostname, certificateInfo, "accepted", false); err != nil {
1494+ log.Printf("[tls] Warning: Failed to store temporary certificate decision: %v", err)
1495+ }
1496+ return true
1497+
1498+ case 2: // Always Accept
1499+ log.Printf("[tls] User chose to ALWAYS ACCEPT for %s", hostname)
1500+ // Store permanent decision
1501+ if err := storeCertificateDecision(hostname, certificateInfo, "accepted", true); err != nil {
1502+ log.Printf("[tls] Warning: Failed to store permanent certificate decision: %v", err)
1503+ }
1504+ return true
1505+
1506+ default:
1507+ log.Printf("[tls] Unknown response code %d for %s, defaulting to reject", responseCode, hostname)
1508+ return false
1509+ }
13791510 }
13801511
13811512 // formatTLSErrorMessage creates a detailed error message based on the error flags
@@ -60,13 +60,14 @@ static WebKitWebView* on_create_new_window(WebKitWebView* web_view,
60 }60 }
61 static void connect_tls_error_handler(WebKitWebView* wv);61 static void connect_tls_error_handler(WebKitWebView* wv);
62 62
63-// Dialog callback structure for TLS warnings63+// Dialog callback structure for modern AlertDialog async handling
64 typedef struct {64 typedef struct {
65- gboolean user_accepted;65+ int response;
66- gboolean dialog_completed;66+ gboolean completed;
67-} TLSDialogData;67+ GMainLoop *loop;
68+} AlertDialogResponse;
68 69
69-static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data);70+static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data);
70 static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);71 static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg);
71 static char* extract_certificate_info(GTlsCertificate* certificate);72 static char* extract_certificate_info(GTlsCertificate* certificate);
72 73
@@ -534,72 +535,82 @@ static void connect_tls_error_handler(WebKitWebView* wv) {
534 G_CALLBACK(on_load_failed_with_tls_errors), NULL);535 G_CALLBACK(on_load_failed_with_tls_errors), NULL);
535 }536 }
536 537
537-// TLS dialog response callback538+// Modern AlertDialog async callback
538-static void on_tls_dialog_response(GtkDialog *dialog, gint response_id, gpointer user_data) {539+static void on_alert_dialog_choose_done(GObject *source, GAsyncResult *result, gpointer user_data) {
539- TLSDialogData *data = (TLSDialogData*)user_data;540+ AlertDialogResponse *data = (AlertDialogResponse*)user_data;
540- data->user_accepted = (response_id == GTK_RESPONSE_ACCEPT);541+ GError *error = NULL;
541- data->dialog_completed = TRUE;542+ data->response = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
542- printf("[dumber] TLS dialog response: %s (response_id=%d)\n",543+ if (error) {
543- data->user_accepted ? "ACCEPTED" : "REJECTED", response_id);544+ printf("[dumber] AlertDialog error: %s\n", error->message);
545+ g_error_free(error);
546+ data->response = 0;
547+ }
548+ data->completed = TRUE;
549+ const char *response_names[] = {"GO_BACK", "PROCEED_ONCE", "ALWAYS_ACCEPT"};
550+ const char *response_name = (data->response >= 0 && data->response <= 2) ?
551+ response_names[data->response] : "UNKNOWN";
552+ printf("[dumber] TLS dialog response: %s (button_index=%d)\n", response_name, data->response);
544 fflush(stdout);553 fflush(stdout);
545- gtk_window_destroy(GTK_WINDOW(dialog));554+ if (data->loop) {
555+ g_main_loop_quit(data->loop);
556+ }
546 }557 }
547 558
548-// Show TLS warning dialog synchronously using GTK4 patterns559+// Show TLS warning dialog synchronously using modern GtkAlertDialog API
549 static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {560 static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) {
550 printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);561 printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname);
551 printf("[dumber] Error message: %s\n", error_msg);562 printf("[dumber] Error message: %s\n", error_msg);
552 fflush(stdout);563 fflush(stdout);
553 564
554- // Create the dialog565+ // Create the modern AlertDialog
555- GtkWidget *dialog = gtk_message_dialog_new(566+ GtkAlertDialog *dialog = gtk_alert_dialog_new("Certificate Error for %s", hostname);
556- parent,567+
557- GTK_DIALOG_MODAL,568+ // Set modal behavior and error details
558- GTK_MESSAGE_WARNING,569+ gtk_alert_dialog_set_modal(dialog, TRUE);
559- GTK_BUTTONS_NONE,570+ gtk_alert_dialog_set_detail(dialog, error_msg);
560- "Certificate Error for %s", hostname571+
561- );572+ // Configure buttons (Go Back = 0, Proceed Once = 1, Always Accept = 2)
562-573+ const char *buttons[] = {"Go Back", "Proceed Once (Unsafe)", "Always Accept This Site", NULL};
563- // Set secondary text with error details574+ gtk_alert_dialog_set_buttons(dialog, buttons);
564- gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error_msg);575+ gtk_alert_dialog_set_default_button(dialog, 0);
565-576+ gtk_alert_dialog_set_cancel_button(dialog, 0);
566- // Add buttons577+
567- gtk_dialog_add_button(GTK_DIALOG(dialog), "Go Back", GTK_RESPONSE_CANCEL);578+ // Set up response data with main loop for sync behavior
568- GtkWidget *proceed_btn = gtk_dialog_add_button(GTK_DIALOG(dialog), "Proceed Anyway (Unsafe)", GTK_RESPONSE_ACCEPT);579+ AlertDialogResponse response_data = {0, FALSE, g_main_loop_new(NULL, FALSE)};
569-580+
570- // Style the proceed button as destructive (red)581+ // Start async operation
571- gtk_widget_add_css_class(proceed_btn, "destructive-action");582+ gtk_alert_dialog_choose(dialog, parent, NULL,
572-583+ on_alert_dialog_choose_done, &response_data);
573- // Set default response to cancel (safer option)584+
574- gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);585+ // Run event loop until dialog completes
575-586+ g_main_loop_run(response_data.loop);
576- // Set up dialog data587+ g_main_loop_unref(response_data.loop);
577- TLSDialogData dialog_data = { FALSE, FALSE };588+
578-589+ // Clean up
579- // Connect response signal590+ g_object_unref(dialog);
580- g_signal_connect(dialog, "response", G_CALLBACK(on_tls_dialog_response), &dialog_data);591+
581-592+ // Return response code: 0=Go Back, 1=Proceed Once, 2=Always Accept
582- // Show the dialog593+ return response_data.response;
583- gtk_window_present(GTK_WINDOW(dialog));
584-
585- // Run a nested main loop until dialog is completed
586- while (!dialog_data.dialog_completed) {
587- g_main_context_iteration(NULL, TRUE);
588- }
589-
590- return dialog_data.user_accepted;
591 }594 }
592 */595 */
593 import "C"596 import "C"
594 597
595 import (598 import (
599+ "context"
600+ "crypto/sha256"
601+ "database/sql"
596 "errors"602 "errors"
603+ "fmt"
597 "log"604 "log"
598 "os"605 "os"
599 "path/filepath"606 "path/filepath"
600 "runtime"607 "runtime"
601 "time"608 "time"
602 "unsafe"609 "unsafe"
610+
611+ "github.com/bnema/dumber/internal/config"
612+ "github.com/bnema/dumber/internal/db"
613+ _ "github.com/ncruces/go-sqlite3"
603 )614 )
604 615
605 // Helper function to convert bool to int for C interop616 // Helper function to convert bool to int for C interop
@@ -610,6 +621,91 @@ func boolToInt(b bool) int {
610 return 0621 return 0
611 }622 }
612 623
624+// getCertificateHash generates a SHA256 hash of the certificate info for storage
625+func getCertificateHash(certificateInfo string) string {
626+ hash := sha256.Sum256([]byte(certificateInfo))
627+ return fmt.Sprintf("%x", hash)
628+}
629+
630+// getDBConnection opens a database connection and returns queries
631+func getDBConnection() (*db.Queries, *sql.DB, error) {
632+ cfg := config.Get()
633+ if cfg == nil {
634+ return nil, nil, fmt.Errorf("config not available")
635+ }
636+
637+ dbPath := cfg.Database.Path
638+ if dbPath == "" {
639+ var err error
640+ dbPath, err = config.GetDatabaseFile()
641+ if err != nil {
642+ return nil, nil, fmt.Errorf("failed to get database path: %w", err)
643+ }
644+ }
645+
646+ database, err := sql.Open("sqlite3", dbPath)
647+ if err != nil {
648+ return nil, nil, fmt.Errorf("failed to open database: %w", err)
649+ }
650+
651+ queries := db.New(database)
652+ return queries, database, nil
653+}
654+
655+// checkStoredCertificateDecision checks if user has previously decided on this certificate
656+func checkStoredCertificateDecision(hostname, certificateInfo string) (string, bool) {
657+ queries, database, err := getDBConnection()
658+ if err != nil {
659+ log.Printf("[tls] Failed to connect to database: %v", err)
660+ return "", false
661+ }
662+ defer database.Close()
663+
664+ certHash := getCertificateHash(certificateInfo)
665+ ctx := context.Background()
666+
667+ validation, err := queries.GetCertificateValidation(ctx, hostname, certHash)
668+ if err != nil {
669+ if err == sql.ErrNoRows {
670+ return "", false // No previous decision
671+ }
672+ log.Printf("[tls] Failed to query certificate validation: %v", err)
673+ return "", false
674+ }
675+
676+ log.Printf("[tls] Found stored certificate decision for %s: %s", hostname, validation.UserDecision)
677+ return validation.UserDecision, true
678+}
679+
680+// storeCertificateDecision stores the user's decision about a certificate
681+func storeCertificateDecision(hostname, certificateInfo, decision string, permanent bool) error {
682+ queries, database, err := getDBConnection()
683+ if err != nil {
684+ return fmt.Errorf("failed to connect to database: %w", err)
685+ }
686+ defer database.Close()
687+
688+ certHash := getCertificateHash(certificateInfo)
689+ ctx := context.Background()
690+
691+ var expiresAt sql.NullTime
692+ if !permanent {
693+ // For temporary decisions, expire after 24 hours
694+ expiresAt = sql.NullTime{
695+ Time: time.Now().Add(24 * time.Hour),
696+ Valid: true,
697+ }
698+ }
699+
700+ err = queries.StoreCertificateValidation(ctx, hostname, certHash, decision, expiresAt)
701+ if err != nil {
702+ return fmt.Errorf("failed to store certificate validation: %w", err)
703+ }
704+
705+ log.Printf("[tls] Stored certificate decision for %s: %s (permanent: %t)", hostname, decision, permanent)
706+ return nil
707+}
708+
613 type nativeView struct {709 type nativeView struct {
614 win *C.GtkWidget710 win *C.GtkWidget
615 view *C.GtkWidget711 view *C.GtkWidget
@@ -1344,13 +1440,27 @@ func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certIn
1344 return C.FALSE // Don't proceed1440 return C.FALSE // Don't proceed
1345 }1441 }
1346 1442
1347-// showTLSWarningDialog displays a warning dialog for TLS certificate errors1443+// showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability
1348 func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {1444 func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool {
1349 if w == nil || w.native == nil || w.native.win == nil {1445 if w == nil || w.native == nil || w.native.win == nil {
1350 log.Printf("[tls] Cannot show dialog: WebView or window is nil")1446 log.Printf("[tls] Cannot show dialog: WebView or window is nil")
1351 return false1447 return false
1352 }1448 }
1353 1449
1450+ // Check if we have a stored decision for this certificate
1451+ if storedDecision, hasStored := checkStoredCertificateDecision(hostname, certificateInfo); hasStored {
1452+ switch storedDecision {
1453+ case "accepted":
1454+ log.Printf("[tls] Using stored ACCEPT decision for %s", hostname)
1455+ return true
1456+ case "rejected":
1457+ log.Printf("[tls] Using stored REJECT decision for %s", hostname)
1458+ return false
1459+ default:
1460+ log.Printf("[tls] Unknown stored decision '%s' for %s, showing dialog", storedDecision, hostname)
1461+ }
1462+ }
1463+
1354 log.Printf("[tls] Showing certificate error dialog for %s", hostname)1464 log.Printf("[tls] Showing certificate error dialog for %s", hostname)
1355 1465
1356 // Format the error message with certificate information1466 // Format the error message with certificate information
@@ -1363,19 +1473,40 @@ func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, cer
1363 cErrorMsg := C.CString(errorMsg)1473 cErrorMsg := C.CString(errorMsg)
1364 defer C.free(unsafe.Pointer(cErrorMsg))1474 defer C.free(unsafe.Pointer(cErrorMsg))
1365 1475
1366- // Show the dialog and get user response1476+ // Show the dialog and get user response (0=Go Back, 1=Proceed Once, 2=Always Accept)
1367 result := C.show_tls_warning_dialog_sync(1477 result := C.show_tls_warning_dialog_sync(
1368 (*C.GtkWindow)(unsafe.Pointer(w.native.win)),1478 (*C.GtkWindow)(unsafe.Pointer(w.native.win)),
1369 cHostname,1479 cHostname,
1370 cErrorMsg,1480 cErrorMsg,
1371 )1481 )
1372 1482
1373- userAccepted := result != 01483+ responseCode := int(result)
1374- log.Printf("[tls] User %s certificate for %s",1484+
1375- map[bool]string{true: "accepted", false: "rejected"}[userAccepted],1485+ switch responseCode {
1376- hostname)1486+ case 0: // Go Back
1377-1487+ log.Printf("[tls] User chose to GO BACK for %s", hostname)
1378- return userAccepted1488+ return false
1489+
1490+ case 1: // Proceed Once
1491+ log.Printf("[tls] User chose to PROCEED ONCE for %s", hostname)
1492+ // Store temporary decision (expires in 24 hours)
1493+ if err := storeCertificateDecision(hostname, certificateInfo, "accepted", false); err != nil {
1494+ log.Printf("[tls] Warning: Failed to store temporary certificate decision: %v", err)
1495+ }
1496+ return true
1497+
1498+ case 2: // Always Accept
1499+ log.Printf("[tls] User chose to ALWAYS ACCEPT for %s", hostname)
1500+ // Store permanent decision
1501+ if err := storeCertificateDecision(hostname, certificateInfo, "accepted", true); err != nil {
1502+ log.Printf("[tls] Warning: Failed to store permanent certificate decision: %v", err)
1503+ }
1504+ return true
1505+
1506+ default:
1507+ log.Printf("[tls] Unknown response code %d for %s, defaulting to reject", responseCode, hostname)
1508+ return false
1509+ }
1379 }1510 }
1380 1511
1381 // formatTLSErrorMessage creates a detailed error message based on the error flags1512 // formatTLSErrorMessage creates a detailed error message based on the error flags
M sqlc.yaml +1 -1
@@ -2,7 +2,7 @@ version: "2"
22 sql:
33 - engine: "sqlite"
44 queries: "./internal/db/queries/"
5- schema: "./migrations/"
5+ schema: "./internal/migrations/"
66 gen:
77 go:
88 package: "db"
@@ -2,7 +2,7 @@ version: "2"
2 sql:2 sql:
3 - engine: "sqlite"3 - engine: "sqlite"
4 queries: "./internal/db/queries/"4 queries: "./internal/db/queries/"
5- schema: "./migrations/"5+ schema: "./internal/migrations/"
6 gen:6 gen:
7 go:7 go:
8 package: "db"8 package: "db"

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