connector.go (37770B)
1 package pq 2 3 import ( 4 "context" 5 "crypto/tls" 6 "database/sql/driver" 7 "fmt" 8 "math/rand" 9 "net" 10 "net/netip" 11 neturl "net/url" 12 "os" 13 "path/filepath" 14 "reflect" 15 "slices" 16 "sort" 17 "strconv" 18 "strings" 19 "time" 20 "unicode" 21 22 "github.com/lib/pq/internal/pgservice" 23 "github.com/lib/pq/internal/pqutil" 24 "github.com/lib/pq/internal/proto" 25 ) 26 27 type ( 28 // SSLMode is a sslmode setting. 29 SSLMode string 30 31 // SSLNegotiation is a sslnegotiation setting. 32 SSLNegotiation string 33 34 // TargetSessionAttrs is a target_session_attrs setting. 35 TargetSessionAttrs string 36 37 // LoadBalanceHosts is a load_balance_hosts setting. 38 LoadBalanceHosts string 39 40 // ProtocolVersion is a min_protocol_version or max_protocol_version 41 // setting. 42 ProtocolVersion string 43 44 // SSLProtocolVersion is a ssl_min_protocol_version or 45 // ssl_max_protocol_version setting. 46 SSLProtocolVersion string 47 ) 48 49 // Values for [SSLMode] that pq supports. 50 const ( 51 // No SSL 52 SSLModeDisable = SSLMode("disable") 53 54 // First try a non-SSL connection and if that fails try an SSL connection. 55 SSLModeAllow = SSLMode("allow") 56 57 // First try an SSL connection and if that fails try a non-SSL connection. 58 SSLModePrefer = SSLMode("prefer") 59 60 // Require SSL, but skip verification. This is the default. 61 SSLModeRequire = SSLMode("require") 62 63 // Require SSL and verify that the certificate was signed by a trusted CA. 64 SSLModeVerifyCA = SSLMode("verify-ca") 65 66 // Require SSL and verify that the certificate was signed by a trusted CA 67 // and the server host name matches the one in the certificate. 68 SSLModeVerifyFull = SSLMode("verify-full") 69 ) 70 71 var sslModes = []SSLMode{SSLModeDisable, SSLModeAllow, SSLModePrefer, SSLModeRequire, 72 SSLModeVerifyFull, SSLModeVerifyCA} 73 74 func (s SSLMode) useSSL() bool { 75 switch s { 76 case SSLModePrefer, SSLModeRequire, SSLModeVerifyCA, SSLModeVerifyFull: 77 return true 78 } 79 return false 80 } 81 82 // Values for [SSLNegotiation] that pq supports. 83 const ( 84 // Negotiate whether SSL should be used. This is the default. 85 SSLNegotiationPostgres = SSLNegotiation("postgres") 86 87 // Always use SSL, don't try to negotiate. 88 SSLNegotiationDirect = SSLNegotiation("direct") 89 ) 90 91 var sslNegotiations = []SSLNegotiation{SSLNegotiationPostgres, SSLNegotiationDirect} 92 93 // Values for [TargetSessionAttrs] that pq supports. 94 const ( 95 // Any successful connection is acceptable. This is the default. 96 TargetSessionAttrsAny = TargetSessionAttrs("any") 97 98 // Session must accept read-write transactions by default: the server must 99 // not be in hot standby mode and default_transaction_read_only must be 100 // off. 101 TargetSessionAttrsReadWrite = TargetSessionAttrs("read-write") 102 103 // Session must not accept read-write transactions by default. 104 TargetSessionAttrsReadOnly = TargetSessionAttrs("read-only") 105 106 // Server must not be in hot standby mode. 107 TargetSessionAttrsPrimary = TargetSessionAttrs("primary") 108 109 // Server must be in hot standby mode. 110 TargetSessionAttrsStandby = TargetSessionAttrs("standby") 111 112 // First try to find a standby server, but if none of the listed hosts is a 113 // standby server, try again in any mode. 114 TargetSessionAttrsPreferStandby = TargetSessionAttrs("prefer-standby") 115 ) 116 117 var targetSessionAttrs = []TargetSessionAttrs{TargetSessionAttrsAny, 118 TargetSessionAttrsReadWrite, TargetSessionAttrsReadOnly, TargetSessionAttrsPrimary, 119 TargetSessionAttrsStandby, TargetSessionAttrsPreferStandby} 120 121 // Values for [LoadBalanceHosts] that pq supports. 122 const ( 123 // Don't load balance; try hosts in the order in which they're provided. 124 // This is the default. 125 LoadBalanceHostsDisable = LoadBalanceHosts("disable") 126 127 // Hosts are tried in random order to balance connections across multiple 128 // PostgreSQL servers. 129 // 130 // When using this value it's recommended to also configure a reasonable 131 // value for connect_timeout. Because then, if one of the nodes that are 132 // used for load balancing is not responding, a new node will be tried. 133 LoadBalanceHostsRandom = LoadBalanceHosts("random") 134 ) 135 136 var loadBalanceHosts = []LoadBalanceHosts{LoadBalanceHostsDisable, LoadBalanceHostsRandom} 137 138 // Values for [ProtocolVersion] that pq supports. 139 const ( 140 // ProtocolVersion30 is the default protocol version, supported in 141 // PostgreSQL 3.0 and newer. 142 ProtocolVersion30 = ProtocolVersion("3.0") 143 144 // ProtocolVersion32 uses a longer secret key length for query cancellation, 145 // supported in PostgreSQL 18 and newer. 146 ProtocolVersion32 = ProtocolVersion("3.2") 147 148 // ProtocolVersionLatest is the latest protocol version that pq supports 149 // (which may not be supported by the server). 150 ProtocolVersionLatest = ProtocolVersion("latest") 151 ) 152 153 var protocolVersions = []ProtocolVersion{ProtocolVersion30, ProtocolVersion32, ProtocolVersionLatest} 154 155 // Values for [SSLProtocolVersion] that pq supports. 156 const ( 157 SSLProtocolVersionTLS10 = SSLProtocolVersion("TLSv1.0") 158 SSLProtocolVersionTLS11 = SSLProtocolVersion("TLSv1.1") 159 SSLProtocolVersionTLS12 = SSLProtocolVersion("TLSv1.2") 160 SSLProtocolVersionTLS13 = SSLProtocolVersion("TLSv1.3") 161 ) 162 163 var sslProtocolVersions = []SSLProtocolVersion{SSLProtocolVersionTLS10, SSLProtocolVersionTLS11, 164 SSLProtocolVersionTLS12, SSLProtocolVersionTLS13} 165 166 func (s SSLProtocolVersion) tlsconf() uint16 { 167 switch s { 168 case SSLProtocolVersionTLS10: 169 return tls.VersionTLS10 170 case SSLProtocolVersionTLS11: 171 return tls.VersionTLS11 172 case SSLProtocolVersionTLS12: 173 return tls.VersionTLS12 174 case SSLProtocolVersionTLS13: 175 return tls.VersionTLS13 176 default: 177 return 0 178 } 179 } 180 181 // Connector represents a fixed configuration for the pq driver with a given 182 // dsn. Connector satisfies the [database/sql/driver.Connector] interface and 183 // can be used to create any number of DB Conn's via [sql.OpenDB]. 184 type Connector struct { 185 cfg Config 186 dialer Dialer 187 } 188 189 // NewConnector returns a connector for the pq driver in a fixed configuration 190 // with the given dsn. The returned connector can be used to create any number 191 // of equivalent Conn's. The returned connector is intended to be used with 192 // [sql.OpenDB]. 193 func NewConnector(dsn string) (*Connector, error) { 194 cfg, err := NewConfig(dsn) 195 if err != nil { 196 return nil, err 197 } 198 return NewConnectorConfig(cfg) 199 } 200 201 // NewConnectorConfig returns a connector for the pq driver in a fixed 202 // configuration with the given [Config]. The returned connector can be used to 203 // create any number of equivalent Conn's. The returned connector is intended to 204 // be used with [sql.OpenDB]. 205 func NewConnectorConfig(cfg Config) (*Connector, error) { 206 return &Connector{cfg: cfg, dialer: defaultDialer{}}, nil 207 } 208 209 // Connect returns a connection to the database using the fixed configuration of 210 // this Connector. Context is not used. 211 func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) { return c.open(ctx) } 212 213 // Dialer allows change the dialer used to open connections. 214 func (c *Connector) Dialer(dialer Dialer) { c.dialer = dialer } 215 216 // Driver returns the underlying driver of this Connector. 217 func (c *Connector) Driver() driver.Driver { return &Driver{} } 218 219 func (p ProtocolVersion) proto() int { 220 switch p { 221 default: 222 return proto.ProtocolVersion30 223 case ProtocolVersion32, ProtocolVersionLatest: 224 return proto.ProtocolVersion32 225 } 226 } 227 228 // Config holds options pq supports when connecting to PostgreSQL. 229 // 230 // The postgres struct tag is used for the value from the DSN (e.g. 231 // "dbname=abc"), and the env struct tag is used for the environment variable 232 // (e.g. "PGDATABASE=abc") 233 type Config struct { 234 // The host to connect to. Absolute paths and values that start with @ are 235 // for unix domain sockets. Defaults to localhost. 236 // 237 // A comma-separated list of host names is also accepted, in which case each 238 // host name in the list is tried in order or randomly if load_balance_hosts 239 // is set. An empty item selects the default of localhost. The 240 // target_session_attrs option controls properties the host must have to be 241 // considered acceptable. 242 Host string `postgres:"host" env:"PGHOST"` 243 244 // IPv4 or IPv6 address to connect to. Using hostaddr allows the application 245 // to avoid a host name lookup, which might be important in applications 246 // with time constraints. A hostname is required for sslmode=verify-full and 247 // the GSSAPI or SSPI authentication methods. 248 // 249 // The following rules are used: 250 // 251 // - If host is given without hostaddr, a host name lookup occurs. 252 // 253 // - If hostaddr is given without host, the value for hostaddr gives the 254 // server network address. The connection attempt will fail if the 255 // authentication method requires a host name. 256 // 257 // - If both host and hostaddr are given, the value for hostaddr gives the 258 // server network address. The value for host is ignored unless the 259 // authentication method requires it, in which case it will be used as the 260 // host name. 261 // 262 // A comma-separated list of hostaddr values is also accepted, in which case 263 // each host in the list is tried in order or randonly if load_balance_hosts 264 // is set. An empty item causes the corresponding host name to be used, or 265 // the default host name if that is empty as well. The target_session_attrs 266 // option controls properties the host must have to be considered 267 // acceptable. 268 Hostaddr netip.Addr `postgres:"hostaddr" env:"PGHOSTADDR"` 269 270 // The port to connect to. Defaults to 5432. 271 // 272 // If multiple hosts were given in the host or hostaddr parameters, this 273 // parameter may specify a comma-separated list of ports of the same length 274 // as the host list, or it may specify a single port number to be used for 275 // all hosts. An empty string, or an empty item in a comma-separated list, 276 // specifies the default of 5432. 277 Port uint16 `postgres:"port" env:"PGPORT"` 278 279 // The name of the database to connect to. 280 Database string `postgres:"dbname" env:"PGDATABASE"` 281 282 // The user to sign in as. Defaults to the current user. 283 User string `postgres:"user" env:"PGUSER"` 284 285 // The user's password. 286 Password string `postgres:"password" env:"PGPASSWORD"` 287 288 // Path to [pgpass] file to store passwords; overrides Password. 289 // 290 // [pgpass]: http://www.postgresql.org/docs/current/static/libpq-pgpass.html 291 Passfile string `postgres:"passfile" env:"PGPASSFILE"` 292 293 // Commandline options to send to the server at connection start. 294 Options string `postgres:"options" env:"PGOPTIONS"` 295 296 // Application name, displayed in pg_stat_activity and log entries. 297 ApplicationName string `postgres:"application_name" env:"PGAPPNAME"` 298 299 // Used if application_name is not given. Specifying a fallback name is 300 // useful in generic utility programs that wish to set a default application 301 // name but allow it to be overridden by the user. 302 FallbackApplicationName string `postgres:"fallback_application_name" env:"-"` 303 304 // Whether to use SSL. Defaults to "require" (different from libpq's default 305 // of "prefer"). 306 // 307 // [RegisterTLSConfig] can be used to registers a custom [tls.Config], which 308 // can be used by setting sslmode=pqgo-«key» in the connection string. 309 SSLMode SSLMode `postgres:"sslmode" env:"PGSSLMODE"` 310 311 // When set to "direct" it will use SSL without negotiation (PostgreSQL ≥17 only). 312 SSLNegotiation SSLNegotiation `postgres:"sslnegotiation" env:"PGSSLNEGOTIATION"` 313 314 // Path to client SSL certificate. The file must contain PEM encoded data. 315 // 316 // Defaults to ~/.postgresql/postgresql.crt 317 SSLCert string `postgres:"sslcert" env:"PGSSLCERT"` 318 319 // Path to secret key for sslcert. The file must contain PEM encoded data. 320 // 321 // Defaults to ~/.postgresql/postgresql.key 322 SSLKey string `postgres:"sslkey" env:"PGSSLKEY"` 323 324 // Path to root certificate. The file must contain PEM encoded data. 325 // 326 // The special value "system" can be used to load the system's root 327 // certificates ([x509.SystemCertPool]). This will change the default 328 // sslmode to verify-full and issue an error if a lower setting is used – as 329 // anyone can register a valid certificate hostname verification becomes 330 // essential. 331 // 332 // Defaults to ~/.postgresql/root.crt. 333 SSLRootCert string `postgres:"sslrootcert" env:"PGSSLROOTCERT"` 334 335 // By default SNI is on, any value which is not starting with "1" disables 336 // SNI. 337 SSLSNI bool `postgres:"sslsni" env:"PGSSLSNI"` 338 339 // Minimum SSL/TLS protocol version to allow for the connection. 340 // 341 // The default is determined by [tls.Config.MinVersion], which is TLSv1.2 at 342 // the time of writing. 343 SSLMinProtocolVersion SSLProtocolVersion `postgres:"ssl_min_protocol_version" env:"SSLPGMINPROTOCOLVERSION"` 344 345 // Maximum SSL/TLS protocol version to allow for the connection. If not set, 346 // this parameter is ignored and the connection will use the maximum bound 347 // defined by the backend, if set. Setting the maximum protocol version is 348 // mainly useful for testing or if some component has issues working with a 349 // newer protocol. 350 SSLMaxProtocolVersion SSLProtocolVersion `postgres:"ssl_max_protocol_version" env:"SSLPGMAXPROTOCOLVERSION"` 351 352 // Interpert sslcert and sslkey as PEM encoded data, rather than a path to a 353 // PEM file. This is a pq extension, not supported in libpq. 354 SSLInline bool `postgres:"sslinline" env:"-"` 355 356 // GSS (Kerberos) service name when constructing the SPN (default is 357 // postgres). This will be combined with the host to form the full SPN: 358 // krbsrvname/host. 359 KrbSrvname string `postgres:"krbsrvname" env:"PGKRBSRVNAME"` 360 361 // GSS (Kerberos) SPN. This takes priority over krbsrvname if present. This 362 // is a pq extension, not supported in libpq. 363 KrbSpn string `postgres:"krbspn" env:"-"` 364 365 // Maximum time to wait while connecting, in seconds. Zero, negative, or not 366 // specified means wait indefinitely 367 ConnectTimeout time.Duration `postgres:"connect_timeout" env:"PGCONNECT_TIMEOUT"` 368 369 // Whether to always send []byte parameters over as binary. Enables single 370 // round-trip mode for non-prepared Query calls. This is a pq extension, not 371 // supported in libpq. 372 BinaryParameters bool `postgres:"binary_parameters" env:"-"` 373 374 // This connection should never use the binary format when receiving query 375 // results from prepared statements. Only provided for debugging. This is a 376 // pq extension, not supported in libpq. 377 DisablePreparedBinaryResult bool `postgres:"disable_prepared_binary_result" env:"-"` 378 379 // Client encoding; pq only supports UTF8 and this must be blank or "UTF8". 380 ClientEncoding string `postgres:"client_encoding" env:"PGCLIENTENCODING"` 381 382 // Date/time representation to use; pq only supports "ISO, MDY" and this 383 // must be blank or "ISO, MDY". 384 Datestyle string `postgres:"datestyle" env:"PGDATESTYLE"` 385 386 // Default time zone. 387 TZ string `postgres:"tz" env:"PGTZ"` 388 389 // Default mode for the genetic query optimizer. 390 Geqo string `postgres:"geqo" env:"PGGEQO"` 391 392 // Determine whether the session must have certain properties to be 393 // acceptable. It's typically used in combination with multiple host names 394 // to select the first acceptable alternative among several hosts. 395 TargetSessionAttrs TargetSessionAttrs `postgres:"target_session_attrs" env:"PGTARGETSESSIONATTRS"` 396 397 // Controls the order in which the client tries to connect to the available 398 // hosts. Once a connection attempt is successful no other hosts will be 399 // tried. This parameter is typically used in combination with multiple host 400 // names. 401 // 402 // This parameter can be used in combination with target_session_attrs to, 403 // for example, load balance over standby servers only. Once successfully 404 // connected, subsequent queries on the returned connection will all be sent 405 // to the same server. 406 LoadBalanceHosts LoadBalanceHosts `postgres:"load_balance_hosts" env:"PGLOADBALANCEHOSTS"` 407 408 // Minimum acceptable PostgreSQL protocol version. If the server does not 409 // support at least this version, the connection will fail. Defaults to 410 // "3.0". 411 MinProtocolVersion ProtocolVersion `postgres:"min_protocol_version" env:"PGMINPROTOCOLVERSION"` 412 413 // Maximum PostgreSQL protocol version to request from the server. Defaults to "3.0". 414 MaxProtocolVersion ProtocolVersion `postgres:"max_protocol_version" env:"PGMAXPROTOCOLVERSION"` 415 416 // Load connection parameters from the service file at ~/.pg_service.conf 417 // (which can be configured with PGSERVICEFILE). 418 // 419 // The service file is a INI-like file to configure connection parameters: 420 // 421 // [servicename] 422 // # Comment 423 // dbname=foo 424 // 425 // Unlike libpq, this does not look at the system-wide service file, as the 426 // location of this is a compile-time value that is not easy for pq to 427 // retrieve. 428 Service string `postgres:"service" env:"PGSERVICE"` 429 430 // Path to connection service file. Defaults to ~/.pg_service.conf. 431 ServiceFile string `postgres:"-" env:"PGSERVICEFILE"` 432 433 // Runtime parameters: any unrecognized parameter in the DSN will be added 434 // to this and sent to PostgreSQL during startup. 435 Runtime map[string]string `postgres:"-" env:"-"` 436 437 // Multi contains additional connection details. The first value is 438 // available in [Config.Host], [Config.Hostaddr], and [Config.Port], and 439 // additional ones (if any) are available here. 440 Multi []ConfigMultihost 441 442 // Record which parameters were given, so we can distinguish between an 443 // empty string "not given at all". 444 // 445 // The alternative is to use pointers or sql.Null[..], but that's more 446 // awkward to use. 447 set []string `env:"set"` 448 449 multiHost []string 450 multiHostaddr []netip.Addr 451 multiPort []uint16 452 } 453 454 // ConfigMultihost specifies an additional server to try to connect to. 455 type ConfigMultihost struct { 456 Host string 457 Hostaddr netip.Addr 458 Port uint16 459 } 460 461 // NewConfig creates a new [Config] from the defaults, environment, service 462 // file, and DSN, in that order. That is: a service overrides any value from the 463 // environment, which in turn gets overridden by the same parameter in the 464 // connection string. 465 // 466 // Most connection parameters supported by PostgreSQL are supported; see the 467 // [Config] struct for supported parameters. pq also lets you specify any 468 // [run-time parameter] such as search_path or work_mem in the connection 469 // string. This is different from libpq, which uses the "options" parameter for 470 // this (which also works in pq). 471 // 472 // # key=value connection strings 473 // 474 // For key=value strings, use single quotes for values that contain whitespace 475 // or empty values. A backslash will escape the next character: 476 // 477 // "user=pqgo password='with spaces'" 478 // "user=''" 479 // "user=space\ man password='it\'s valid'" 480 // 481 // # URL connection strings 482 // 483 // pq supports URL-style postgres:// or postgresql:// connection strings in the 484 // form: 485 // 486 // postgres[ql]://[user[:pwd]@][net-location][:port][/dbname][?param1=value1&...] 487 // 488 // Go's [net/url.Parse] is more strict than PostgreSQL's URL parser and will 489 // (correctly) reject %2F in the host part. This means that unix-socket URLs: 490 // 491 // postgres://[user[:pwd]@][unix-socket][:port[/dbname]][?param1=value1&...] 492 // postgres://%2Ftmp%2Fpostgres/db 493 // 494 // will not work. You will need to use "host=/tmp/postgres dbname=db". 495 // 496 // Similarly, multiple ports also won't work, but ?port= will: 497 // 498 // postgres://host1,host2:5432,6543/dbname Doesn't work 499 // postgres://host1,host2/dbname?port=5432,6543 Works 500 // 501 // # Environment 502 // 503 // Most [PostgreSQL environment variables] are supported by pq. Environment 504 // variables have a lower precedence than explicitly provided connection 505 // parameters. pq will return an error if environment variables it does not 506 // support are set. Environment variables have a lower precedence than 507 // explicitly provided connection parameters. 508 // 509 // [PostgreSQL environment variables]: http://www.postgresql.org/docs/current/static/libpq-envars.html 510 // [run-time parameter]: http://www.postgresql.org/docs/current/static/runtime-config.html 511 func NewConfig(dsn string) (Config, error) { 512 return newConfig(dsn, os.Environ()) 513 } 514 515 // Clone returns a copy of the [Config]. 516 func (cfg Config) Clone() Config { 517 rt := make(map[string]string) 518 for k, v := range cfg.Runtime { 519 rt[k] = v 520 } 521 c := cfg 522 c.Runtime = rt 523 c.set = append([]string{}, cfg.set...) 524 return c 525 } 526 527 // hosts returns a slice of copies of this config, one for each host. 528 func (cfg Config) hosts() []Config { 529 cfgs := make([]Config, 1, len(cfg.Multi)+1) 530 cfgs[0] = cfg.Clone() 531 for _, m := range cfg.Multi { 532 c := cfg.Clone() 533 c.Host, c.Hostaddr, c.Port = m.Host, m.Hostaddr, m.Port 534 cfgs = append(cfgs, c) 535 } 536 537 if cfg.LoadBalanceHosts == LoadBalanceHostsRandom { 538 rand.Shuffle(len(cfgs), func(i, j int) { cfgs[i], cfgs[j] = cfgs[j], cfgs[i] }) 539 } 540 541 return cfgs 542 } 543 544 func newConfig(dsn string, env []string) (Config, error) { 545 cfg := Config{ 546 Host: "localhost", 547 Port: 5432, 548 SSLSNI: true, 549 MinProtocolVersion: "3.0", 550 MaxProtocolVersion: "3.0", 551 } 552 if err := cfg.fromEnv(env); err != nil { 553 return Config{}, err 554 } 555 if err := cfg.fromDSN(dsn); err != nil { 556 return Config{}, err 557 } 558 if err := cfg.fromService(); err != nil { 559 return Config{}, err 560 } 561 562 // Need to have exactly the same number of host and hostaddr, or only specify one. 563 if cfg.isset("host") && cfg.Host != "" && cfg.Hostaddr != (netip.Addr{}) && len(cfg.multiHost) != len(cfg.multiHostaddr) { 564 return Config{}, fmt.Errorf("pq: could not match %d host names to %d hostaddr values", 565 len(cfg.multiHost)+1, len(cfg.multiHostaddr)+1) 566 } 567 // Need one port that applies to all or exactly the same number of ports as hosts. 568 l, ll := max(len(cfg.multiHost), len(cfg.multiHostaddr)), len(cfg.multiPort) 569 if l > 0 && ll > 0 && l != ll { 570 return Config{}, fmt.Errorf("pq: could not match %d port numbers to %d hosts", ll+1, l+1) 571 } 572 573 // Populate Multi 574 if len(cfg.multiHostaddr) > len(cfg.multiHost) { 575 cfg.multiHost = make([]string, len(cfg.multiHostaddr)) 576 } 577 for i, h := range cfg.multiHost { 578 p := cfg.Port 579 if len(cfg.multiPort) > 0 { 580 p = cfg.multiPort[i] 581 } 582 var addr netip.Addr 583 if len(cfg.multiHostaddr) > 0 { 584 addr = cfg.multiHostaddr[i] 585 } 586 cfg.Multi = append(cfg.Multi, ConfigMultihost{ 587 Host: h, 588 Port: p, 589 Hostaddr: addr, 590 }) 591 } 592 593 // Use the "fallback" application name if necessary 594 if cfg.isset("fallback_application_name") && !cfg.isset("application_name") { 595 cfg.ApplicationName = cfg.FallbackApplicationName 596 } 597 598 // We can't work with any client_encoding other than UTF-8 currently. 599 // However, we have historically allowed the user to set it to UTF-8 600 // explicitly, and there's no reason to break such programs, so allow that. 601 // Note that the "options" setting could also set client_encoding, but 602 // parsing its value is not worth it. Instead, we always explicitly send 603 // client_encoding as a separate run-time parameter, which should override 604 // anything set in options. 605 if cfg.isset("client_encoding") && !isUTF8(cfg.ClientEncoding) { 606 return Config{}, fmt.Errorf(`pq: unsupported client_encoding %q: must be absent or "UTF8"`, cfg.ClientEncoding) 607 } 608 // DateStyle needs a similar treatment. 609 if cfg.isset("datestyle") && cfg.Datestyle != "ISO, MDY" { 610 return Config{}, fmt.Errorf(`pq: unsupported datestyle %q: must be absent or "ISO, MDY"`, cfg.Datestyle) 611 } 612 cfg.ClientEncoding, cfg.Datestyle = "UTF8", "ISO, MDY" 613 614 // Set default user if not explicitly provided. 615 if !cfg.isset("user") { 616 u, err := pqutil.User() 617 if err != nil { 618 return Config{}, err 619 } 620 cfg.User = u 621 } 622 623 // SSL is not necessary or supported over UNIX domain sockets. 624 if nw, _ := cfg.network(); nw == "unix" { 625 cfg.SSLMode = SSLModeDisable 626 } 627 628 if cfg.MinProtocolVersion > cfg.MaxProtocolVersion { 629 return Config{}, fmt.Errorf("pq: min_protocol_version %q cannot be greater than max_protocol_version %q", 630 cfg.MinProtocolVersion, cfg.MaxProtocolVersion) 631 } 632 if cfg.SSLNegotiation == SSLNegotiationDirect { 633 switch cfg.SSLMode { 634 case SSLModeDisable, SSLModeAllow, SSLModePrefer: 635 return Config{}, fmt.Errorf( 636 `pq: weak sslmode %q may not be used with sslnegotiation=direct (use "require", "verify-ca", or "verify-full")`, 637 cfg.SSLMode) 638 } 639 } 640 if cfg.SSLRootCert == "system" { 641 if !cfg.isset("sslmode") { 642 cfg.SSLMode = SSLModeVerifyFull 643 } 644 if cfg.SSLMode != SSLModeVerifyFull { 645 return Config{}, fmt.Errorf( 646 `pq: weak sslmode %q may not be used with sslrootcert=system (use "verify-full")`, 647 cfg.SSLMode) 648 } 649 } 650 651 return cfg, nil 652 } 653 654 func (cfg Config) network() (string, string) { 655 if cfg.Hostaddr != (netip.Addr{}) { 656 return "tcp", net.JoinHostPort(cfg.Hostaddr.String(), strconv.Itoa(int(cfg.Port))) 657 } 658 // UNIX domain sockets are either represented by an (absolute) file system 659 // path or they live in the abstract name space (starting with an @). 660 if filepath.IsAbs(cfg.Host) || strings.HasPrefix(cfg.Host, "@") { 661 sockPath := filepath.Join(cfg.Host, ".s.PGSQL."+strconv.Itoa(int(cfg.Port))) 662 return "unix", sockPath 663 } 664 return "tcp", net.JoinHostPort(cfg.Host, strconv.Itoa(int(cfg.Port))) 665 } 666 667 func (cfg *Config) fromEnv(env []string) error { 668 e := make(map[string]string) 669 for _, v := range env { 670 k, v, ok := strings.Cut(v, "=") 671 if !ok { 672 continue 673 } 674 switch k { 675 case "PGREQUIRESSL", "PGSSLCOMPRESSION", // Deprecated. 676 "PGREALM", "PGGSSENCMODE", "PGGSSDELEGATION", "PGGSSLIB", // krb stuff 677 "PGREQUIREAUTH", "PGCHANNELBINDING", 678 "PGSSLCERTMODE", "PGSSLCRL", "PGSSLCRLDIR", "PGREQUIREPEER": 679 return fmt.Errorf("pq: environment variable $%s is not supported", k) 680 case "PGKRBSRVNAME": 681 if newGss == nil { 682 return fmt.Errorf("pq: environment variable $%s is not supported as Kerberos is not enabled", k) 683 } 684 } 685 e[k] = v 686 } 687 return cfg.setFromTag(e, "env", false) 688 } 689 690 // fromDSN parses the options from name and adds them to the values. 691 // 692 // The parsing code is based on conninfo_parse from libpq's fe-connect.c 693 func (cfg *Config) fromDSN(dsn string) error { 694 if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { 695 var err error 696 dsn, err = convertURL(dsn) 697 if err != nil { 698 return err 699 } 700 } 701 702 var ( 703 opt = make(map[string]string) 704 s = []rune(dsn) 705 i int 706 next = func() (rune, bool) { 707 if i >= len(s) { 708 return 0, false 709 } 710 r := s[i] 711 i++ 712 return r, true 713 } 714 skipSpaces = func() (rune, bool) { 715 r, ok := next() 716 for unicode.IsSpace(r) && ok { 717 r, ok = next() 718 } 719 return r, ok 720 } 721 ) 722 723 for { 724 var ( 725 keyRunes, valRunes []rune 726 r rune 727 ok bool 728 ) 729 730 if r, ok = skipSpaces(); !ok { 731 break 732 } 733 734 // Scan the key 735 for !unicode.IsSpace(r) && r != '=' { 736 keyRunes = append(keyRunes, r) 737 if r, ok = next(); !ok { 738 break 739 } 740 } 741 742 // Skip any whitespace if we're not at the = yet 743 if r != '=' { 744 r, ok = skipSpaces() 745 } 746 747 // The current character should be = 748 if r != '=' || !ok { 749 return fmt.Errorf(`missing "=" after %q in connection info string`, string(keyRunes)) 750 } 751 752 // Skip any whitespace after the = 753 if r, ok = skipSpaces(); !ok { 754 // If we reach the end here, the last value is just an empty string as per libpq. 755 opt[string(keyRunes)] = "" 756 break 757 } 758 759 if r != '\'' { 760 for !unicode.IsSpace(r) { 761 if r == '\\' { 762 if r, ok = next(); !ok { 763 return fmt.Errorf(`missing character after backslash`) 764 } 765 } 766 valRunes = append(valRunes, r) 767 768 if r, ok = next(); !ok { 769 break 770 } 771 } 772 } else { 773 quote: 774 for { 775 if r, ok = next(); !ok { 776 return fmt.Errorf(`unterminated quoted string literal in connection string`) 777 } 778 switch r { 779 case '\'': 780 break quote 781 case '\\': 782 r, _ = next() 783 fallthrough 784 default: 785 valRunes = append(valRunes, r) 786 } 787 } 788 } 789 790 opt[string(keyRunes)] = string(valRunes) 791 } 792 793 return cfg.setFromTag(opt, "postgres", false) 794 } 795 796 func (cfg *Config) fromService() error { 797 if cfg.Service == "" { 798 return nil 799 } 800 801 if !cfg.isset("PGSERVICEFILE") { 802 if home := pqutil.Home(false); home != "" { 803 cfg.ServiceFile = filepath.Join(home, ".pg_service.conf") 804 } 805 } 806 807 opts, err := pgservice.FindService(cfg.ServiceFile, cfg.Service) 808 if err != nil { 809 return fmt.Errorf("pq: %w", err) 810 } 811 return cfg.setFromTag(opts, "postgres", true) 812 } 813 814 func (cfg *Config) setFromTag(o map[string]string, tag string, service bool) error { 815 f := "pq: wrong value for %q: " 816 if tag == "env" { 817 f = "pq: wrong value for $%s: " 818 } 819 var ( 820 types = reflect.TypeOf(cfg).Elem() 821 values = reflect.ValueOf(cfg).Elem() 822 ) 823 for i := 0; i < types.NumField(); i++ { 824 var ( 825 rt = types.Field(i) 826 rv = values.Field(i) 827 k = rt.Tag.Get(tag) 828 connectTimeout = (tag == "postgres" && k == "connect_timeout") || (tag == "env" && k == "PGCONNECT_TIMEOUT") 829 host = (tag == "postgres" && k == "host") || (tag == "env" && k == "PGHOST") 830 hostaddr = (tag == "postgres" && k == "hostaddr") || (tag == "env" && k == "PGHOSTADDR") 831 port = (tag == "postgres" && k == "port") || (tag == "env" && k == "PGPORT") 832 sslmode = (tag == "postgres" && k == "sslmode") || (tag == "env" && k == "PGSSLMODE") 833 sslnegotiation = (tag == "postgres" && k == "sslnegotiation") || (tag == "env" && k == "PGSSLNEGOTIATION") 834 targetsessionattrs = (tag == "postgres" && k == "target_session_attrs") || (tag == "env" && k == "PGTARGETSESSIONATTRS") 835 loadbalancehosts = (tag == "postgres" && k == "load_balance_hosts") || (tag == "env" && k == "PGLOADBALANCEHOSTS") 836 minprotocolversion = (tag == "postgres" && k == "min_protocol_version") || (tag == "env" && k == "PGMINPROTOCOLVERSION") 837 maxprotocolversion = (tag == "postgres" && k == "max_protocol_version") || (tag == "env" && k == "PGMAXPROTOCOLVERSION") 838 sslminprotocolversion = (tag == "postgres" && k == "ssl_min_protocol_version") || (tag == "env" && k == "SSLPGMINPROTOCOLVERSION") 839 sslmaxprotocolversion = (tag == "postgres" && k == "ssl_max_protocol_version") || (tag == "env" && k == "SSLPGMAXPROTOCOLVERSION") 840 ) 841 if k == "" || k == "-" { 842 continue 843 } 844 845 v, ok := o[k] 846 delete(o, k) 847 if ok { 848 t, ok := rt.Tag.Lookup("postgres") 849 if !ok || t == "" || t == "-" { // For PGSERVICEFILE, which can only be from env 850 t, ok = rt.Tag.Lookup("env") 851 } 852 if ok && t != "" && t != "-" { 853 cfg.set = append(cfg.set, t) 854 } 855 switch rt.Type.Kind() { 856 default: 857 return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type.Kind()) 858 case reflect.Struct: 859 if rt.Type == reflect.TypeOf(netip.Addr{}) { 860 if hostaddr { 861 vv := strings.Split(v, ",") 862 v = vv[0] 863 for _, vvv := range vv[1:] { 864 if vvv == "" { 865 cfg.multiHostaddr = append(cfg.multiHostaddr, netip.Addr{}) 866 } else { 867 ip, err := netip.ParseAddr(vvv) 868 if err != nil { 869 return fmt.Errorf(f+"%w", k, err) 870 } 871 cfg.multiHostaddr = append(cfg.multiHostaddr, ip) 872 } 873 } 874 } 875 ip, err := netip.ParseAddr(v) 876 if err != nil { 877 return fmt.Errorf(f+"%w", k, err) 878 } 879 rv.Set(reflect.ValueOf(ip)) 880 } else { 881 return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type) 882 } 883 case reflect.String: 884 if sslmode && !slices.Contains(sslModes, SSLMode(v)) && !(strings.HasPrefix(v, "pqgo-") && hasTLSConfig(v[5:])) { 885 return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(sslModes)) 886 } 887 if sslnegotiation && !slices.Contains(sslNegotiations, SSLNegotiation(v)) { 888 return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(sslNegotiations)) 889 } 890 if targetsessionattrs && !slices.Contains(targetSessionAttrs, TargetSessionAttrs(v)) { 891 return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(targetSessionAttrs)) 892 } 893 if loadbalancehosts && !slices.Contains(loadBalanceHosts, LoadBalanceHosts(v)) { 894 return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(loadBalanceHosts)) 895 } 896 if (minprotocolversion || maxprotocolversion) && !slices.Contains(protocolVersions, ProtocolVersion(v)) { 897 return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(protocolVersions)) 898 } 899 if (sslminprotocolversion || sslmaxprotocolversion) && !slices.Contains(sslProtocolVersions, SSLProtocolVersion(v)) { 900 return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(sslProtocolVersions)) 901 } 902 if host { 903 vv := strings.Split(v, ",") 904 v = vv[0] 905 for i, vvv := range vv[1:] { 906 if vvv == "" { 907 vv[i+1] = "localhost" 908 } 909 } 910 cfg.multiHost = append(cfg.multiHost, vv[1:]...) 911 } 912 rv.SetString(v) 913 case reflect.Int64: 914 n, err := strconv.ParseInt(v, 10, 64) 915 if err != nil { 916 return fmt.Errorf(f+"%w", k, err) 917 } 918 if connectTimeout { 919 n = int64(time.Duration(n) * time.Second) 920 } 921 rv.SetInt(n) 922 case reflect.Uint16: 923 if port { 924 vv := strings.Split(v, ",") 925 v = vv[0] 926 for _, vvv := range vv[1:] { 927 if vvv == "" { 928 vvv = "5432" 929 } 930 n, err := strconv.ParseUint(vvv, 10, 16) 931 if err != nil { 932 return fmt.Errorf(f+"%w", k, err) 933 } 934 cfg.multiPort = append(cfg.multiPort, uint16(n)) 935 } 936 } 937 n, err := strconv.ParseUint(v, 10, 16) 938 if err != nil { 939 return fmt.Errorf(f+"%w", k, err) 940 } 941 rv.SetUint(n) 942 case reflect.Bool: 943 b, err := pqutil.ParseBool(v) 944 if err != nil { 945 return fmt.Errorf(f+"%w", k, err) 946 } 947 rv.SetBool(b) 948 } 949 } 950 } 951 952 if service && len(o) > 0 { 953 // TODO(go1.23): use maps.Keys once we require Go 1.23. 954 var key string 955 for k := range o { 956 key = k 957 break 958 } 959 return fmt.Errorf("pq: unknown setting %q in service file for service %q", key, cfg.Service) 960 } 961 962 // Set run-time; we delete map keys as they're set in the struct. 963 if !service && tag == "postgres" { 964 // Make sure database= sets dbname=, as that previously worked (kind of 965 // by accident). 966 // TODO(v2): remove 967 if d, ok := o["database"]; ok { 968 cfg.Database = d 969 delete(o, "database") 970 } 971 cfg.Runtime = o 972 } 973 974 return nil 975 } 976 977 // Should generally only be used from newConfig(), as it will never be set if 978 // people go outside that. 979 func (cfg Config) isset(name string) bool { 980 return slices.Contains(cfg.set, name) 981 } 982 983 // Convert to a map; used only in tests. 984 func (cfg Config) tomap() map[string]string { 985 var ( 986 o = make(map[string]string) 987 values = reflect.ValueOf(cfg) 988 types = reflect.TypeOf(cfg) 989 ) 990 for i := 0; i < types.NumField(); i++ { 991 var ( 992 rt = types.Field(i) 993 rv = values.Field(i) 994 k = rt.Tag.Get("postgres") 995 ) 996 if k == "" || k == "-" { 997 continue 998 } 999 if !rv.IsZero() || slices.Contains(cfg.set, k) { 1000 switch rt.Type.Kind() { 1001 default: 1002 if s, ok := rv.Interface().(fmt.Stringer); ok { 1003 o[k] = s.String() 1004 } else { 1005 o[k] = rv.String() 1006 } 1007 case reflect.Uint16: 1008 n := rv.Uint() 1009 o[k] = strconv.FormatUint(n, 10) 1010 case reflect.Int64: 1011 n := rv.Int() 1012 if k == "connect_timeout" { 1013 n = int64(time.Duration(n) / time.Second) 1014 } 1015 o[k] = strconv.FormatInt(n, 10) 1016 case reflect.Bool: 1017 if rv.Bool() { 1018 o[k] = "yes" 1019 } else { 1020 o[k] = "no" 1021 } 1022 } 1023 } 1024 } 1025 for k, v := range cfg.Runtime { 1026 o[k] = v 1027 } 1028 return o 1029 } 1030 1031 // Create DSN for this config; used only in tests. 1032 func (cfg Config) string() string { 1033 var ( 1034 m = cfg.tomap() 1035 keys = make([]string, 0, len(m)) 1036 ) 1037 for k := range m { 1038 switch k { 1039 case "datestyle", "client_encoding": 1040 continue 1041 case "host", "port", "user", "sslsni", "min_protocol_version", "max_protocol_version": 1042 if !cfg.isset(k) { 1043 continue 1044 } 1045 } 1046 if k == "application_name" && m[k] == "pqgo" { 1047 continue 1048 } 1049 if k == "host" && len(cfg.multiHost) > 0 { 1050 m[k] += "," + strings.Join(cfg.multiHost, ",") 1051 } 1052 if k == "hostaddr" && len(cfg.multiHostaddr) > 0 { 1053 for _, ha := range cfg.multiHostaddr { 1054 m[k] += "," 1055 if ha != (netip.Addr{}) { 1056 m[k] += ha.String() 1057 } 1058 } 1059 } 1060 if k == "port" && len(cfg.multiPort) > 0 { 1061 for _, p := range cfg.multiPort { 1062 m[k] += "," + strconv.Itoa(int(p)) 1063 } 1064 } 1065 keys = append(keys, k) 1066 } 1067 sort.Strings(keys) 1068 1069 var b strings.Builder 1070 for i, k := range keys { 1071 if i > 0 { 1072 b.WriteByte(' ') 1073 } 1074 b.WriteString(k) 1075 b.WriteByte('=') 1076 var ( 1077 v = m[k] 1078 nv = make([]rune, 0, len(v)+2) 1079 quote = v == "" 1080 ) 1081 for _, c := range v { 1082 if c == ' ' { 1083 quote = true 1084 } 1085 if c == '\'' { 1086 nv = append(nv, '\\') 1087 } 1088 nv = append(nv, c) 1089 } 1090 if quote { 1091 b.WriteByte('\'') 1092 } 1093 b.WriteString(string(nv)) 1094 if quote { 1095 b.WriteByte('\'') 1096 } 1097 } 1098 return b.String() 1099 } 1100 1101 // Recognize all sorts of silly things as "UTF-8", like Postgres does 1102 func isUTF8(name string) bool { 1103 s := strings.Map(func(c rune) rune { 1104 if 'A' <= c && c <= 'Z' { 1105 return c + ('a' - 'A') 1106 } 1107 if 'a' <= c && c <= 'z' || '0' <= c && c <= '9' { 1108 return c 1109 } 1110 return -1 // discard 1111 }, name) 1112 return s == "utf8" || s == "unicode" 1113 } 1114 1115 func convertURL(url string) (string, error) { 1116 u, err := neturl.Parse(url) 1117 if err != nil { 1118 return "", err 1119 } 1120 1121 if u.Scheme != "postgres" && u.Scheme != "postgresql" { 1122 return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) 1123 } 1124 1125 var kvs []string 1126 escaper := strings.NewReplacer(`'`, `\'`, `\`, `\\`) 1127 accrue := func(k, v string) { 1128 if v != "" { 1129 kvs = append(kvs, k+"='"+escaper.Replace(v)+"'") 1130 } 1131 } 1132 1133 if u.User != nil { 1134 pw, _ := u.User.Password() 1135 accrue("user", u.User.Username()) 1136 accrue("password", pw) 1137 } 1138 1139 if host, port, err := net.SplitHostPort(u.Host); err != nil { 1140 accrue("host", u.Host) 1141 } else { 1142 accrue("host", host) 1143 accrue("port", port) 1144 } 1145 1146 if u.Path != "" { 1147 accrue("dbname", u.Path[1:]) 1148 } 1149 1150 q := u.Query() 1151 for k := range q { 1152 accrue(k, q.Get(k)) 1153 } 1154 1155 sort.Strings(kvs) // Makes testing easier (not a performance concern) 1156 return strings.Join(kvs, " "), nil 1157 }