Files
曾志威 9da95beb42
continuous-integration/drone Build is failing
feat: refine management UI, connection diagnostics and local startup
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
2026-10-13 19:43:23 +08:00

154 lines
4.5 KiB
Go

package store
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"linkport/internal/security"
"os"
"path/filepath"
)
func OpenPortable(path string, keyReaders ...func() (string, error)) (*Store, error) {
var legacyKey func() (string, error)
if len(keyReaders) > 0 {
legacyKey = keyReaders[0]
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
if err == nil {
if err = file.Close(); err != nil {
return nil, err
}
} else if !errors.Is(err, os.ErrExist) {
return nil, err
}
database, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
database.SetMaxOpenConns(1)
defer database.Close()
if _, err = database.Exec("PRAGMA busy_timeout=5000"); err != nil {
return nil, err
}
if err = checkSupportedSchema(database); err != nil {
return nil, err
}
var metadataExists int
if err = database.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='local_metadata'").Scan(&metadataExists); err != nil {
return nil, err
}
var key string
if metadataExists != 0 {
err = database.QueryRow("SELECT value FROM local_metadata WHERE name='master-key'").Scan(&key)
if err != nil {
return nil, fmt.Errorf("read embedded key: %w", err)
}
} else {
var tables int
if err = database.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&tables); err != nil {
return nil, err
}
if tables != 0 {
if legacyKey == nil {
return nil, errors.New("legacy encrypted database requires its original key for one-time migration; no replacement key was generated")
}
key, err = legacyKey()
if err != nil {
return nil, err
}
} else {
raw := make([]byte, 32)
if _, err = rand.Read(raw); err != nil {
return nil, err
}
key = base64.StdEncoding.EncodeToString(raw)
}
vault, err := security.NewVault(key)
if err != nil {
return nil, err
}
if tables != 0 {
if err = validateStoredKey(database, vault); err != nil {
return nil, err
}
}
transaction, err := database.Begin()
if err != nil {
return nil, err
}
defer transaction.Rollback()
if _, err = transaction.Exec("CREATE TABLE IF NOT EXISTS local_metadata(name TEXT PRIMARY KEY,value BLOB NOT NULL)"); err != nil {
return nil, err
}
if _, err = transaction.Exec("INSERT INTO local_metadata(name,value) VALUES('master-key',?) ON CONFLICT(name) DO NOTHING", key); err != nil {
return nil, err
}
if err = transaction.Commit(); err != nil {
return nil, err
}
if err = database.QueryRow("SELECT value FROM local_metadata WHERE name='master-key'").Scan(&key); err != nil {
return nil, err
}
}
vault, err := security.NewVault(key)
if err != nil {
return nil, err
}
if err = database.Close(); err != nil {
return nil, err
}
return openStore(path, vault, true)
}
func validateStoredKey(database *sql.DB, vault *security.Vault) error {
for _, item := range [][2]string{
{"proxies", "SELECT 'proxy:'||id,config FROM proxies"},
{"gateways", "SELECT 'proxy:'||id,config FROM gateways"},
{"targets", "SELECT 'target:'||id,config FROM targets"},
{"outbounds", "SELECT 'outbound:'||id,config FROM outbounds"},
{"assets", "SELECT 'asset:'||id,config FROM assets"},
{"ssh_host_key", "SELECT 'shared-ssh-host-key',config FROM ssh_host_key"},
{"alert_settings", "SELECT 'alert-settings',config FROM alert_settings"},
{"webdav_sync_settings", "SELECT 'webdav-sync-settings',config FROM webdav_sync_settings"},
{"config_templates", "SELECT 'template:'||id,config FROM config_templates"},
{"config_history", "SELECT 'history:'||kind||':'||id||':'||revision,config FROM config_history"},
{"operational_settings", "SELECT 'workspace-scope-catalog',config FROM operational_settings WHERE id='scopes'"},
{"commands", "SELECT 'command:'||id,content FROM commands"},
} {
var exists int
if err := database.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", item[0]).Scan(&exists); err != nil {
return err
}
if exists == 0 {
continue
}
rows, err := database.Query(item[1])
if err != nil {
return err
}
for rows.Next() {
var id string
var blob []byte
if err = rows.Scan(&id, &blob); err == nil {
_, err = vault.Open(blob, id)
}
if err != nil {
rows.Close()
return fmt.Errorf("legacy key validation failed for %s: %w", item[0], err)
}
}
err = rows.Err()
rows.Close()
if err != nil {
return err
}
}
return nil
}