package pgconnimport ()typeAfterConnectFuncfunc(ctx context.Context, pgconn *PgConn) errortypeValidateConnectFuncfunc(ctx context.Context, pgconn *PgConn) errortypeGetSSLPasswordFuncfunc(ctx context.Context) string// Config is the settings used to establish a connection to a PostgreSQL server. It must be created by [ParseConfig]. A// manually initialized Config will cause ConnectConfig to panic.typeConfigstruct { Host string// host (e.g. localhost) or absolute path to unix domain socket directory (e.g. /private/tmp) Port uint16 Database string User string Password string TLSConfig *tls.Config// nil disables TLS ConnectTimeout time.Duration DialFunc DialFunc// e.g. net.Dialer.DialContext LookupFunc LookupFunc// e.g. net.Resolver.LookupHost BuildFrontend BuildFrontendFunc RuntimeParams map[string]string// Run-time parameters to set on connection as session default values (e.g. search_path or application_name) KerberosSrvName string KerberosSpn string Fallbacks []*FallbackConfig// ValidateConnect is called during a connection attempt after a successful authentication with the PostgreSQL server. // It can be used to validate that the server is acceptable. If this returns an error the connection is closed and the next // fallback config is tried. This allows implementing high availability behavior such as libpq does with target_session_attrs. ValidateConnect ValidateConnectFunc// AfterConnect is called after ValidateConnect. It can be used to set up the connection (e.g. Set session variables // or prepare statements). If this returns an error the connection attempt fails. AfterConnect AfterConnectFunc// OnNotice is a callback function called when a notice response is received. OnNotice NoticeHandler// OnNotification is a callback function called when a notification from the LISTEN/NOTIFY system is received. OnNotification NotificationHandler createdByParseConfig bool// Used to enforce created by ParseConfig rule.}// ParseConfigOptions contains options that control how a config is built such as GetSSLPassword.typeParseConfigOptionsstruct {// GetSSLPassword gets the password to decrypt a SSL client certificate. This is analogous to the the libpq function // PQsetSSLKeyPassHook_OpenSSL. GetSSLPassword GetSSLPasswordFunc}// Copy returns a deep copy of the config that is safe to use and modify.// The only exception is the TLSConfig field:// according to the tls.Config docs it must not be modified after creation.func ( *Config) () *Config { := new(Config) * = *if .TLSConfig != nil { .TLSConfig = .TLSConfig.Clone() }if .RuntimeParams != nil { .RuntimeParams = make(map[string]string, len(.RuntimeParams))for , := range .RuntimeParams { .RuntimeParams[] = } }if .Fallbacks != nil { .Fallbacks = make([]*FallbackConfig, len(.Fallbacks))for , := range .Fallbacks { := new(FallbackConfig) * = *if .TLSConfig != nil { .TLSConfig = .TLSConfig.Clone() } .Fallbacks[] = } }return}// FallbackConfig is additional settings to attempt a connection with when the primary Config fails to establish a// network connection. It is used for TLS fallback such as sslmode=prefer and high availability (HA) connections.typeFallbackConfigstruct { Host string// host (e.g. localhost) or path to unix domain socket directory (e.g. /private/tmp) Port uint16 TLSConfig *tls.Config// nil disables TLS}// isAbsolutePath checks if the provided value is an absolute path either// beginning with a forward slash (as on Linux-based systems) or with a capital// letter A-Z followed by a colon and a backslash, e.g., "C:\", (as on Windows).func isAbsolutePath( string) bool { := func( string) bool {iflen() < 3 {returnfalse } := [0] := [1] := [2]if >= 'A' && <= 'Z' && == ':' && == '\\' {returntrue }returnfalse }returnstrings.HasPrefix(, "/") || ()}// NetworkAddress converts a PostgreSQL host and port into network and address suitable for use with// net.Dial.func ( string, uint16) (, string) {ifisAbsolutePath() { = "unix" = filepath.Join(, ".s.PGSQL.") + strconv.FormatInt(int64(), 10) } else { = "tcp" = net.JoinHostPort(, strconv.Itoa(int())) }return , }// ParseConfig builds a *Config from connString with similar behavior to the PostgreSQL standard C library libpq. It// uses the same defaults as libpq (e.g. port=5432) and understands most PG* environment variables. ParseConfig closely// matches the parsing behavior of libpq. connString may either be in URL format or keyword = value format (DSN style).// See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING for details. connString also may be// empty to only read from the environment. If a password is not supplied it will attempt to read the .pgpass file.//// # Example DSN// user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca//// # Example URL// postgres://jack:secret@pg.example.com:5432/mydb?sslmode=verify-ca//// The returned *Config may be modified. However, it is strongly recommended that any configuration that can be done// through the connection string be done there. In particular the fields Host, Port, TLSConfig, and Fallbacks can be// interdependent (e.g. TLSConfig needs knowledge of the host to validate the server certificate). These fields should// not be modified individually. They should all be modified or all left unchanged.//// ParseConfig supports specifying multiple hosts in similar manner to libpq. Host and port may include comma separated// values that will be tried in order. This can be used as part of a high availability system. See// https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS for more information.//// # Example URL// postgres://jack:secret@foo.example.com:5432,bar.example.com:5432/mydb//// ParseConfig currently recognizes the following environment variable and their parameter key word equivalents passed// via database URL or DSN://// PGHOST// PGPORT// PGDATABASE// PGUSER// PGPASSWORD// PGPASSFILE// PGSERVICE// PGSERVICEFILE// PGSSLMODE// PGSSLCERT// PGSSLKEY// PGSSLROOTCERT// PGSSLPASSWORD// PGAPPNAME// PGCONNECT_TIMEOUT// PGTARGETSESSIONATTRS//// See http://www.postgresql.org/docs/11/static/libpq-envars.html for details on the meaning of environment variables.//// See https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-PARAMKEYWORDS for parameter key word names. They are// usually but not always the environment variable name downcased and without the "PG" prefix.//// Important Security Notes://// ParseConfig tries to match libpq behavior with regard to PGSSLMODE. This includes defaulting to "prefer" behavior if// not set.//// See http://www.postgresql.org/docs/11/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION for details on what level of// security each sslmode provides.//// The sslmode "prefer" (the default), sslmode "allow", and multiple hosts are implemented via the Fallbacks field of// the Config struct. If TLSConfig is manually changed it will not affect the fallbacks. For example, in the case of// sslmode "prefer" this means it will first try the main Config settings which use TLS, then it will try the fallback// which does not use TLS. This can lead to an unexpected unencrypted connection if the main TLS config is manually// changed later but the unencrypted fallback is present. Ensure there are no stale fallbacks when manually setting// TLSConfig.//// Other known differences with libpq://// When multiple hosts are specified, libpq allows them to have different passwords set via the .pgpass file. pgconn// does not.//// In addition, ParseConfig accepts the following options://// - servicefile.// libpq only reads servicefile from the PGSERVICEFILE environment variable. ParseConfig accepts servicefile as a// part of the connection string.func ( string) (*Config, error) {varParseConfigOptionsreturnParseConfigWithOptions(, )}// ParseConfigWithOptions builds a *Config from connString and options with similar behavior to the PostgreSQL standard// C library libpq. options contains settings that cannot be specified in a connString such as providing a function to// get the SSL password.func ( string, ParseConfigOptions) (*Config, error) { := defaultSettings() := parseEnvSettings() := make(map[string]string)if != "" {varerror// connString may be a database URL or a DSNifstrings.HasPrefix(, "postgres://") || strings.HasPrefix(, "postgresql://") { , = parseURLSettings()if != nil {returnnil, &parseConfigError{connString: , msg: "failed to parse as URL", err: } } } else { , = parseDSNSettings()if != nil {returnnil, &parseConfigError{connString: , msg: "failed to parse as DSN", err: } } } } := mergeSettings(, , )if , := ["service"]; { , := parseServiceSettings(["servicefile"], )if != nil {returnnil, &parseConfigError{connString: , msg: "failed to read service", err: } } = mergeSettings(, , , ) } := &Config{createdByParseConfig: true,Database: ["database"],User: ["user"],Password: ["password"],RuntimeParams: make(map[string]string),BuildFrontend: func( io.Reader, io.Writer) *pgproto3.Frontend {returnpgproto3.NewFrontend(, ) }, }if , := ["connect_timeout"]; { , := parseConnectTimeoutSetting()if != nil {returnnil, &parseConfigError{connString: , msg: "invalid connect_timeout", err: } } .ConnectTimeout = .DialFunc = makeConnectTimeoutDialFunc() } else { := makeDefaultDialer() .DialFunc = .DialContext } .LookupFunc = makeDefaultResolver().LookupHost := map[string]struct{}{"host": {},"port": {},"database": {},"user": {},"password": {},"passfile": {},"connect_timeout": {},"sslmode": {},"sslkey": {},"sslcert": {},"sslrootcert": {},"sslpassword": {},"sslsni": {},"krbspn": {},"krbsrvname": {},"target_session_attrs": {},"service": {},"servicefile": {}, }// Adding kerberos configurationif , := ["krbsrvname"]; { .KerberosSrvName = ["krbsrvname"] }if , := ["krbspn"]; { .KerberosSpn = ["krbspn"] }for , := range {if , := []; {continue } .RuntimeParams[] = } := []*FallbackConfig{} := strings.Split(["host"], ",") := strings.Split(["port"], ",")for , := range {varstringif < len() { = [] } else { = [0] } , := parsePort()if != nil {returnnil, &parseConfigError{connString: , msg: "invalid port", err: } }var []*tls.Config// Ignore TLS settings if Unix domain socket like libpqif , := NetworkAddress(, ); == "unix" { = append(, nil) } else {varerror , = configTLS(, , )if != nil {returnnil, &parseConfigError{connString: , msg: "failed to configure TLS", err: } } }for , := range { = append(, &FallbackConfig{Host: ,Port: ,TLSConfig: , }) } } .Host = [0].Host .Port = [0].Port .TLSConfig = [0].TLSConfig .Fallbacks = [1:] , := pgpassfile.ReadPassfile(["passfile"])if == nil {if .Password == "" { := .Hostif , := NetworkAddress(.Host, .Port); == "unix" { = "localhost" } .Password = .FindPassword(, strconv.Itoa(int(.Port)), .Database, .User) } }switch := ["target_session_attrs"]; {case"read-write": .ValidateConnect = ValidateConnectTargetSessionAttrsReadWritecase"read-only": .ValidateConnect = ValidateConnectTargetSessionAttrsReadOnlycase"primary": .ValidateConnect = ValidateConnectTargetSessionAttrsPrimarycase"standby": .ValidateConnect = ValidateConnectTargetSessionAttrsStandbycase"prefer-standby": .ValidateConnect = ValidateConnectTargetSessionAttrsPreferStandbycase"any":// do nothingdefault:returnnil, &parseConfigError{connString: , msg: fmt.Sprintf("unknown target_session_attrs value: %v", )} }return , nil}func mergeSettings( ...map[string]string) map[string]string { := make(map[string]string)for , := range {for , := range { [] = } }return}func parseEnvSettings() map[string]string { := make(map[string]string) := map[string]string{"PGHOST": "host","PGPORT": "port","PGDATABASE": "database","PGUSER": "user","PGPASSWORD": "password","PGPASSFILE": "passfile","PGAPPNAME": "application_name","PGCONNECT_TIMEOUT": "connect_timeout","PGSSLMODE": "sslmode","PGSSLKEY": "sslkey","PGSSLCERT": "sslcert","PGSSLSNI": "sslsni","PGSSLROOTCERT": "sslrootcert","PGSSLPASSWORD": "sslpassword","PGTARGETSESSIONATTRS": "target_session_attrs","PGSERVICE": "service","PGSERVICEFILE": "servicefile", }for , := range { := os.Getenv()if != "" { [] = } }return}func parseURLSettings( string) (map[string]string, error) { := make(map[string]string) , := url.Parse()if != nil {returnnil, }if .User != nil { ["user"] = .User.Username()if , := .User.Password(); { ["password"] = } }// Handle multiple host:port's in url.Host by splitting them into host,host,host and port,port,port.var []stringvar []stringfor , := rangestrings.Split(.Host, ",") {if == "" {continue }ifisIPOnly() { = append(, strings.Trim(, "[]"))continue } , , := net.SplitHostPort()if != nil {returnnil, fmt.Errorf("failed to split host:port in '%s', err: %w", , ) }if != "" { = append(, ) }if != "" { = append(, ) } }iflen() > 0 { ["host"] = strings.Join(, ",") }iflen() > 0 { ["port"] = strings.Join(, ",") } := strings.TrimLeft(.Path, "/")if != "" { ["database"] = } := map[string]string{"dbname": "database", }for , := range .Query() {if , := []; { = } [] = [0] }return , nil}func isIPOnly( string) bool {returnnet.ParseIP(strings.Trim(, "[]")) != nil || !strings.Contains(, ":")}var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}func parseDSNSettings( string) (map[string]string, error) { := make(map[string]string) := map[string]string{"dbname": "database", }forlen() > 0 {var , string := strings.IndexRune(, '=')if < 0 {returnnil, errors.New("invalid dsn") } = strings.Trim([:], " \t\n\r\v\f") = strings.TrimLeft([+1:], " \t\n\r\v\f")iflen() == 0 { } elseif [0] != '\'' { := 0for ; < len(); ++ {ifasciiSpace[[]] == 1 {break }if [] == '\\' { ++if == len() {returnnil, errors.New("invalid backslash") } } } = strings.Replace(strings.Replace([:], "\\\\", "\\", -1), "\\'", "'", -1)if == len() { = "" } else { = [+1:] } } else { // quoted string = [1:] := 0for ; < len(); ++ {if [] == '\'' {break }if [] == '\\' { ++ } }if == len() {returnnil, errors.New("unterminated quoted string in connection info string") } = strings.Replace(strings.Replace([:], "\\\\", "\\", -1), "\\'", "'", -1)if == len() { = "" } else { = [+1:] } }if , := []; { = }if == "" {returnnil, errors.New("invalid dsn") } [] = }return , nil}func parseServiceSettings(, string) (map[string]string, error) { , := pgservicefile.ReadServicefile()if != nil {returnnil, fmt.Errorf("failed to read service file: %v", ) } , := .GetService()if != nil {returnnil, fmt.Errorf("unable to find service: %v", ) } := map[string]string{"dbname": "database", } := make(map[string]string, len(.Settings))for , := range .Settings {if , := []; { = } [] = }return , nil}// configTLS uses libpq's TLS parameters to construct []*tls.Config. It is// necessary to allow returning multiple TLS configs as sslmode "allow" and// "prefer" allow fallback.func configTLS( map[string]string, string, ParseConfigOptions) ([]*tls.Config, error) { := := ["sslmode"] := ["sslrootcert"] := ["sslcert"] := ["sslkey"] := ["sslpassword"] := ["sslsni"]// Match libpq default behaviorif == "" { = "prefer" }if == "" { = "1" } := &tls.Config{}switch {case"disable":return []*tls.Config{nil}, nilcase"allow", "prefer": .InsecureSkipVerify = truecase"require":// According to PostgreSQL documentation, if a root CA file exists, // the behavior of sslmode=require should be the same as that of verify-ca // // See https://www.postgresql.org/docs/12/libpq-ssl.htmlif != "" {goto } .InsecureSkipVerify = truebreak :fallthroughcase"verify-ca":// Don't perform the default certificate verification because it // will verify the hostname. Instead, verify the server's // certificate chain ourselves in VerifyPeerCertificate and // ignore the server name. This emulates libpq's verify-ca // behavior. // // See https://github.com/golang/go/issues/21971#issuecomment-332693931 // and https://pkg.go.dev/crypto/tls?tab=doc#example-Config-VerifyPeerCertificate // for more info. .InsecureSkipVerify = true .VerifyPeerCertificate = func( [][]byte, [][]*x509.Certificate) error { := make([]*x509.Certificate, len())for , := range { , := x509.ParseCertificate()if != nil {returnerrors.New("failed to parse certificate from server: " + .Error()) } [] = }// Leave DNSName empty to skip hostname verification. := x509.VerifyOptions{Roots: .RootCAs,Intermediates: x509.NewCertPool(), }// Skip the first cert because it's the leaf. All others // are intermediates.for , := range [1:] { .Intermediates.AddCert() } , := [0].Verify()return }case"verify-full": .ServerName = default:returnnil, errors.New("sslmode is invalid") }if != "" { := x509.NewCertPool() := , := os.ReadFile()if != nil {returnnil, fmt.Errorf("unable to read CA file: %w", ) }if !.AppendCertsFromPEM() {returnnil, errors.New("unable to add CA to cert pool") } .RootCAs = .ClientCAs = }if ( != "" && == "") || ( == "" && != "") {returnnil, errors.New(`both "sslcert" and "sslkey" are required`) }if != "" && != "" { , := os.ReadFile()if != nil {returnnil, fmt.Errorf("unable to read sslkey: %w", ) } , := pem.Decode()var []bytevar []bytevarerror// If PEM is encrypted, attempt to decrypt using pass phraseifx509.IsEncryptedPEMBlock() {// Attempt decryption with pass phrase // NOTE: only supports RSA (PKCS#1)if != "" { , = x509.DecryptPEMBlock(, []byte()) }//if sslpassword not provided or has decryption error when use it //try to find sslpassword with callback functionif == "" || != nil {if .GetSSLPassword != nil { = .GetSSLPassword(context.Background()) }if == "" {returnnil, fmt.Errorf("unable to find sslpassword") } } , = x509.DecryptPEMBlock(, []byte())// Should we also provide warning for PKCS#1 needed?if != nil {returnnil, fmt.Errorf("unable to decrypt key: %w", ) } := pem.Block{Type: "RSA PRIVATE KEY",Bytes: , } = pem.EncodeToMemory(&) } else { = pem.EncodeToMemory() } , := os.ReadFile()if != nil {returnnil, fmt.Errorf("unable to read cert: %w", ) } , := tls.X509KeyPair(, )if != nil {returnnil, fmt.Errorf("unable to load cert: %w", ) } .Certificates = []tls.Certificate{} }// Set Server Name Indication (SNI), if enabled by connection parameters. // Per RFC 6066, do not set it if the host is a literal IP address (IPv4 // or IPv6).if == "1" && net.ParseIP() == nil { .ServerName = }switch {case"allow":return []*tls.Config{nil, }, nilcase"prefer":return []*tls.Config{, nil}, nilcase"require", "verify-ca", "verify-full":return []*tls.Config{}, nildefault:panic("BUG: bad sslmode should already have been caught") }}func parsePort( string) (uint16, error) { , := strconv.ParseUint(, 10, 16)if != nil {return0, }if < 1 || > math.MaxUint16 {return0, errors.New("outside range") }returnuint16(), nil}func makeDefaultDialer() *net.Dialer {return &net.Dialer{KeepAlive: 5 * time.Minute}}func makeDefaultResolver() *net.Resolver {returnnet.DefaultResolver}func parseConnectTimeoutSetting( string) (time.Duration, error) { , := strconv.ParseInt(, 10, 64)if != nil {return0, }if < 0 {return0, errors.New("negative timeout") }returntime.Duration() * time.Second, nil}func makeConnectTimeoutDialFunc( time.Duration) DialFunc { := makeDefaultDialer() .Timeout = return .DialContext}// ValidateConnectTargetSessionAttrsReadWrite is an ValidateConnectFunc that implements libpq compatible// target_session_attrs=read-write.func ( context.Context, *PgConn) error { := .ExecParams(, "show transaction_read_only", nil, nil, nil, nil).Read()if .Err != nil {return .Err }ifstring(.Rows[0][0]) == "on" {returnerrors.New("read only connection") }returnnil}// ValidateConnectTargetSessionAttrsReadOnly is an ValidateConnectFunc that implements libpq compatible// target_session_attrs=read-only.func ( context.Context, *PgConn) error { := .ExecParams(, "show transaction_read_only", nil, nil, nil, nil).Read()if .Err != nil {return .Err }ifstring(.Rows[0][0]) != "on" {returnerrors.New("connection is not read only") }returnnil}// ValidateConnectTargetSessionAttrsStandby is an ValidateConnectFunc that implements libpq compatible// target_session_attrs=standby.func ( context.Context, *PgConn) error { := .ExecParams(, "select pg_is_in_recovery()", nil, nil, nil, nil).Read()if .Err != nil {return .Err }ifstring(.Rows[0][0]) != "t" {returnerrors.New("server is not in hot standby mode") }returnnil}// ValidateConnectTargetSessionAttrsPrimary is an ValidateConnectFunc that implements libpq compatible// target_session_attrs=primary.func ( context.Context, *PgConn) error { := .ExecParams(, "select pg_is_in_recovery()", nil, nil, nil, nil).Read()if .Err != nil {return .Err }ifstring(.Rows[0][0]) == "t" {returnerrors.New("server is in standby mode") }returnnil}// ValidateConnectTargetSessionAttrsPreferStandby is an ValidateConnectFunc that implements libpq compatible// target_session_attrs=prefer-standby.func ( context.Context, *PgConn) error { := .ExecParams(, "select pg_is_in_recovery()", nil, nil, nil, nil).Read()if .Err != nil {return .Err }ifstring(.Rows[0][0]) != "t" {return &NotPreferredError{err: errors.New("server is not in hot standby mode")} }returnnil}
The pages are generated with Goldsv0.6.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds.