Skip to content

Commit

Permalink
ssl: reuse *tls.Config if connection settings are identical
Browse files Browse the repository at this point in the history
Previously, we would reload and re-parse a certificate from disk every
single time we initialized a connection and the sslrootcert setting
was enabled. This results in a lot of allocations that can be avoided.

Instead, save the *tls.Config for a given configuration hash, and
reuse it when we see it again.

Fixes #1032.
  • Loading branch information
kevinburke1 committed Mar 25, 2021
1 parent 072e83d commit 9449587
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 35 deletions.
18 changes: 18 additions & 0 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"os/user"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -394,6 +395,23 @@ func network(o values) (string, string) {

type values map[string]string

// Hash returns a deterministic hash of values.
func (v values) Hash() []byte {
keys := make([]string, len(v))
i := 0
for key := range v {
keys[i] = key
i++
}
sort.Strings(keys)
h := sha256.New()
for _, key := range keys {
h.Write([]byte(key))
h.Write([]byte(v[key]))
}
return h.Sum(nil)
}

// scanner implements a tokenizer for libpq-style option strings.
type scanner struct {
s []rune
Expand Down
81 changes: 46 additions & 35 deletions ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,36 @@ package pq
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"os"
"os/user"
"path/filepath"
"sync"
)

// ssl generates a function to upgrade a net.Conn based on the "sslmode" and
// related settings. The function is nil when no upgrade should take place.
func ssl(o values) (func(net.Conn) (net.Conn, error), error) {
// To avoid allocating the map if we never use ssl
var configMapOnce sync.Once
var configMapMu sync.Mutex
var configMap map[string]*ssldata

type ssldata struct {
Conf *tls.Config
VerifyCAOnly bool
}

func getTLSConf(o values) (*ssldata, error) {
verifyCaOnly := false
configMapOnce.Do(func() {
configMap = make(map[string]*ssldata)
})
configMapMu.Lock()
conf, ok := configMap[string(o.Hash())]
configMapMu.Unlock()
if ok {
return conf, nil
}
tlsConf := tls.Config{}
switch mode := o["sslmode"]; mode {
// "require" is the default.
Expand Down Expand Up @@ -59,20 +78,35 @@ func ssl(o values) (func(net.Conn) (net.Conn, error), error) {
return nil, err
}

// This pseudo-parameter is not recognized by the PostgreSQL server, so let's delete it after use.
delete(o, "sslinline")

// Accept renegotiation requests initiated by the backend.
//
// Renegotiation was deprecated then removed from PostgreSQL 9.5, but
// the default configuration of older versions has it enabled. Redshift
// also initiates renegotiations and cannot be reconfigured.
tlsConf.Renegotiation = tls.RenegotiateFreelyAsClient

data := &ssldata{&tlsConf, verifyCaOnly}
configMapMu.Lock()
configMap[string(o.Hash())] = data
fmt.Printf("o: %#v\n", string(o.Hash()))
configMapMu.Unlock()
return data, nil
}

// ssl generates a function to upgrade a net.Conn based on the "sslmode" and
// related settings. The function is nil when no upgrade should take place.
func ssl(o values) (func(net.Conn) (net.Conn, error), error) {
data, err := getTLSConf(o)
if data == nil && err == nil {
return nil, nil
}
if err != nil {
return nil, err
}
return func(conn net.Conn) (net.Conn, error) {
client := tls.Client(conn, &tlsConf)
if verifyCaOnly {
err := sslVerifyCertificateAuthority(client, &tlsConf)
client := tls.Client(conn, data.Conf)
if data.VerifyCAOnly {
err := sslVerifyCertificateAuthority(client, data.Conf)
if err != nil {
return nil, err
}
Expand All @@ -86,19 +120,6 @@ func ssl(o values) (func(net.Conn) (net.Conn, error), error) {
// in the user's home directory. The configured files must exist and have
// the correct permissions.
func sslClientCertificates(tlsConf *tls.Config, o values) error {
sslinline := o["sslinline"]
if sslinline == "true" {
cert, err := tls.X509KeyPair([]byte(o["sslcert"]), []byte(o["sslkey"]))
// Clear out these params, in case they were to be sent to the PostgreSQL server by mistake
o["sslcert"] = ""
o["sslkey"] = ""
if err != nil {
return err
}
tlsConf.Certificates = []tls.Certificate{cert}
return nil
}

// user.Current() might fail when cross-compiling. We have to ignore the
// error and continue without home directory defaults, since we wouldn't
// know from where to load them.
Expand Down Expand Up @@ -153,19 +174,9 @@ func sslCertificateAuthority(tlsConf *tls.Config, o values) error {
if sslrootcert := o["sslrootcert"]; len(sslrootcert) > 0 {
tlsConf.RootCAs = x509.NewCertPool()

sslinline := o["sslinline"]

var cert []byte
if sslinline == "true" {
// // Clear out this param, in case it were to be sent to the PostgreSQL server by mistake
o["sslrootcert"] = ""
cert = []byte(sslrootcert)
} else {
var err error
cert, err = ioutil.ReadFile(sslrootcert)
if err != nil {
return err
}
cert, err := ioutil.ReadFile(sslrootcert)
if err != nil {
return err
}

if !tlsConf.RootCAs.AppendCertsFromPEM(cert) {
Expand Down

0 comments on commit 9449587

Please sign in to comment.