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. | |||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | 7 | ### 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 | 14 | - **Performance-optimized WebKit memory configuration system**: | |
| 9 | 15 | - **New `WebkitMemoryConfig`** configuration section with performance-focused defaults for faster page loading | |
| 10 | 16 | - **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 | 21 | - **Early crash handling initialization**: Moved crash handler setup to application entry point for better crash recovery | |
| 16 | 22 | ||
| 17 | 23 | ### 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 | 28 | - **Major main.go refactoring**: Massive code reduction from 1071 lines to 61 lines (~95% reduction) | |
| 19 | 29 | - **Extracted browser application logic** to dedicated `internal/app/browser` package for better separation of concerns | |
| 20 | 30 | - **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 | ### Added | 7 | ### 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 loading | 15 | - **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 navigation | 16 | - **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 recovery | 21 | - **Early crash handling initialization**: Moved crash handler setup to application entry point for better crash recovery | ||
| 16 | 22 | ||||
| 17 | ### Changed | 23 | ### 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 concerns | 29 | - **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 ( | |||
| 14 | 14 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | |
| 15 | 15 | github.com/google/go-cmp v0.7.0 // indirect | |
| 16 | 16 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
| 17 | + | github.com/mattn/go-sqlite3 v1.14.32 // indirect | |
| 17 | 18 | github.com/ncruces/julianday v1.0.0 // indirect | |
| 18 | 19 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect | |
| 19 | 20 | github.com/rogpeppe/go-internal v1.12.0 // indirect | |
| @@ -14,6 +14,7 @@ require ( | |||||
| 14 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | 14 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | ||
| 15 | github.com/google/go-cmp v0.7.0 // indirect | 15 | github.com/google/go-cmp v0.7.0 // indirect | ||
| 16 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | 16 | 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 // indirect | 18 | github.com/ncruces/julianday v1.0.0 // indirect | ||
| 18 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect | 19 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect | ||
| 19 | github.com/rogpeppe/go-internal v1.12.0 // indirect | 20 | github.com/rogpeppe/go-internal v1.12.0 // indirect | ||
M go.sum +2 -0 generated file: collapsed
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 ( | |||
| 8 | 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 | 20 | type History struct { | |
| 12 | 21 | ID int64 `json:"id"` | |
| 13 | 22 | 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 { | |||
| 13 | 13 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | |
| 14 | 14 | // Cleanup zoom level entries older than specified days | |
| 15 | 15 | 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 | 18 | // Delete zoom level setting for a domain | |
| 17 | 19 | DeleteZoomLevel(ctx context.Context, domain string) error | |
| 20 | + | GetCertificateValidation(ctx context.Context, hostname string, certificateHash string) (CertificateValidation, error) | |
| 18 | 21 | GetHistory(ctx context.Context, limit int64) ([]History, error) | |
| 19 | 22 | GetShortcuts(ctx context.Context) ([]Shortcut, error) | |
| 20 | 23 | // Get zoom level for a specific domain | |
| @@ -26,6 +29,7 @@ type Querier interface { | |||
| 26 | 29 | SearchHistory(ctx context.Context, column1 sql.NullString, column2 sql.NullString, limit int64) ([]History, error) | |
| 27 | 30 | // Set or update zoom level for a domain with validation | |
| 28 | 31 | 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 | 33 | UpdateHistoryFavicon(ctx context.Context, faviconUrl sql.NullString, url string) error | |
| 30 | 34 | } | |
| 31 | 35 | ||
| @@ -13,8 +13,11 @@ type Querier interface { | |||||
| 13 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | 13 | AddOrUpdateHistory(ctx context.Context, url string, title sql.NullString) error | ||
| 14 | // Cleanup zoom level entries older than specified days | 14 | // Cleanup zoom level entries older than specified days | ||
| 15 | CleanupOldZoomLevels(ctx context.Context, dollar_1 sql.NullString) error | 15 | 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 domain | 18 | // Delete zoom level setting for a domain | ||
| 17 | DeleteZoomLevel(ctx context.Context, domain string) error | 19 | 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 domain | 23 | // 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 validation | 30 | // Set or update zoom level for a domain with validation | ||
| 28 | SetZoomLevel(ctx context.Context, domain string, zoomFactor float64) error | 31 | 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) error | 33 | 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, | |||
| 60 | 60 | } | |
| 61 | 61 | static void connect_tls_error_handler(WebKitWebView* wv); | |
| 62 | 62 | ||
| 63 | - | // Dialog callback structure for TLS warnings | |
| 63 | + | // Dialog callback structure for modern AlertDialog async handling | |
| 64 | 64 | typedef struct { | |
| 65 | - | gboolean user_accepted; | |
| 66 | - | gboolean dialog_completed; | |
| 67 | - | } TLSDialogData; | |
| 65 | + | int response; | |
| 66 | + | gboolean completed; | |
| 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 | 71 | static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg); | |
| 71 | 72 | static char* extract_certificate_info(GTlsCertificate* certificate); | |
| 72 | 73 | ||
| @@ -534,72 +535,82 @@ static void connect_tls_error_handler(WebKitWebView* wv) { | |||
| 534 | 535 | G_CALLBACK(on_load_failed_with_tls_errors), NULL); | |
| 535 | 536 | } | |
| 536 | 537 | ||
| 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); | |
| 544 | 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 patterns | |
| 559 | + | // Show TLS warning dialog synchronously using modern GtkAlertDialog API | |
| 549 | 560 | static gboolean show_tls_warning_dialog_sync(GtkWindow *parent, const char* hostname, const char* error_msg) { | |
| 550 | 561 | printf("[dumber] Creating TLS warning dialog for hostname: %s\n", hostname); | |
| 551 | 562 | printf("[dumber] Error message: %s\n", error_msg); | |
| 552 | 563 | fflush(stdout); | |
| 553 | 564 | ||
| 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; | |
| 591 | 594 | } | |
| 592 | 595 | */ | |
| 593 | 596 | import "C" | |
| 594 | 597 | ||
| 595 | 598 | import ( | |
| 599 | + | "context" | |
| 600 | + | "crypto/sha256" | |
| 601 | + | "database/sql" | |
| 596 | 602 | "errors" | |
| 603 | + | "fmt" | |
| 597 | 604 | "log" | |
| 598 | 605 | "os" | |
| 599 | 606 | "path/filepath" | |
| 600 | 607 | "runtime" | |
| 601 | 608 | "time" | |
| 602 | 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 | 616 | // Helper function to convert bool to int for C interop | |
| @@ -610,6 +621,91 @@ func boolToInt(b bool) int { | |||
| 610 | 621 | 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 | 709 | type nativeView struct { | |
| 614 | 710 | win *C.GtkWidget | |
| 615 | 711 | view *C.GtkWidget | |
| @@ -1344,13 +1440,27 @@ func goHandleTLSError(failingURI *C.char, host *C.char, errorFlags C.int, certIn | |||
| 1344 | 1440 | return C.FALSE // Don't proceed | |
| 1345 | 1441 | } | |
| 1346 | 1442 | ||
| 1347 | - | // showTLSWarningDialog displays a warning dialog for TLS certificate errors | |
| 1443 | + | // showTLSWarningDialog displays a warning dialog for TLS certificate errors with storage capability | |
| 1348 | 1444 | func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, certificateInfo string) bool { | |
| 1349 | 1445 | if w == nil || w.native == nil || w.native.win == nil { | |
| 1350 | 1446 | log.Printf("[tls] Cannot show dialog: WebView or window is nil") | |
| 1351 | 1447 | 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 | 1464 | log.Printf("[tls] Showing certificate error dialog for %s", hostname) | |
| 1355 | 1465 | ||
| 1356 | 1466 | // Format the error message with certificate information | |
| @@ -1363,19 +1473,40 @@ func (w *WebView) showTLSWarningDialog(hostname, uri string, errorFlags int, cer | |||
| 1363 | 1473 | cErrorMsg := C.CString(errorMsg) | |
| 1364 | 1474 | defer C.free(unsafe.Pointer(cErrorMsg)) | |
| 1365 | 1475 | ||
| 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) | |
| 1367 | 1477 | result := C.show_tls_warning_dialog_sync( | |
| 1368 | 1478 | (*C.GtkWindow)(unsafe.Pointer(w.native.win)), | |
| 1369 | 1479 | cHostname, | |
| 1370 | 1480 | cErrorMsg, | |
| 1371 | 1481 | ) | |
| 1372 | 1482 | ||
| 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 | + | } | |
| 1379 | 1510 | } | |
| 1380 | 1511 | ||
| 1381 | 1512 | // 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 warnings | 63 | + | // 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 callback | 538 | + | // 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 patterns | 559 | + | // 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 dialog | 565 | + | // 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", hostname | 571 | + | |
| 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 details | 574 | + | 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 buttons | 577 | + | |
| 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 data | 587 | + | g_main_loop_unref(response_data.loop); |
| 577 | - | TLSDialogData dialog_data = { FALSE, FALSE }; | 588 | + | |
| 578 | - | 589 | + | // Clean up | |
| 579 | - | // Connect response signal | 590 | + | 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 dialog | 593 | + | 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 interop | 616 | // Helper function to convert bool to int for C interop | ||
| @@ -610,6 +621,91 @@ func boolToInt(b bool) int { | |||||
| 610 | return 0 | 621 | 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.GtkWidget | 710 | win *C.GtkWidget | ||
| 615 | view *C.GtkWidget | 711 | 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 proceed | 1440 | return C.FALSE // Don't proceed | ||
| 1345 | } | 1441 | } | ||
| 1346 | 1442 | ||||
| 1347 | - | // showTLSWarningDialog displays a warning dialog for TLS certificate errors | 1443 | + | // 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 false | 1447 | 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 information | 1466 | // 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 response | 1476 | + | // 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 != 0 | 1483 | + | 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 userAccepted | 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 | + | } | |||
| 1379 | } | 1510 | } | ||
| 1380 | 1511 | ||||
| 1381 | // formatTLSErrorMessage creates a detailed error message based on the error flags | 1512 | // formatTLSErrorMessage creates a detailed error message based on the error flags | ||
M sqlc.yaml +1 -1
| @@ -2,7 +2,7 @@ version: "2" | |||
| 2 | 2 | sql: | |
| 3 | 3 | - engine: "sqlite" | |
| 4 | 4 | queries: "./internal/db/queries/" | |
| 5 | - | schema: "./migrations/" | |
| 5 | + | schema: "./internal/migrations/" | |
| 6 | 6 | gen: | |
| 7 | 7 | go: | |
| 8 | 8 | 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" | ||