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