This commit is contained in:
parent
7fc0e1ca8f
commit
a74e8100d3
19
backend/config/resource_cleanup.go
Normal file
19
backend/config/resource_cleanup.go
Normal file
@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 无返回值的租约释放接口记录关闭失败,不将路径或驱动错误中的敏感信息写入日志。
|
||||
func releaseConfigLease(unlock func(), lease io.Closer) {
|
||||
unlock()
|
||||
closeConfigResource(lease)
|
||||
}
|
||||
|
||||
func closeConfigResource(resource io.Closer) {
|
||||
if err := resource.Close(); err != nil {
|
||||
log.WithField("action", "释放配置资源").Error("关闭配置资源失败,请检查文件句柄和数据库连接状态")
|
||||
}
|
||||
}
|
||||
54
backend/config/resource_cleanup_test.go
Normal file
54
backend/config/resource_cleanup_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
logtest "github.com/sirupsen/logrus/hooks/test"
|
||||
)
|
||||
|
||||
type cleanupTestResource struct {
|
||||
close func() error
|
||||
}
|
||||
|
||||
func (r cleanupTestResource) Close() error { return r.close() }
|
||||
|
||||
func TestReleaseConfigLeaseUnlocksBeforeClosing(t *testing.T) {
|
||||
var steps []string
|
||||
resource := cleanupTestResource{close: func() error {
|
||||
steps = append(steps, "close")
|
||||
return nil
|
||||
}}
|
||||
releaseConfigLease(func() { steps = append(steps, "unlock") }, resource)
|
||||
if strings.Join(steps, ",") != "unlock,close" {
|
||||
t.Fatalf("资源释放顺序不正确: %v", steps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseConfigResourceReportsFailureWithoutSensitiveDetails(t *testing.T) {
|
||||
previous := log.StandardLogger().ReplaceHooks(make(log.LevelHooks))
|
||||
t.Cleanup(func() { log.StandardLogger().ReplaceHooks(previous) })
|
||||
hook := logtest.NewGlobal()
|
||||
const sensitive = "sensitive-cleanup-value"
|
||||
closeCount := 0
|
||||
resource := cleanupTestResource{close: func() error {
|
||||
closeCount++
|
||||
return errors.New(sensitive)
|
||||
}}
|
||||
closeConfigResource(resource)
|
||||
entry := hook.LastEntry()
|
||||
if closeCount != 1 || entry == nil || entry.Level != log.ErrorLevel {
|
||||
t.Fatal("关闭失败没有记录为错误")
|
||||
}
|
||||
if strings.Contains(entry.Message+fmt.Sprint(entry.Data), sensitive) {
|
||||
t.Fatal("关闭错误日志泄露了资源内部信息")
|
||||
}
|
||||
hook.Reset()
|
||||
closeConfigResource(cleanupTestResource{close: func() error { return nil }})
|
||||
if hook.LastEntry() != nil {
|
||||
t.Fatal("成功关闭资源不应记录错误")
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -99,10 +100,9 @@ func stageFlatSSLAtRoot(root, mode string, privateKey, certificate, issuer []byt
|
||||
}
|
||||
unlock, err := lockStateFile(lease)
|
||||
if err != nil {
|
||||
lease.Close()
|
||||
return ManagedSSLReference{}, err
|
||||
return ManagedSSLReference{}, errors.Join(err, lease.Close())
|
||||
}
|
||||
managedSSLStorage.staged[publicPath] = func() { unlock(); lease.Close() }
|
||||
managedSSLStorage.staged[publicPath] = func() { releaseConfigLease(unlock, lease) }
|
||||
complete = true
|
||||
return ManagedSSLReference{Mode: mode, CertificateRef: "ssl/" + base + ".crt", PrivateKeyRef: "ssl/" + base + ".key", CertificatePath: publicPath, PrivateKeyPath: privatePath}, nil
|
||||
}
|
||||
@ -204,7 +204,9 @@ func pruneFlatSSLLocked(root string, protected []SSLConfig) error {
|
||||
}
|
||||
unlock, err := lockStateFile(lease)
|
||||
if err != nil {
|
||||
lease.Close()
|
||||
if closeErr := lease.Close(); closeErr != nil {
|
||||
return errors.Join(err, closeErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = storage.Remove(filepath.Base(privatePath))
|
||||
@ -212,8 +214,7 @@ func pruneFlatSSLLocked(root string, protected []SSLConfig) error {
|
||||
err = storage.Remove(filepath.Base(publicPath))
|
||||
}
|
||||
unlock()
|
||||
lease.Close()
|
||||
if err != nil {
|
||||
if err := errors.Join(err, lease.Close()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,9 +218,11 @@ func pruneManagedSSLLocked(root string, protected []SSLConfig) error {
|
||||
unlock, err := lockStateFile(lease)
|
||||
if err != nil {
|
||||
keep[path] = true
|
||||
lease.Close()
|
||||
if closeErr := lease.Close(); closeErr != nil {
|
||||
return errors.Join(err, closeErr)
|
||||
}
|
||||
} else {
|
||||
leases = append(leases, func() { unlock(); lease.Close() })
|
||||
leases = append(leases, func() { releaseConfigLease(unlock, lease) })
|
||||
}
|
||||
}
|
||||
versions = append(versions, retainedSSLVersion{directory: path, modified: info.ModTime()})
|
||||
|
||||
@ -164,16 +164,13 @@ func importManagedSSLAtRoot(root string, mode string, privateKeyPEM, certificate
|
||||
return atomicfile.WriteRoot(candidateRoot, name, data, permission)
|
||||
}
|
||||
if err := write("private.key", privateKeyPEM, 0600); err != nil {
|
||||
candidateRoot.Close()
|
||||
return ManagedSSLReference{}, err
|
||||
return ManagedSSLReference{}, errors.Join(err, candidateRoot.Close())
|
||||
}
|
||||
if err := write("public.crt", certificatePEM, 0600); err != nil {
|
||||
candidateRoot.Close()
|
||||
return ManagedSSLReference{}, err
|
||||
return ManagedSSLReference{}, errors.Join(err, candidateRoot.Close())
|
||||
}
|
||||
if err := write("issuer.crt", issuerPEM, 0600); err != nil {
|
||||
candidateRoot.Close()
|
||||
return ManagedSSLReference{}, err
|
||||
return ManagedSSLReference{}, errors.Join(err, candidateRoot.Close())
|
||||
}
|
||||
if err := candidateRoot.Close(); err != nil {
|
||||
return ManagedSSLReference{}, err
|
||||
@ -557,10 +554,9 @@ func stageManagedSSLAtRoot(root string, mode string, privateKeyPEM, certificateP
|
||||
}
|
||||
unlock, err := lockStateFile(lease)
|
||||
if err != nil {
|
||||
lease.Close()
|
||||
return ManagedSSLReference{}, err
|
||||
return ManagedSSLReference{}, errors.Join(err, lease.Close())
|
||||
}
|
||||
managedSSLStorage.staged[directory] = func() { unlock(); lease.Close() }
|
||||
managedSSLStorage.staged[directory] = func() { releaseConfigLease(unlock, lease) }
|
||||
return ManagedSSLReference{Mode: mode, CertificateRef: filepath.ToSlash(publicRef), PrivateKeyRef: filepath.ToSlash(privateRef), CertificatePath: publicPath, PrivateKeyPath: privatePath}, nil
|
||||
}
|
||||
|
||||
@ -656,8 +652,7 @@ func removeManagedSSLDirectory(directory string) error {
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if err := storage.Remove(entry.Name()); err != nil {
|
||||
storage.Close()
|
||||
return err
|
||||
return errors.Join(err, storage.Close())
|
||||
}
|
||||
}
|
||||
if err := storage.Close(); err != nil {
|
||||
|
||||
@ -163,19 +163,18 @@ func (s *TransactionStore) openState() (*stateDatabase, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := database.PingContext(ctx); err != nil {
|
||||
database.Close()
|
||||
closeConfigResource(database)
|
||||
return nil, ErrStateDatabaseUnavailable
|
||||
}
|
||||
if !s.legacyState {
|
||||
if _, err := database.Exec(configstate.Schema(driver)); err != nil {
|
||||
database.Close()
|
||||
closeConfigResource(database)
|
||||
return nil, ErrStateDatabaseUnavailable
|
||||
}
|
||||
}
|
||||
if filename != "" {
|
||||
if err := os.Chmod(filename, 0600); err != nil {
|
||||
database.Close()
|
||||
return nil, err
|
||||
return nil, errors.Join(err, database.Close())
|
||||
}
|
||||
}
|
||||
return database, nil
|
||||
|
||||
@ -37,10 +37,9 @@ func (s *TransactionStore) lockState() (func(), error) {
|
||||
}
|
||||
unlock, err := lockStateFile(file)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, errors.New("配置状态正在被其他进程使用,请稍后重试")
|
||||
return nil, errors.Join(errors.New("配置状态正在被其他进程使用,请稍后重试"), file.Close())
|
||||
}
|
||||
return func() { unlock(); file.Close() }, nil
|
||||
return func() { releaseConfigLease(unlock, file) }, nil
|
||||
}
|
||||
|
||||
func (s *TransactionStore) saveState(kind, id string, record stateRecord) error {
|
||||
|
||||
@ -2,6 +2,7 @@ package res_init
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -206,8 +207,7 @@ func Init(serverVersion string) error {
|
||||
return err
|
||||
}
|
||||
if err := startAdminResetControl(); err != nil {
|
||||
stopServices()
|
||||
return err
|
||||
return errors.Join(err, stopServices())
|
||||
}
|
||||
serviceErrors := make(chan error, 9)
|
||||
adminResetErrors := adminResetServeErrors
|
||||
@ -221,13 +221,11 @@ func Init(serverVersion string) error {
|
||||
hooks.Init(serverVersion)
|
||||
outboxRuntime, err = outbox.Start(db.Instance)
|
||||
if err != nil {
|
||||
stopServices()
|
||||
return fmt.Errorf("start persistent outbox: %w", err)
|
||||
return errors.Join(fmt.Errorf("启动持久化发件队列失败: %w", err), stopServices())
|
||||
}
|
||||
httpRun, httpsRun, err := http_server.PrepareWebServers()
|
||||
if err != nil {
|
||||
stopServices()
|
||||
return fmt.Errorf("prepare web services: %w", err)
|
||||
return errors.Join(fmt.Errorf("准备网页服务失败: %w", err), stopServices())
|
||||
}
|
||||
mailSecurity := config.CurrentMailSecurity()
|
||||
// SMTP 25 用于服务器间投递,始终保留并支持机会性 STARTTLS。
|
||||
@ -244,8 +242,7 @@ func Init(serverVersion string) error {
|
||||
|
||||
// 等待 HTTP 服务启动后再执行需要 HTTP 回调的 ACME 操作。
|
||||
if err := waitHTTPReady(serviceErrors); err != nil {
|
||||
stopServices()
|
||||
return err
|
||||
return errors.Join(err, stopServices())
|
||||
}
|
||||
|
||||
// 更新 SSL 配置。
|
||||
@ -269,8 +266,7 @@ func Init(serverVersion string) error {
|
||||
select {
|
||||
case err := <-serviceErrors:
|
||||
log.WithError(err).WithFields(log.Fields{"action": "运行邮箱服务", "result": "failure"}).Error("邮箱服务运行失败")
|
||||
stopServices()
|
||||
return err
|
||||
return errors.Join(err, stopServices())
|
||||
case <-signal.RestartChan:
|
||||
log.WithFields(log.Fields{"action": "重启服务", "result": "started"}).Info("已收到服务重启请求")
|
||||
if err := stopServices(); err != nil {
|
||||
|
||||
@ -288,8 +288,7 @@ func vacuumSQLite(ctx context.Context, source, target string) error {
|
||||
if err := database.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("ping sqlite source: %w", err)
|
||||
}
|
||||
query := "VACUUM INTO '" + strings.ReplaceAll(target, "'", "''") + "'"
|
||||
if _, err := database.ExecContext(ctx, query); err != nil {
|
||||
if _, err := database.ExecContext(ctx, "VACUUM INTO ?", target); err != nil {
|
||||
return fmt.Errorf("copy sqlite database: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -78,6 +78,33 @@ func TestSQLiteExecutorCopiesDatabaseAndSwitchesConfiguration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVacuumSQLiteBindsDestinationAsData(t *testing.T) {
|
||||
for _, name := range []string{"copy with spaces.db", "copy'quote.db", "copy'; DROP TABLE messages; --.db"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, root := newSQLiteMigrationFixture(t, nil)
|
||||
source := filepath.Join(root, "data", "smail.db")
|
||||
target := filepath.Join(root, "data", name)
|
||||
if err := vacuumSQLite(context.Background(), source, target); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []string{source, target} {
|
||||
database, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var body string
|
||||
queryErr := database.QueryRow("SELECT body FROM messages WHERE id = 1").Scan(&body)
|
||||
if err := errors.Join(queryErr, database.Close()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body != "测试邮件" {
|
||||
t.Fatal("源库或复制库的邮件内容被意外修改")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteExecutorSupportsNestedReferences(t *testing.T) {
|
||||
var switched config.DatabaseConfig
|
||||
service, root := newSQLiteMigrationFixture(t, func(_ context.Context, candidate config.DatabaseConfig) error {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user