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) 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 hosts = append(hosts, h) 415 ports = append(ports, p) 416 } 417 if len(hosts) > 0 { 418 settings["host"] = strings.Join(hosts, ",") 419 } 420 if len(ports) > 0 { 421 settings["port"] = strings.Join(ports, ",") 422 } 423 424 database := strings.TrimLeft(url.Path, "/") 425 if database != "" { 426 settings["database"] = database 427 } 428 429 for k, v := range url.Query() { 430 settings[k] = v[0] 431 } 432 433 return settings, nil 434} 435 436func isIPOnly(host string) bool { 437 return net.ParseIP(strings.Trim(host, "[]")) != nil || !strings.Contains(host, ":") 438} 439 440var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} 441 442func parseDSNSettings(s string) (map[string]string, error) { 443 settings := make(map[string]string) 444 445 nameMap := map[string]string{ 446 "dbname": "database", 447 } 448 449 for len(s) > 0 { 450 var key, val string 451 eqIdx := strings.IndexRune(s, '=') 452 if eqIdx < 0 { 453 return nil, errors.New("invalid dsn") 454 } 455 456 key = strings.Trim(s[:eqIdx], " \t\n\r\v\f") 457 s = strings.TrimLeft(s[eqIdx+1:], " \t\n\r\v\f") 458 if len(s) == 0 { 459 } else if s[0] != '\'' { 460 end := 0 461 for ; end < len(s); end++ { 462 if asciiSpace[s[end]] == 1 { 463 break 464 } 465 if s[end] == '\\' { 466 end++ 467 if end == len(s) { 468 return nil, errors.New("invalid backslash") 469 } 470 } 471 } 472 val = strings.Replace(strings.Replace(s[:end], "\\\\", "\\", -1), "\\'", "'", -1) 473 if end == len(s) { 474 s = "" 475 } else { 476 s = s[end+1:] 477 } 478 } else { // quoted string 479 s = s[1:] 480 end := 0 481 for ; end < len(s); end++ { 482 if s[end] == '\'' { 483 break 484 } 485 if s[end] == '\\' { 486 end++ 487 } 488 } 489 if end == len(s) { 490 return nil, errors.New("unterminated quoted string in connection info string") 491 } 492 val = strings.Replace(strings.Replace(s[:end], "\\\\", "\\", -1), "\\'", "'", -1) 493 if end == len(s) { 494 s = "" 495 } else { 496 s = s[end+1:] 497 } 498 } 499 500 if k, ok := nameMap[key]; ok { 501 key = k 502 } 503 504 if key == "" { 505 return nil, errors.New("invalid dsn") 506 } 507 508 settings[key] = val 509 } 510 511 return settings, nil 512} 513 514func parseServiceSettings(servicefilePath, serviceName string) (map[string]string, error) { 515 servicefile, err := pgservicefile.ReadServicefile(servicefilePath) 516 if err != nil { 517 return nil, fmt.Errorf("failed to read service file: %v", servicefilePath) 518 } 519 520 service, err := servicefile.GetService(serviceName) 521 if err != nil { 522 return nil, fmt.Errorf("unable to find service: %v", serviceName) 523 } 524 525 nameMap := map[string]string{ 526 "dbname": "database", 527 } 528 529 settings := make(map[string]string, len(service.Settings)) 530 for k, v := range service.Settings { 531 if k2, present := nameMap[k]; present { 532 k = k2 533 } 534 settings[k] = v 535 } 536 537 return settings, nil 538} 539 540// configTLS uses libpq's TLS parameters to construct []*tls.Config. It is 541// necessary to allow returning multiple TLS configs as sslmode "allow" and 542// "prefer" allow fallback. 543func configTLS(settings map[string]string) ([]*tls.Config, error) { 544 host := settings["host"] 545 sslmode := settings["sslmode"] 546 sslrootcert := settings["sslrootcert"] 547 sslcert := settings["sslcert"] 548 sslkey := settings["sslkey"] 549 550 // Match libpq default behavior 551 if sslmode == "" { 552 sslmode = "prefer" 553 } 554 555 tlsConfig := &tls.Config{} 556 557 switch sslmode { 558 case "disable": 559 return []*tls.Config{nil}, nil 560 case "allow", "prefer": 561 tlsConfig.InsecureSkipVerify = true 562 case "require": 563 // According to PostgreSQL documentation, if a root CA file exists, 564 // the behavior of sslmode=require should be the same as that of verify-ca 565 // 566 // See https://www.postgresql.org/docs/12/libpq-ssl.html 567 if sslrootcert != "" { 568 goto nextCase 569 } 570 tlsConfig.InsecureSkipVerify = true 571 break 572 nextCase: 573 fallthrough 574 case "verify-ca": 575 // Don't perform the default certificate verification because it 576 // will verify the hostname. Instead, verify the server's 577 // certificate chain ourselves in VerifyPeerCertificate and 578 // ignore the server name. This emulates libpq's verify-ca 579 // behavior. 580 // 581 // See https://github.com/golang/go/issues/21971#issuecomment-332693931 582 // and https://pkg.go.dev/crypto/tls?tab=doc#example-Config-VerifyPeerCertificate 583 // for more info. 584 tlsConfig.InsecureSkipVerify = true 585 tlsConfig.VerifyPeerCertificate = func(certificates [][]byte, _ [][]*x509.Certificate) error { 586 certs := make([]*x509.Certificate, len(certificates)) 587 for i, asn1Data := range certificates { 588 cert, err := x509.ParseCertificate(asn1Data) 589 if err != nil { 590 return errors.New("failed to parse certificate from server: " + err.Error()) 591 } 592 certs[i] = cert 593 } 594 595 // Leave DNSName empty to skip hostname verification. 596 opts := x509.VerifyOptions{ 597 Roots: tlsConfig.RootCAs, 598 Intermediates: x509.NewCertPool(), 599 } 600 // Skip the first cert because it's the leaf. All others 601 // are intermediates. 602 for _, cert := range certs[1:] { 603 opts.Intermediates.AddCert(cert) 604 } 605 _, err := certs[0].Verify(opts) 606 return err 607 } 608 case "verify-full": 609 tlsConfig.ServerName = host 610 default: 611 return nil, errors.New("sslmode is invalid") 612 } 613 614 if sslrootcert != "" { 615 caCertPool := x509.NewCertPool() 616 617 caPath := sslrootcert 618 caCert, err := ioutil.ReadFile(caPath) 619 if err != nil { 620 return nil, fmt.Errorf("unable to read CA file: %w", err) 621 } 622 623 if !caCertPool.AppendCertsFromPEM(caCert) { 624 return nil, errors.New("unable to add CA to cert pool") 625 } 626 627 tlsConfig.RootCAs = caCertPool 628 tlsConfig.ClientCAs = caCertPool 629 } 630 631 if (sslcert != "" && sslkey == "") || (sslcert == "" && sslkey != "") { 632 return nil, errors.New(`both "sslcert" and "sslkey" are required`) 633 } 634 635 if sslcert != "" && sslkey != "" { 636 cert, err := tls.LoadX509KeyPair(sslcert, sslkey) 637 if err != nil { 638 return nil, fmt.Errorf("unable to read cert: %w", err) 639 } 640 641 tlsConfig.Certificates = []tls.Certificate{cert} 642 } 643 644 switch sslmode { 645 case "allow": 646 return []*tls.Config{nil, tlsConfig}, nil 647 case "prefer": 648 return []*tls.Config{tlsConfig, nil}, nil 649 case "require", "verify-ca", "verify-full": 650 return []*tls.Config{tlsConfig}, nil 651 default: 652 panic("BUG: bad sslmode should already have been caught") 653 } 654} 655 656func parsePort(s string) (uint16, error) { 657 port, err := strconv.ParseUint(s, 10, 16) 658 if err != nil { 659 return 0, err 660 } 661 if port < 1 || port > math.MaxUint16 { 662 return 0, errors.New("outside range") 663 } 664 return uint16(port), nil 665} 666 667func makeDefaultDialer() *net.Dialer { 668 return &net.Dialer{KeepAlive: 5 * time.Minute} 669} 670 671func makeDefaultResolver() *net.Resolver { 672 return net.DefaultResolver 673} 674 675func makeDefaultBuildFrontendFunc(minBufferLen int) BuildFrontendFunc { 676 return func(r io.Reader, w io.Writer) Frontend { 677 cr, err := chunkreader.NewConfig(r, chunkreader.Config{MinBufLen: minBufferLen}) 678 if err != nil { 679 panic(fmt.Sprintf("BUG: chunkreader.NewConfig failed: %v", err)) 680 } 681 frontend := pgproto3.NewFrontend(cr, w) 682 683 return frontend 684 } 685} 686 687func parseConnectTimeoutSetting(s string) (time.Duration, error) { 688 timeout, err := strconv.ParseInt(s, 10, 64) 689 if err != nil { 690 return 0, err 691 } 692 if timeout < 0 { 693 return 0, errors.New("negative timeout") 694 } 695 return time.Duration(timeout) * time.Second, nil 696} 697 698func makeConnectTimeoutDialFunc(timeout time.Duration) DialFunc { 699 d := makeDefaultDialer() 700 d.Timeout = timeout 701 return d.DialContext 702} 703 704// ValidateConnectTargetSessionAttrsReadWrite is an ValidateConnectFunc that implements libpq compatible 705// target_session_attrs=read-write. 706func ValidateConnectTargetSessionAttrsReadWrite(ctx context.Context, pgConn *PgConn) error { 707 result := pgConn.ExecParams(ctx, "show transaction_read_only", nil, nil, nil, nil).Read() 708 if result.Err != nil { 709 return result.Err 710 } 711 712 if string(result.Rows[0][0]) == "on" { 713 return errors.New("read only connection") 714 } 715 716 return nil 717} 718