Skip to content

Commit

Permalink
feat: add SetCertificateFromFile and SetCertificateFromString (#941)
Browse files Browse the repository at this point in the history
- refactor root cert and client root methods to make consistent signature
  • Loading branch information
jeevatkm authored Jan 4, 2025
1 parent e7bb2cd commit caa2b42
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 90 deletions.
18 changes: 18 additions & 0 deletions .testdata/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC+jCCAeKgAwIBAgIRAJce5ewsoW44j0qvSABmq7owDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yNTAxMDQwNzA3MTNaFw0yNjAxMDQwNzA3
MTNaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQCYkTN1g/0Z3KkS3w0lX9yhZkwiA0obXCeFs7hpRP0p4WlW3uADyXQ5
h2MaYx8OCA7oGU7/dWOPhtE3rgFEz7IwLxcP5d02ukLGlFD69D6KLyTXwCFmvOWQ
5fbOq4s73WTNDfYSTYNzeujDCjeu/Bk0OVhdxbyZdyrpdm+UBfH8uIDoGeCRXnji
nqG9HNOQx6r/S6FqC5j/7PrVl1i66WlqRzKEJB94uejfujrHq8RjQm/wzEutU5df
C39zEEEx75qQt7Jc0asm1AqAKSq34xn4rVajWrBZ/WudUUizHfaBDP61uPFvPyKW
JDvTSdeoM9TPX0y0cjo6AwSrdLl7flrRAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIF
oDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuC
CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAdHvPQe3EJ4/X6K/bklJUhIfM
KBauH8VMBfri7xLawleKssm7GdiFivSA0g1pArkl8SALBlPqhrx7rwlyyivLTZaR
VFvXaQ9eU0zGnSnDnKVz6CX/zn3TKfcgZPEBclayh0ldm7A8xSJWaWbRZ+s9e9x1
XcQTn2KkMZfBDMnGEWQ3KZrClvO5ZfkqSiyzEm9+eF0m0E7ujTyfSVMsPdyldA6U
pHG8omQTyOzJl2I4z7DlS0AEsL0TJHV4iKr9rDei2xQz/wtful5qU/taYp2Y6zMH
8ytnDldJhmcCwmvtqvK5p6CbkatE7TFyw2CxQJHnQef+Y4W94sSZWg9CGRKDIQ==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions .testdata/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCYkTN1g/0Z3KkS
3w0lX9yhZkwiA0obXCeFs7hpRP0p4WlW3uADyXQ5h2MaYx8OCA7oGU7/dWOPhtE3
rgFEz7IwLxcP5d02ukLGlFD69D6KLyTXwCFmvOWQ5fbOq4s73WTNDfYSTYNzeujD
Cjeu/Bk0OVhdxbyZdyrpdm+UBfH8uIDoGeCRXnjinqG9HNOQx6r/S6FqC5j/7PrV
l1i66WlqRzKEJB94uejfujrHq8RjQm/wzEutU5dfC39zEEEx75qQt7Jc0asm1AqA
KSq34xn4rVajWrBZ/WudUUizHfaBDP61uPFvPyKWJDvTSdeoM9TPX0y0cjo6AwSr
dLl7flrRAgMBAAECggEAJPTPNUEilxgncGXNZmdBJ2uDN536XoRFIpL1MbK/bFyo
yp00QFaVK7ZK4EJwbFKxYbF3vFOwKT0sAsPIlOWGsTtG59fzbOVTdYzJzPBLEef3
kbd9n8hUB3RdA5T0Ji0r1Kv0FlzmYZu9NDmOYXm5lTfq2tQiKj5+i4zf3EhQZLng
4wVxBT7yQUQcstJv5K1L6HVzunSYtbHx8ZVxmw+tJ4lMCK23KPlvncZZTT8chWdT
3GOp5nYIHk9E5jQnBnj7p73sxZUCZlb8uhLtdcgAXc4scptEVO+7n5zOaXIv40Oz
yfkESgHcZWAMDvnkxdySHlD38Z2LIKDGbqR6O9wcwQKBgQDBO6fFPXO41nsxdVCB
nhCgL2hsGjaxJzGBLaOJNVMMFRASN3Yqvs4N1Hn7lawRI/FRRffxjLkZfNGEBSF2
OipdvX19Oe2hCZxvwHPoe5sb/Dh6KE7If1hRLOCXg/8E7ADBtAp94dam1WF4Kh6N
Va6+n2YKif2rqye1YtRoUU46iQKBgQDKH/eMcMRUe9IySxHLogidOUwa0X7WrxF/
PkXGpPbHQtMOJF5cVzh+L+foUKXNM60lgmCH0438GKU7kirC/dVtD/bwE598/XFZ
vnjPV7Adf9vBz9NN8cS/4uEfQYbvTRmrnrQK+ZhOe8hmwjapxqdWrVHNUtvx18vL
qBwR4YjsCQKBgCycMx1MFJ1FludSKCXkcf4pM7hRTPMVE065VJnmn6eYbT9nYnZ3
2mZC+W5lnXXPkHSs7JLtZAZIVK5f6Nu8je9aQdBZQUz+RQlfquKvNp39WqSJDbcn
/yGudKNGK+fc/Ee74vgw3Tdi57+wKaGDeHY1on8oYFHzj5VGnbb/nknRAoGBAK2Z
hyQ4NmfZcU+A6mfbY0qmS5c9F5OMCZsgAQ374XiDDIK4+dKVlw/KVYRSwBTerXfp
4r7GFMzQ3hmsEM4o9YYWkCDiubjAdPp/fYOX7MtpZXWw6euoGzQzyObvgNVHgyTD
yh8jAI1oA1c+t3RaCp+HfRq8b+vnTEI+wN0auF8BAoGBAJmw+GgHCZGpw2XPNu+X
8kuVGbQYAjTOXhBM4WzZyhfH1TWKLGn7C9YixhE2AW0UWKDvy+6OqPhe8q3KVms3
8YZ1W+vbUNEZNGE0XrB5ZMXfePiqisCz0jgP9OAuT+ii4aI3MAm3zgCEC6UTMvLq
gNBu3Tcy6udxnUf7czzJDRtE
-----END PRIVATE KEY-----
12 changes: 7 additions & 5 deletions cert_watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ func TestClient_SetRootCertificateWatcher(t *testing.T) {
// Make sure that TLS handshake happens for all request
// (otherwise, test may succeed because 1st TLS session is re-used)
DisableKeepAlives: true,
}).SetRootCertificateWatcher(paths.RootCACert, &CertWatcherOptions{
PoolInterval: poolingInterval,
}).SetClientRootCertificateWatcher(paths.RootCACert, &CertWatcherOptions{
PoolInterval: poolingInterval,
}).SetDebug(false)
}).SetRootCertificatesWatcher(
&CertWatcherOptions{PoolInterval: poolingInterval},
paths.RootCACert,
).SetClientRootCertificatesWatcher(
&CertWatcherOptions{PoolInterval: poolingInterval},
paths.RootCACert,
).SetDebug(false)

url := strings.Replace(ts.URL, "127.0.0.1", "localhost", 1)
t.Log("Test URL:", url)
Expand Down
227 changes: 155 additions & 72 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,52 @@ func (c *Client) RemoveProxy() *Client {
return c
}

// SetCertificates method helps to conveniently set client certificates into Resty.
// SetCertificateFromString method helps to set client certificates into Resty
// from cert and key files to perform SSL client authentication
//
// client.SetCertificateFromFile("certs/client.pem", "certs/client.key")
func (c *Client) SetCertificateFromFile(certFilePath, certKeyFilePath string) *Client {
cert, err := tls.LoadX509KeyPair(certFilePath, certKeyFilePath)
if err != nil {
c.Logger().Errorf("client certificate/key parsing error: %v", err)
return c
}
c.SetCertificates(cert)
return c
}

// SetCertificateFromString method helps to set client certificates into Resty
// from string to perform SSL client authentication
//
// myClientCertStr := `-----BEGIN CERTIFICATE-----
// ... cert content ...
// -----END CERTIFICATE-----`
//
// myClientCertKeyStr := `-----BEGIN PRIVATE KEY-----
// ... cert key content ...
// -----END PRIVATE KEY-----`
//
// client.SetCertificateFromString(myClientCertStr, myClientCertKeyStr)
func (c *Client) SetCertificateFromString(certStr, certKeyStr string) *Client {
cert, err := tls.X509KeyPair([]byte(certStr), []byte(certKeyStr))
if err != nil {
c.Logger().Errorf("client certificate/key parsing error: %v", err)
return c
}
c.SetCertificates(cert)
return c
}

// SetCertificates method helps to conveniently set client certificates into Resty
// to perform SSL client authentication
//
// cert, err := tls.LoadX509KeyPair("certs/client.pem", "certs/client.key")
// if err != nil {
// log.Printf("ERROR client certificate/key parsing error: %v", err)
// return
// }
//
// client.SetCertificates(cert)
func (c *Client) SetCertificates(certs ...tls.Certificate) *Client {
config, err := c.tlsConfig()
if err != nil {
Expand All @@ -1502,72 +1547,142 @@ func (c *Client) SetCertificates(certs ...tls.Certificate) *Client {
return c
}

// SetRootCertificate method helps to add one or more root certificates into the Resty client
// SetRootCertificates method helps to add one or more root certificates into the Resty client
//
// client.SetRootCertificate("/path/to/root/pemFile.pem")
func (c *Client) SetRootCertificate(pemFilePath string) *Client {
rootPemData, err := os.ReadFile(pemFilePath)
if err != nil {
c.Logger().Errorf("%v", err)
return c
// // one pem file path
// client.SetRootCertificates("/path/to/root/pemFile.pem")
//
// // one or more pem file path(s)
// client.SetRootCertificates(
// "/path/to/root/pemFile1.pem",
// "/path/to/root/pemFile2.pem"
// "/path/to/root/pemFile3.pem"
// )
//
// // if you happen to have string slices
// client.SetRootCertificates(certs...)
func (c *Client) SetRootCertificates(pemFilePaths ...string) *Client {
for _, fp := range pemFilePaths {
rootPemData, err := os.ReadFile(fp)
if err != nil {
c.Logger().Errorf("%v", err)
return c
}
c.handleCAs("root", rootPemData)
}
c.handleCAs("root", rootPemData)
return c
}

// SetRootCertificateWatcher enables dynamic reloading of one or more root certificates.
// SetRootCertificatesWatcher method enables dynamic reloading of one or more root certificates.
// It is designed for scenarios involving long-running Resty clients where certificates may be renewed.
// The caller is responsible for calling Close to stop the watcher.
//
// client.SetRootCertificateWatcher("root-ca.crt", &CertWatcherOptions{
// PoolInterval: time.Hour * 24,
// })
//
// defer client.Close()
func (c *Client) SetRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client {
c.SetRootCertificate(pemFilePath)
c.initCertWatcher(pemFilePath, "root", options)
// client.SetRootCertificatesWatcher(
// &resty.CertWatcherOptions{
// PoolInterval: 24 * time.Hour,
// },
// "root-ca.crt",
// )
func (c *Client) SetRootCertificatesWatcher(options *CertWatcherOptions, pemFilePaths ...string) *Client {
c.SetRootCertificates(pemFilePaths...)
for _, fp := range pemFilePaths {
c.initCertWatcher(fp, "root", options)
}
return c
}

// SetRootCertificateFromString method helps to add one or more root certificates
// into the Resty client
//
// client.SetRootCertificateFromString("pem certs content")
// myRootCertStr := `-----BEGIN CERTIFICATE-----
// ... cert content ...
// -----END CERTIFICATE-----`
//
// client.SetRootCertificateFromString(myRootCertStr)
func (c *Client) SetRootCertificateFromString(pemCerts string) *Client {
c.handleCAs("root", []byte(pemCerts))
return c
}

// SetClientRootCertificate method helps to add one or more client's root
// SetClientRootCertificates method helps to add one or more client's root
// certificates into the Resty client
//
// client.SetClientRootCertificate("/path/to/root/pemFile.pem")
func (c *Client) SetClientRootCertificate(pemFilePath string) *Client {
rootPemData, err := os.ReadFile(pemFilePath)
if err != nil {
c.Logger().Errorf("%v", err)
return c
// // one pem file path
// client.SetClientCertificates("/path/to/client/pemFile.pem")
//
// // one or more pem file path(s)
// client.SetClientCertificates(
// "/path/to/client/pemFile1.pem",
// "/path/to/client/pemFile2.pem"
// "/path/to/client/pemFile3.pem"
// )
//
// // if you happen to have string slices
// client.SetClientCertificates(certs...)
func (c *Client) SetClientRootCertificates(pemFilePaths ...string) *Client {
for _, fp := range pemFilePaths {
pemData, err := os.ReadFile(fp)
if err != nil {
c.Logger().Errorf("%v", err)
return c
}
c.handleCAs("client-root", pemData)
}
c.handleCAs("client", rootPemData)
return c
}

// SetClientRootCertificateWatcher enables dynamic reloading of one or more client root certificates.
// SetClientRootCertificatesWatcher method enables dynamic reloading of one or more client root certificates.
// It is designed for scenarios involving long-running Resty clients where certificates may be renewed.
// The caller is responsible for calling Close to stop the watcher.
//
// client.SetClientRootCertificateWatcher("root-ca.crt", &CertWatcherOptions{
// PoolInterval: time.Hour * 24,
// })
// defer client.Close()
func (c *Client) SetClientRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client {
c.SetClientRootCertificate(pemFilePath)
c.initCertWatcher(pemFilePath, "client", options)
// client.SetClientRootCertificatesWatcher(
// &resty.CertWatcherOptions{
// PoolInterval: 24 * time.Hour,
// },
// "root-ca.crt",
// )
func (c *Client) SetClientRootCertificatesWatcher(options *CertWatcherOptions, pemFilePaths ...string) *Client {
c.SetClientRootCertificates(pemFilePaths...)
for _, fp := range pemFilePaths {
c.initCertWatcher(fp, "client-root", options)
}
return c
}

// SetClientRootCertificateFromString method helps to add one or more clients
// root certificates into the Resty client
//
// myClientRootCertStr := `-----BEGIN CERTIFICATE-----
// ... cert content ...
// -----END CERTIFICATE-----`
//
// client.SetClientRootCertificateFromString(myClientRootCertStr)
func (c *Client) SetClientRootCertificateFromString(pemCerts string) *Client {
c.handleCAs("client-root", []byte(pemCerts))
return c
}

func (c *Client) handleCAs(scope string, permCerts []byte) {
config, err := c.tlsConfig()
if err != nil {
c.Logger().Errorf("%v", err)
return
}

c.lock.Lock()
defer c.lock.Unlock()
switch scope {
case "root":
if config.RootCAs == nil {
config.RootCAs = x509.NewCertPool()
}
config.RootCAs.AppendCertsFromPEM(permCerts)
case "client-root":
if config.ClientCAs == nil {
config.ClientCAs = x509.NewCertPool()
}
config.ClientCAs.AppendCertsFromPEM(permCerts)
}
}

func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcherOptions) {
tickerDuration := defaultWatcherPoolingInterval
if options != nil && options.PoolInterval > 0 {
Expand Down Expand Up @@ -1611,9 +1726,9 @@ func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcher

switch scope {
case "root":
c.SetRootCertificate(pemFilePath)
case "client":
c.SetClientRootCertificate(pemFilePath)
c.SetRootCertificates(pemFilePath)
case "client-root":
c.SetClientRootCertificates(pemFilePath)
}

c.debugf("Cert %s reloaded.", pemFilePath)
Expand All @@ -1622,38 +1737,6 @@ func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcher
}()
}

// SetClientRootCertificateFromString method helps to add one or more clients
// root certificates into the Resty client
//
// client.SetClientRootCertificateFromString("pem certs content")
func (c *Client) SetClientRootCertificateFromString(pemCerts string) *Client {
c.handleCAs("client", []byte(pemCerts))
return c
}

func (c *Client) handleCAs(scope string, permCerts []byte) {
config, err := c.tlsConfig()
if err != nil {
c.Logger().Errorf("%v", err)
return
}

c.lock.Lock()
defer c.lock.Unlock()
switch scope {
case "root":
if config.RootCAs == nil {
config.RootCAs = x509.NewCertPool()
}
config.RootCAs.AppendCertsFromPEM(permCerts)
case "client":
if config.ClientCAs == nil {
config.ClientCAs = x509.NewCertPool()
}
config.ClientCAs.AppendCertsFromPEM(permCerts)
}
}

// OutputDirectory method returns the output directory value from the client.
func (c *Client) OutputDirectory() string {
c.lock.RLock()
Expand Down
Loading

0 comments on commit caa2b42

Please sign in to comment.