From fa0cea93b89bd014037c14c7b5e0d12ffe0f1c97 Mon Sep 17 00:00:00 2001 From: smail Date: Mon, 21 Sep 2026 01:23:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPostgreSQL=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E8=BF=9E=E6=8E=A5=E9=80=89=E9=A1=B9=E4=B8=8E?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=BF=81=E7=A7=BB=E4=B8=8D=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/migrate.go | 14 +++++- backend/config/migrate_postgres_test.go | 51 ++++++++++++++++++++++ backend/services/setup/db.go | 3 +- backend/services/setup/db_database_test.go | 2 +- 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 backend/config/migrate_postgres_test.go diff --git a/backend/config/migrate.go b/backend/config/migrate.go index c518a32..ba5bbb8 100644 --- a/backend/config/migrate.go +++ b/backend/config/migrate.go @@ -683,9 +683,21 @@ func parseLegacyMySQLDSN(value string) (DatabaseConfig, string, bool) { func parseLegacyPostgreSQLDSN(value string) (DatabaseConfig, string, bool) { parsed, err := url.ParseRequestURI(value) - if err != nil || (parsed.Scheme != "postgres" && parsed.Scheme != "postgresql") || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.User == nil || parsed.Host == "" { + if err != nil || (parsed.Scheme != "postgres" && parsed.Scheme != "postgresql") || parsed.Fragment != "" || parsed.User == nil || parsed.Host == "" { return DatabaseConfig{}, "", false } + // 仅接受正式运行能够原样表达的选项;未知、重复和降级 TLS 的选项仍需人工迁移。 + query, err := url.ParseQuery(parsed.RawQuery) + if err != nil { + return DatabaseConfig{}, "", false + } + supported := map[string]string{"sslmode": "require", "timezone": "UTC", "search_path": "public"} + for key, values := range query { + expected, known := supported[key] + if !known || len(values) != 1 || values[0] != expected { + return DatabaseConfig{}, "", false + } + } username := parsed.User.Username() password, ok := parsed.User.Password() if !ok || !validLegacyDatabaseUser(username) || !validLegacyDatabasePassword(password) { diff --git a/backend/config/migrate_postgres_test.go b/backend/config/migrate_postgres_test.go new file mode 100644 index 0000000..85cdb94 --- /dev/null +++ b/backend/config/migrate_postgres_test.go @@ -0,0 +1,51 @@ +package config + +import "testing" + +func TestLegacyPostgresRuntimeOptions(t *testing.T) { + for _, test := range []struct { + query string + valid bool + }{ + {"", true}, + {"sslmode=require", true}, + {"sslmode=require&timezone=UTC&search_path=public", true}, + {"sslmode=disable", false}, + {"sslmode=prefer", false}, + {"sslmode=verify-full", false}, + {"sslmode=require&sslmode=disable", false}, + {"sslmode=require&sslmode=require", false}, + {"sslmode=require&options=-c%20search_path%3Dprivate", false}, + {"sslmode=require&search_path=private", false}, + {"sslmode=require&timezone=Asia%2FShanghai", false}, + {"sslmode=require&unknown=", false}, + {"sslmode=require;search_path=private", false}, + {"sslmode=%zz", false}, + } { + t.Run(test.query, func(t *testing.T) { + value := "postgres://smail:sample@127.0.0.1:5432/smail" + if test.query != "" { + value += "?" + test.query + } + database, password, valid := parseLegacyPostgreSQLDSN(value) + if valid != test.valid { + t.Fatalf("连接选项验证错误:%t", valid) + } + if valid && (database.Type != DatabaseTypePostgreSQL || database.Port != 5432 || password != "sample") { + t.Fatal("安全选项影响了数据库连接字段") + } + }) + } +} + +func TestLegacyPostgresSetupDSNMigration(t *testing.T) { + root := t.TempDir() + writeLegacyMigrationFixture(t, root, map[string]any{ + "dbType": DBTypePostgres, + "dbDSN": "postgres://smail:sample@127.0.0.1:5432/smail?sslmode=require", + }) + plan, err := PlanLegacyMigration(root, RuntimeValidationOptions{}) + if err != nil || len(plan.ManualFields) != 0 { + t.Fatalf("首次安装产生的安全连接串不能迁移:%v", err) + } +} diff --git a/backend/services/setup/db.go b/backend/services/setup/db.go index 2855810..fedf1d6 100644 --- a/backend/services/setup/db.go +++ b/backend/services/setup/db.go @@ -363,7 +363,8 @@ func buildPostgresDSN(settings DatabaseSettings, previousDSN string) (string, er query = previous.Query() } if query.Get("sslmode") == "" { - query.Set("sslmode", "disable") + // 首次安装与正式运行保持相同的 TLS 要求,避免完成安装时切换连接语义。 + query.Set("sslmode", "require") } parsed := &url.URL{ Scheme: "postgres", diff --git a/backend/services/setup/db_database_test.go b/backend/services/setup/db_database_test.go index c8a466b..766da1c 100644 --- a/backend/services/setup/db_database_test.go +++ b/backend/services/setup/db_database_test.go @@ -135,7 +135,7 @@ func TestBuildPostgresDSNUsesStructuredFields(t *testing.T) { if host != "2001:db8::2" || port != "5432" || strings.TrimPrefix(parsed.Path, "/") != "smail_1" { t.Fatalf("unexpected connection settings: host=%q port=%q name=%q", host, port, parsed.Path) } - if parsed.Query().Get("sslmode") != "disable" { + if parsed.Query().Get("sslmode") != "require" { t.Fatalf("unexpected sslmode: %q", parsed.Query().Get("sslmode")) } }