mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 09:29:52 +00:00
98 lines
3.7 KiB
Go
98 lines
3.7 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package backup
|
|
|
|
import (
|
|
"errors"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// ConnectionService captures a single-host PostgreSQL URL for pg_dump. The
|
|
// result contains secrets and must only be written to private staging, never
|
|
// logged or passed as a process argument. Unsupported settings fail closed.
|
|
func ConnectionService(dsn string) (string, error) {
|
|
return connectionService(dsn, os.Environ())
|
|
}
|
|
|
|
func connectionService(dsn string, environ []string) (string, error) {
|
|
invalid := errors.New("backup requires a single-host PostgreSQL URL with explicit user and database and supported libpq settings")
|
|
u, err := url.Parse(dsn)
|
|
if err != nil || (u.Scheme != "postgres" && u.Scheme != "postgresql") || u.User == nil || u.User.Username() == "" ||
|
|
u.Hostname() == "" || strings.Contains(u.Hostname(), ",") || strings.TrimLeft(u.Path, "/") == "" || u.Fragment != "" {
|
|
return "", invalid
|
|
}
|
|
settings := map[string]string{"host": u.Hostname(), "port": "5432", "user": u.User.Username(),
|
|
"dbname": strings.TrimLeft(u.Path, "/"), "sslmode": "prefer", "gssencmode": "disable"}
|
|
// Match the pgx startup environment for the supported native client settings.
|
|
// Target fields come exclusively from the URL. Service indirection and newer
|
|
// protocol options need separate compatibility work, not a guessed translation.
|
|
envKeys := map[string]string{"PGPASSWORD": "password", "PGPASSFILE": "passfile", "PGAPPNAME": "application_name",
|
|
"PGCONNECT_TIMEOUT": "connect_timeout", "PGSSLMODE": "sslmode", "PGSSLKEY": "sslkey", "PGSSLCERT": "sslcert",
|
|
"PGSSLROOTCERT": "sslrootcert", "PGSSLPASSWORD": "sslpassword", "PGSSLSNI": "sslsni",
|
|
"PGTARGETSESSIONATTRS": "target_session_attrs", "PGOPTIONS": "options"}
|
|
for _, entry := range environ {
|
|
key, value, _ := strings.Cut(entry, "=")
|
|
if value == "" {
|
|
continue
|
|
}
|
|
switch key {
|
|
case "PGSERVICE", "PGSERVICEFILE", "PGSSLNEGOTIATION", "PGMINPROTOCOLVERSION", "PGMAXPROTOCOLVERSION", "PGTZ":
|
|
return "", invalid
|
|
}
|
|
if name := envKeys[key]; name != "" {
|
|
settings[name] = value
|
|
}
|
|
if key == "PGPORT" && u.Port() == "" {
|
|
settings["port"] = value
|
|
}
|
|
}
|
|
if port := u.Port(); port != "" {
|
|
settings["port"] = port
|
|
}
|
|
if port, err := strconv.Atoi(settings["port"]); err != nil || port < 1 || port > 65535 {
|
|
return "", invalid
|
|
}
|
|
if password, ok := u.User.Password(); ok {
|
|
settings["password"] = password
|
|
}
|
|
query, err := url.ParseQuery(u.RawQuery)
|
|
if err != nil {
|
|
return "", invalid
|
|
}
|
|
for key, values := range query {
|
|
if len(values) != 1 || values[0] == "" {
|
|
return "", invalid
|
|
}
|
|
switch key {
|
|
case "pool_max_conns", "pool_min_conns", "pool_min_idle_conns", "pool_max_conn_lifetime", "pool_max_conn_idle_time", "pool_health_check_period", "pool_max_conn_lifetime_jitter":
|
|
continue // pgx-only pool controls; main has already validated them.
|
|
case "sslmode", "sslcert", "sslkey", "sslrootcert", "sslpassword", "sslsni", "passfile", "connect_timeout", "application_name", "target_session_attrs", "options":
|
|
settings[key] = values[0]
|
|
default:
|
|
return "", invalid
|
|
}
|
|
}
|
|
var result strings.Builder
|
|
result.WriteString("[beacon_backup]\n")
|
|
keys := make([]string, 0, len(settings))
|
|
for key, value := range settings {
|
|
// libpq service files have line-based, unquoted values. Reject anything
|
|
// their parser would trim or interpret as another entry.
|
|
if strings.TrimSpace(value) != value || strings.IndexFunc(value, unicode.IsControl) >= 0 || len(value) > 1000 {
|
|
return "", invalid
|
|
}
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
result.WriteString(key + "=" + settings[key] + "\n")
|
|
}
|
|
return result.String(), nil
|
|
}
|