1package pgconn 2 3import ( 4 "context" 5 "crypto/tls" 6 "crypto/x509" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "math" 12 "net" 13 "net/url" 14 "os" 15 "path/filepath" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/jackc/chunkreader/v2" 21 "github.com/jackc/pgpassfile" 22 "github.com/jackc/pgproto3/v2" 23 "github.com/jackc/pgservicefile" 24) 25 26type AfterConnectFunc func(ctx context.Context, pgconn *PgConn) error 27type ValidateConnectFunc func(ctx context.Context, pgconn *PgConn) error 28 29// Config is the settings used to establish a connection to a PostgreSQL server. It must be created by ParseConfig. A 30// manually initialized Config will cause ConnectConfig to panic. 31type Config struct { 32 Host string // host (e.g. localhost) or absolute path to unix domain socket directory (e.g. /private/tmp) 33 Port uint16 34 Database string 35 User string 36 Password string 37 TLSConfig *tls.Config // nil disables TLS 38 ConnectTimeout time.Duration 39 DialFunc DialFunc // e.g. net.Dialer.DialContext 40 LookupFunc LookupFunc // e.g. net.Resolver.LookupHost 41 BuildFrontend BuildFrontendFunc 42 RuntimeParams map[string]string // Run-time parameters to set on connection as session default values (e.g. search_path or application_name) 43 44 Fallbacks []*FallbackConfig 45 46 // ValidateConnect is called during a connection attempt after a successful authentication with the PostgreSQL server. 47 // It can be used to validate that the server is acceptable. If this returns an error the connection is closed and the next 48 // fallback config is tried. This allows implementing high availability behavior such as libpq does with target_session_attrs. 49 ValidateConnect ValidateConnectFunc 50 51 // AfterConnect is called after ValidateConnect. It can be used to set up the connection (e.g. Set session variables 52 // or prepare statements). If this returns an error the connection attempt fails. 53 AfterConnect AfterConnectFunc 54 55 // OnNotice is a callback function called when a notice response is received. 56 OnNotice NoticeHandler 57 58 // OnNotification is a callback function called when a notification from the LISTEN/NOTIFY system is received. 59 OnNotification NotificationHandler 60 61 createdByParseConfig bool // Used to enforce created by ParseConfig rule. 62} 63 64// Copy returns a deep copy of the config that is safe to use and modify. 65// The only exception is the TLSConfig field: 66// according to the tls.Config docs it must not be modified after creation. 67func (c *Config) Copy() *Config { 68 newConf := new(Config) 69 *newConf = *c 70 if newConf.TLSConfig != nil { 71 newConf.TLSConfig = c.TLSConfig.Clone() 72 } 73 if newConf.RuntimeParams != nil { 74 newConf.RuntimeParams = make(map[string]string, len(c.RuntimeParams)) 75 for k, v := range c.RuntimeParams { 76 newConf.RuntimeParams[k] = v 77 } 78 } 79 if newConf.Fallbacks != nil { 80 newConf.Fallbacks = make([]*FallbackConfig, len(c.Fallbacks)) 81 for i, fallback := range c.Fallbacks { 82 newFallback := new(FallbackConfig) 83 *newFallback = *fallback 84 if newFallback.TLSConfig != nil { 85 newFallback.TLSConfig = fallback.TLSConfig.Clone() 86 } 87 newConf.Fallbacks[i] = newFallback 88 } 89 } 90 return newConf 91} 92 93// FallbackConfig is additional settings to attempt a connection with when the primary Config fails to establish a 94// network connection. It is used for TLS fallback such as sslmode=prefer and high availability (HA) connections. 95type FallbackConfig struct { 96 Host string // host (e.g. localhost) or path to unix domain socket directory (e.g. /private/tmp) 97 Port uint16 98 TLSConfig *tls.Config // nil disables TLS 99} 100 101// NetworkAddress converts a PostgreSQL host and port into network and address suitable for use with 102// net.Dial. 103func NetworkAddress(host string, port uint16) (network, address string) { 104 if strings.HasPrefix(host, "/") { 105 network = "unix" 106 address = filepath.Join(host, ".s.PGSQL.") + strconv.FormatInt(int64(port), 10) 107 } else { 108 network = "tcp" 109 address = net.JoinHostPort(host, strconv.Itoa(int(port))) 110 } 111 return network, address 112} 113 114// ParseConfig builds a *Config with similar behavior to the PostgreSQL standard C library libpq. It uses the same 115// defaults as libpq (e.g. port=5432) and understands most PG* environment variables. ParseConfig closely matches 116// the parsing behavior of libpq. connString may either be in URL format or keyword = value format (DSN style). See 117// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING for details. connString also may be 118// empty to only read from the environment. If a password is not supplied it will attempt to read the .pgpass file. 119// 120// # Example DSN 121// user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca 122// 123// # Example URL 124// postgres://jack:secret@pg.example.com:5432/mydb?sslmode=verify-ca 125// 126// The returned *Config may be modified. However, it is strongly recommended that any configuration that can be done 127// through the connection string be done there. In particular the fields Host, Port, TLSConfig, and Fallbacks can be 128// interdependent (e.g. TLSConfig needs knowledge of the host to validate the server certificate). These fields should 129// not be modified individually. They should all be modified or all left unchanged. 130// 131// ParseConfig supports specifying multiple hosts in similar manner to libpq. Host and port may include comma separated 132// values that will be tried in order. This can be used as part of a high availability system. See 133// https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS for more information. 134// 135// # Example URL 136// postgres://jack:secret@foo.example.com:5432,bar.example.com:5432/mydb 137// 138// ParseConfig currently recognizes the following environment variable and their parameter key word equivalents passed 139// via database URL or DSN: 140// 141// PGHOST 142// PGPORT 143// PGDATABASE 144// PGUSER 145// PGPASSWORD 146// PGPASSFILE 147// PGSERVICE 148// PGSERVICEFILE 149// PGSSLMODE 150// PGSSLCERT 151// PGSSLKEY 152// PGSSLROOTCERT 153// PGAPPNAME 154// PGCONNECT_TIMEOUT 155// PGTARGETSESSIONATTRS 156// 157// See http://www.postgresql.org/docs/11/static/libpq-envars.html for details on the meaning of environment variables. 158// 159// See https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-PARAMKEYWORDS for parameter key word names. They are 160// usually but not always the environment variable name downcased and without the "PG" prefix. 161// 162// Important Security Notes: 163// 164// ParseConfig tries to match libpq behavior with regard to PGSSLMODE. This includes defaulting to "prefer" behavior if 165// not set. 166// 167// See http://www.postgresql.org/docs/11/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION for details on what level of 168// security each sslmode provides. 169// 170// The sslmode "prefer" (the default), sslmode "allow", and multiple hosts are implemented via the Fallbacks field of 171// the Config struct. If TLSConfig is manually changed it will not affect the fallbacks. For example, in the case of 172// sslmode "prefer" this means it will first try the main Config settings which use TLS, then it will try the fallback 173// which does not use TLS. This can lead to an unexpected unencrypted connection if the main TLS config is manually 174// changed later but the unencrypted fallback is present. Ensure there are no stale fallbacks when manually setting 175// TLCConfig. 176// 177// Other known differences with libpq: 178// 179// If a host name resolves into multiple addresses, libpq will try all addresses. pgconn will only try the first. 180// 181// When multiple hosts are specified, libpq allows them to have different passwords set via the .pgpass file. pgconn 182// does not. 183// 184// In addition, ParseConfig accepts the following options: 185// 186// min_read_buffer_size 187// The minimum size of the internal read buffer. Default 8192. 188// servicefile 189// libpq only reads servicefile from the PGSERVICEFILE environment variable. ParseConfig accepts servicefile as a 190// part of the connection string. 191func ParseConfig(connString string) (*Config, error) { 192 defaultSettings := defaultSettings() 193 envSettings := parseEnvSettings() 194 195 connStringSettings := make(map[string]string) 196 if connString != "" { 197 var err error 198 // connString may be a database URL or a DSN 199 if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") { 200 connStringSettings, err = parseURLSettings(connString) 201 if err != nil { 202 return nil, &parseConfigError{connString: connString, msg: "failed to parse as URL", err: err} 203 } 204 } else { 205 connStringSettings, err = parseDSNSettings(connString) 206 if err != nil { 207 return nil, &parseConfigError{connString: connString, msg: "failed to parse as DSN", err: err} 208 } 209 } 210 } 211 212 settings := mergeSettings(defaultSettings, envSettings, connStringSettings) 213 if service, present := settings["service"]; present { 214 serviceSettings, err := parseServiceSettings(settings["servicefile"], service) 215 if err != nil { 216 return nil, &parseConfigError{connString: connString, msg: "failed to read service", err: err} 217 } 218 219 settings = mergeSettings(defaultSettings, envSettings, serviceSettings, connStringSettings) 220 } 221 222 minReadBufferSize, err := strconv.ParseInt(settings["min_read_buffer_size"], 10, 32) 223 if err != nil { 224 return nil, &parseConfigError{connString: connString, msg: "cannot parse min_read_buffer_size", err: err} 225 } 226 227 config := &Config{ 228 createdByParseConfig: true, 229 Database: settings["database"], 230 User: settings["user"], 231 Password: settings["password"], 232 RuntimeParams: make(map[string]string), 233 BuildFrontend: makeDefaultBuildFrontendFunc(int(minReadBufferSize)), 234 } 235 236 if connectTimeoutSetting, present := settings["connect_timeout"]; present { 237 connectTimeout, err := parseConnectTimeoutSetting(connectTimeoutSetting) 238 if err != nil { 239 return nil, &parseConfigError{connString: connString, msg: "invalid connect_timeout", err: err} 240 } 241 config.ConnectTimeout = connectTimeout 242 config.DialFunc = makeConnectTimeoutDialFunc(connectTimeout) 243 } else { 244 defaultDialer := makeDefaultDialer() 245 config.DialFunc = defaultDialer.DialContext 246 } 247 248 config.LookupFunc = makeDefaultResolver().LookupHost 249 250 notRuntimeParams := map[string]struct{}{ 251 "host": struct{}{}, 252 "port": struct{}{}, 253 "database": struct{}{}, 254 "user": struct{}{}, 255 "password": struct{}{}, 256 "passfile": struct{}{}, 257 "connect_timeout": struct{}{}, 258 "sslmode": struct{}{}, 259 "sslkey": struct{}{}, 260 "sslcert": struct{}{}, 261 "sslrootcert": struct{}{}, 262 "target_session_attrs": struct{}{}, 263 "min_read_buffer_size": struct{}{}, 264 "service": struct{}{}, 265 "servicefile": struct{}{}, 266 } 267 268 for k, v := range settings { 269 if _, present := notRuntimeParams[k]; present { 270 continue 271 } 272 config.RuntimeParams[k] = v 273 } 274 275 fallbacks := []*FallbackConfig{} 276 277 hosts := strings.Split(settings["host"], ",") 278 ports := strings.Split(settings["port"], ",") 279 280 for i, host := range hosts { 281 var portStr string 282 if i < len(ports) { 283 portStr = ports[i] 284 } else { 285 portStr = ports[0] 286 } 287 288 port, err := parsePort(portStr) 289 if err != nil { 290 return nil, &parseConfigError{connString: connString, msg: "invalid port", err: err} 291 } 292 293 var tlsConfigs []*tls.Config 294 295 // Ignore TLS settings if Unix domain socket like libpq 296 if network, _ := NetworkAddress(host, port); network == "unix" { 297 tlsConfigs = append(tlsConfigs, nil) 298 } else { 299 var err error 300 tlsConfigs, err = configTLS(settings, host) 301 if err != nil { 302 return nil, &parseConfigError{connString: connString, msg: "failed to configure TLS", err: err} 303 } 304 } 305 306 for _, tlsConfig := range tlsConfigs { 307 fallbacks = append(fallbacks, &FallbackConfig{ 308 Host: host, 309 Port: port, 310 TLSConfig: tlsConfig, 311 }) 312 } 313 } 314 315 config.Host = fallbacks[0].Host 316 config.Port = fallbacks[0].Port 317 config.TLSConfig = fallbacks[0].TLSConfig 318 config.Fallbacks = fallbacks[1:] 319 320 passfile, err := pgpassfile.ReadPassfile(settings["passfile"]) 321 if err == nil { 322 if config.Password == "" { 323 host := config.Host 324 if network, _ := NetworkAddress(config.Host, config.Port); network == "unix" { 325 host = "localhost" 326 } 327 328 config.Password = passfile.FindPassword(host, strconv.Itoa(int(config.Port)), config.Database, config.User) 329 } 330 } 331 332 if settings["target_session_attrs"] == "read-write" { 333 config.ValidateConnect = ValidateConnectTargetSessionAttrsReadWrite 334 } else if settings["target_session_attrs"] != "any" { 335 return nil, &parseConfigError{connString: connString, msg: fmt.Sprintf("unknown target_session_attrs value: %v", settings["target_session_attrs"])} 336 } 337 338 return config, nil 339} 340 341func mergeSettings(settingSets ...map[string]string) map[string]string { 342 settings := make(map[string]string) 343 344 for _, s2 := range settingSets { 345 for k, v := range s2 { 346 settings[k] = v 347 } 348 } 349 350 return settings 351} 352 353func parseEnvSettings() map[string]string { 354 settings := make(map[string]string) 355 356 nameMap := map[string]string{ 357 "PGHOST": "host", 358 "PGPORT": "port", 359 "PGDATABASE": "database", 360 "PGUSER": "user", 361 "PGPASSWORD": "password", 362 "PGPASSFILE": "passfile", 363 "PGAPPNAME": "application_name", 364 "PGCONNECT_TIMEOUT": "connect_timeout", 365 "PGSSLMODE": "sslmode", 366 "PGSSLKEY": "sslkey", 367 "PGSSLCERT": "sslcert", 368 "PGSSLROOTCERT": "sslrootcert", 369 "PGTARGETSESSIONATTRS": "target_session_attrs", 370 "PGSERVICE": "service", 371 "PGSERVICEFILE": "servicefile", 372 } 373 374 for envname, realname := range nameMap { 375 value := os.Getenv(envname) 376 if value != "" { 377 settings[realname] = value 378 } 379 } 380 381 return settings 382} 383 384func parseURLSettings(connString string) (map[string]string, error) { 385 settings := make(map[string]string) 386 387 url, err := url.Parse(connString) 388 if err != nil { 389 return nil, err 390 } 391 392 if url.User != nil { 393 settings["user"] = url.User.Username() 394 if password, present := url.User.Password(); present { 395 settings["password"] = password 396 } 397 } 398 399 // Handle multiple host:port's in url.Host by splitting them into host,host,host and port,port,port. 400 var hosts []string 401 var ports []string 402 for _, host := range strings.Split(url.Host, ",") { 403 if host == "" { 404 continue 405 } 406 if isIPOnly(host) { 407 hosts = append(hosts, strings.Trim(host, "[]")) 408 continue 409 } 410 h, p, err := net.SplitHostPort(host) 411 if err != nil { 412 return nil, fmt.Errorf("failed to split host:port in '%s', err: %w", host, err) 413 } 414 if h != "" { 415 hosts = append(hosts, h) 416 } 417 if p != "" { 418 ports = append(ports, p) 419 } 420 } 421 if len(hosts) > 0 { 422 settings["host"] = strings.Join(hosts, ",") 423 } 424 if len(ports) > 0 { 425 settings["port"] = strings.Join(ports, ",") 426 } 427 428 database := strings.TrimLeft(url.Path, "/") 429 if database != "" { 430 settings["database"] = database 431 } 432 433 nameMap := map[string]string{ 434 "dbname": "database", 435 } 436 437 for k, v := range url.Query() { 438 if k2, present := nameMap[k]; present { 439 k = k2 440 } 441 442 settings[k] = v[0] 443 } 444 445 return settings, nil 446} 447 448func isIPOnly(host string) bool { 449 return net.ParseIP(strings.Trim(host, "[]")) != nil || !strings.Contains(host, ":") 450} 451 452var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} 453 454func parseDSNSettings(s string) (map[string]string, error) { 455 settings := make(map[string]string) 456 457 nameMap := map[string]string{ 458 "dbname": "database", 459 } 460 461 for len(s) > 0 { 462 var key, val string 463 eqIdx := strings.IndexRune(s, '=') 464 if eqIdx < 0 { 465 return nil, errors.New("invalid dsn") 466 } 467 468 key = strings.Trim(s[:eqIdx], " \t\n\r\v\f") 469 s = strings.TrimLeft(s[eqIdx+1:], " \t\n\r\v\f") 470 if len(s) == 0 { 471 } else if s[0] != '\'' { 472 end := 0 473 for ; end < len(s); end++ { 474 if asciiSpace[s[end]] == 1 { 475 break 476 } 477 if s[end] == '\\' { 478 end++ 479 if end == len(s) { 480 return nil, errors.New("invalid backslash") 481 } 482 } 483 } 484 val = strings.Replace(strings.Replace(s[:end], "\\\\", "\\", -1), "\\'", "'", -1) 485 if end == len(s) { 486 s = "" 487 } else { 488 s = s[end+1:] 489 } 490 } else { // quoted string 491 s = s[1:] 492 end := 0 493 for ; end < len(s); end++ { 494 if s[end] == '\'' { 495 break 496 } 497 if s[end] == '\\' { 498 end++ 499 } 500 } 501 if end == len(s) { 502 return nil, errors.New("unterminated quoted string in connection info string") 503 } 504 val = strings.Replace(strings.Replace(s[:end], "\\\\", "\\", -1), "\\'", "'", -1) 505 if end == len(s) { 506 s = "" 507 } else { 508 s = s[end+1:] 509 } 510 } 511 512 if k, ok := nameMap[key]; ok { 513 key = k 514 } 515 516 if key == "" { 517 return nil, errors.New("invalid dsn") 518 } 519 520 settings[key] = val 521 } 522 523 return settings, nil 524} 525 526func parseServiceSettings(servicefilePath, serviceName string) (map[string]string, error) { 527 servicefile, err := pgservicefile.ReadServicefile(servicefilePath) 528 if err != nil { 529 return nil, fmt.Errorf("failed to read service file: %v", servicefilePath) 530 } 531 532 service, err := servicefile.GetService(serviceName) 533 if err != nil { 534 return nil, fmt.Errorf("unable to find service: %v", serviceName) 535 } 536 537 nameMap := map[string]string{ 538 "dbname": "database", 539 } 540 541 settings := make(map[string]string, len(service.Settings)) 542 for k, v := range service.Settings { 543 if k2, present := nameMap[k]; present { 544 k = k2 545 } 546 settings[k] = v 547 } 548 549 return settings, nil 550} 551 552// configTLS uses libpq's TLS parameters to construct []*tls.Config. It is 553// necessary to allow returning multiple TLS configs as sslmode "allow" and 554// "prefer" allow fallback. 555func configTLS(settings map[string]string, thisHost string) ([]*tls.Config, error) { 556 host := thisHost 557 sslmode := settings["sslmode"] 558 sslrootcert := settings["sslrootcert"] 559 sslcert := settings["sslcert"] 560 sslkey := settings["sslkey"] 561 562 // Match libpq default behavior 563 if sslmode == "" { 564 sslmode = "prefer" 565 } 566 567 tlsConfig := &tls.Config{} 568 569 switch sslmode { 570 case "disable": 571 return []*tls.Config{nil}, nil 572 case "allow", "prefer": 573 tlsConfig.InsecureSkipVerify = true 574 case "require": 575 // According to PostgreSQL documentation, if a root CA file exists, 576 // the behavior of sslmode=require should be the same as that of verify-ca 577 // 578 // See https://www.postgresql.org/docs/12/libpq-ssl.html 579 if sslrootcert != "" { 580 goto nextCase 581 } 582 tlsConfig.InsecureSkipVerify = true 583 break 584 nextCase: 585 fallthrough 586 case "verify-ca": 587 // Don't perform the default certificate verification because it 588 // will verify the hostname. Instead, verify the server's 589 // certificate chain ourselves in VerifyPeerCertificate and 590 // ignore the server name. This emulates libpq's verify-ca 591 // behavior. 592 // 593 // See https://github.com/golang/go/issues/21971#issuecomment-332693931 594 // and https://pkg.go.dev/crypto/tls?tab=doc#example-Config-VerifyPeerCertificate 595 // for more info. 596 tlsConfig.InsecureSkipVerify = true 597 tlsConfig.VerifyPeerCertificate = func(certificates [][]byte, _ [][]*x509.Certificate) error { 598 certs := make([]*x509.Certificate, len(certificates)) 599 for i, asn1Data := range certificates { 600 cert, err := x509.ParseCertificate(asn1Data) 601 if err != nil { 602 return errors.New("failed to parse certificate from server: " + err.Error()) 603 } 604 certs[i] = cert 605 } 606 607 // Leave DNSName empty to skip hostname verification. 608 opts := x509.VerifyOptions{ 609 Roots: tlsConfig.RootCAs, 610 Intermediates: x509.NewCertPool(), 611 } 612 // Skip the first cert because it's the leaf. All others 613 // are intermediates. 614 for _, cert := range certs[1:] { 615 opts.Intermediates.AddCert(cert) 616 } 617 _, err := certs[0].Verify(opts) 618 return err 619 } 620 case "verify-full": 621 tlsConfig.ServerName = host 622 default: 623 return nil, errors.New("sslmode is invalid") 624 } 625 626 if sslrootcert != "" { 627 caCertPool := x509.NewCertPool() 628 629 caPath := sslrootcert 630 caCert, err := ioutil.ReadFile(caPath) 631 if err != nil { 632 return nil, fmt.Errorf("unable to read CA file: %w", err) 633 } 634 635 if !caCertPool.AppendCertsFromPEM(caCert) { 636 return nil, errors.New("unable to add CA to cert pool") 637 } 638 639 tlsConfig.RootCAs = caCertPool 640 tlsConfig.ClientCAs = caCertPool 641 } 642 643 if (sslcert != "" && sslkey == "") || (sslcert == "" && sslkey != "") { 644 return nil, errors.New(`both "sslcert" and "sslkey" are required`) 645 } 646 647 if sslcert != "" && sslkey != "" { 648 cert, err := tls.LoadX509KeyPair(sslcert, sslkey) 649 if err != nil { 650 return nil, fmt.Errorf("unable to read cert: %w", err) 651 } 652 653 tlsConfig.Certificates = []tls.Certificate{cert} 654 } 655 656 switch sslmode { 657 case "allow": 658 return []*tls.Config{nil, tlsConfig}, nil 659 case "prefer": 660 return []*tls.Config{tlsConfig, nil}, nil 661 case "require", "verify-ca", "verify-full": 662 return []*tls.Config{tlsConfig}, nil 663 default: 664 panic("BUG: bad sslmode should already have been caught") 665 } 666} 667 668func parsePort(s string) (uint16, error) { 669 port, err := strconv.ParseUint(s, 10, 16) 670 if err != nil { 671 return 0, err 672 } 673 if port < 1 || port > math.MaxUint16 { 674 return 0, errors.New("outside range") 675 } 676 return uint16(port), nil 677} 678 679func makeDefaultDialer() *net.Dialer { 680 return &net.Dialer{KeepAlive: 5 * time.Minute} 681} 682 683func makeDefaultResolver() *net.Resolver { 684 return net.DefaultResolver 685} 686 687func makeDefaultBuildFrontendFunc(minBufferLen int) BuildFrontendFunc { 688 return func(r io.Reader, w io.Writer) Frontend { 689 cr, err := chunkreader.NewConfig(r, chunkreader.Config{MinBufLen: minBufferLen}) 690 if err != nil { 691 panic(fmt.Sprintf("BUG: chunkreader.NewConfig failed: %v", err)) 692 } 693 frontend := pgproto3.NewFrontend(cr, w) 694 695 return frontend 696 } 697} 698 699func parseConnectTimeoutSetting(s string) (time.Duration, error) { 700 timeout, err := strconv.ParseInt(s, 10, 64) 701 if err != nil { 702 return 0, err 703 } 704 if timeout < 0 { 705 return 0, errors.New("negative timeout") 706 } 707 return time.Duration(timeout) * time.Second, nil 708} 709 710func makeConnectTimeoutDialFunc(timeout time.Duration) DialFunc { 711 d := makeDefaultDialer() 712 d.Timeout = timeout 713 return d.DialContext 714} 715 716// ValidateConnectTargetSessionAttrsReadWrite is an ValidateConnectFunc that implements libpq compatible 717// target_session_attrs=read-write. 718func ValidateConnectTargetSessionAttrsReadWrite(ctx context.Context, pgConn *PgConn) error { 719 result := pgConn.ExecParams(ctx, "show transaction_read_only", nil, nil, nil, nil).Read() 720 if result.Err != nil { 721 return result.Err 722 } 723 724 if string(result.Rows[0][0]) == "on" { 725 return errors.New("read only connection") 726 } 727 728 return nil 729} 730