Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

266 lines
6.5 KiB
Go

package postgres
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"log"
"net"
"net/url"
"regexp"
"strings"
"time"
"github.com/jackc/pgx/v5/pgconn"
_ "github.com/jackc/pgx/v5/stdlib"
)
// RedactDSN returns the host portion of the DSN for diagnostics,
// stripping credentials, query parameters, and path components
// that may contain secrets.
func RedactDSN(dsn string) string {
u, err := url.Parse(dsn)
if err != nil {
return ""
}
return u.Hostname()
}
// CheckSSL returns an error when the PG connection string targets
// a non-loopback host without TLS encryption. It uses the pgx
// driver's own DSN parser to resolve the effective host and TLS
// configuration, avoiding bypasses from exotic DSN formats.
//
// A connection is rejected when any path in the TLS negotiation
// chain (primary + fallbacks) permits plaintext for a non-loopback
// host. This rejects sslmode=disable, allow, and prefer.
func CheckSSL(dsn string) error {
cfg, err := pgconn.ParseConfig(dsn)
if err != nil {
return fmt.Errorf("parsing pg connection string: %w", err)
}
if isLoopback(cfg.Host) {
return nil
}
if hasPlaintextPath(cfg) {
return fmt.Errorf(
"pg connection to %s permits plaintext; "+
"set sslmode=require (or verify-full) "+
"for non-local hosts, "+
"or set allow_insecure = true under [pg] or [pg.NAME] "+
"in config to override",
cfg.Host,
)
}
return nil
}
// WarnInsecureSSL logs a warning when the PG connection string
// targets a non-loopback host without TLS encryption. Uses the
// pgx driver's DSN parser for accurate host/TLS resolution.
func WarnInsecureSSL(dsn string) {
cfg, err := pgconn.ParseConfig(dsn)
if err != nil {
return
}
if isLoopback(cfg.Host) {
return
}
if hasPlaintextPath(cfg) {
log.Printf(
"warning: pg connection to %s permits "+
"plaintext; consider sslmode=require or "+
"verify-full for non-local hosts",
cfg.Host,
)
}
}
// hasPlaintextPath returns true if any path in the pgconn
// connection chain (primary config + fallbacks) has TLS disabled.
// This catches sslmode=disable (no TLS), sslmode=allow (plaintext
// first, TLS fallback), and sslmode=prefer (TLS first, plaintext
// fallback).
func hasPlaintextPath(cfg *pgconn.Config) bool {
if cfg.TLSConfig == nil {
return true
}
for _, fb := range cfg.Fallbacks {
if fb.TLSConfig == nil {
return true
}
}
return false
}
// isLoopback returns true if host is a loopback address,
// localhost, a unix socket path, or empty (defaults to local
// connection).
func isLoopback(host string) bool {
if host == "" || host == "localhost" {
return true
}
ip := net.ParseIP(host)
if ip != nil && ip.IsLoopback() {
return true
}
// Unix socket paths start with /
if len(host) > 0 && host[0] == '/' {
return true
}
return false
}
// validIdentifier matches simple SQL identifiers (letters,
// digits, underscores). Used to reject schema names that could
// enable SQL injection.
var validIdentifier = regexp.MustCompile(
`^[a-zA-Z_][a-zA-Z0-9_]*$`,
)
// quoteIdentifier double-quotes a SQL identifier, escaping any
// embedded double quotes. Rejects empty or non-identifier strings
// to prevent injection.
func quoteIdentifier(name string) (string, error) {
if name == "" {
return "", fmt.Errorf(
"schema name must not be empty",
)
}
if !validIdentifier.MatchString(name) {
return "", fmt.Errorf(
"invalid schema name: %q", name,
)
}
return `"` + name + `"`, nil
}
// Open opens a PG connection pool, validates SSL, and sets
// search_path to the given schema on every connection.
//
// The schema name is validated and quoted to prevent injection.
// When allowInsecure is true, non-loopback connections without
// TLS produce a warning instead of failing.
func Open(
dsn, schema string, allowInsecure bool,
) (*sql.DB, error) {
if dsn == "" {
return nil, fmt.Errorf("postgres URL is required")
}
quoted, err := quoteIdentifier(schema)
if err != nil {
return nil, fmt.Errorf("invalid pg schema: %w", err)
}
if allowInsecure {
WarnInsecureSSL(dsn)
} else if err := CheckSSL(dsn); err != nil {
return nil, err
}
// Append search_path and timezone as runtime parameters in
// the DSN so every connection in the pool inherits them.
// pgx's stdlib driver passes options through to ConnConfig.
connStr, err := appendConnParams(dsn, map[string]string{
"search_path": quoted,
"TimeZone": "UTC",
})
if err != nil {
return nil, fmt.Errorf(
"setting connection params: %w", err,
)
}
db, err := sql.Open("pgx", connStr)
if err != nil {
return nil, fmt.Errorf(
"opening pg (host=%s): %w",
RedactDSN(dsn), err,
)
}
db.SetMaxOpenConns(5)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
ctx, cancel := context.WithTimeout(
context.Background(), 10*time.Second,
)
defer cancel()
if err := db.PingContext(ctx); err != nil {
db.Close()
return nil, fmt.Errorf(
"pg ping (host=%s): %w",
RedactDSN(dsn), err,
)
}
return db, nil
}
func pgTargetFingerprint(dsn, schema string) (string, error) {
if dsn == "" {
return "", fmt.Errorf("postgres URL is required")
}
cfg, err := pgconn.ParseConfig(dsn)
if err != nil {
return "", fmt.Errorf("parsing pg connection string: %w", err)
}
fields := []string{
"v1",
cfg.Database,
cfg.User,
schema,
}
appendTargetEndpoint := func(host string, port uint16) {
if host != "" && !strings.HasPrefix(host, "/") {
host = strings.ToLower(host)
}
fields = append(fields, host, fmt.Sprintf("%d", port))
}
appendTargetEndpoint(cfg.Host, cfg.Port)
for _, fallback := range cfg.Fallbacks {
appendTargetEndpoint(fallback.Host, fallback.Port)
}
h := sha256.New()
for _, field := range fields {
fmt.Fprintf(h, "%d:%s", len(field), field)
}
return "v1:" + hex.EncodeToString(h.Sum(nil)), nil
}
// appendConnParams injects key=value connection parameters into
// the DSN. For URI-style DSNs it adds query parameters; for
// key=value DSNs it appends key=value pairs.
func appendConnParams(
dsn string, params map[string]string,
) (string, error) {
// URI format: postgres://...
if strings.HasPrefix(dsn, "postgres://") ||
strings.HasPrefix(dsn, "postgresql://") {
u, err := url.Parse(dsn)
if err != nil {
return "", fmt.Errorf(
"parsing pg URI: %w", err,
)
}
q := u.Query()
for k, v := range params {
q.Set(k, v)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
// Key=value format: append parameters.
result := dsn
for k, v := range params {
if result != "" {
result += " "
}
result += k + "=" + v
}
return result, nil
}