From 4e0d39bc8380f7a0b04d01db2a7f1c47c6469f26 Mon Sep 17 00:00:00 2001 From: Clement Doucy Date: Sun, 22 Sep 2024 20:06:46 +0200 Subject: [PATCH 1/7] feat: Implemented ability to dynamically reload TLS certificates --- client.go | 18 ++++ pem_watcher.go | 124 ++++++++++++++++++++++ pem_watcher_test.go | 250 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 392 insertions(+) create mode 100644 pem_watcher.go create mode 100644 pem_watcher_test.go diff --git a/client.go b/client.go index e8e2df6..a040263 100644 --- a/client.go +++ b/client.go @@ -1369,6 +1369,15 @@ func (c *Client) SetRootCertificate(pemFilePath string) *Client { return c } +// SetRootCertificateWatcher enables dynamic reloading of one or more root certificates. +// It is designed for scenarios involving long-running Resty clients where certificates may be renewed. +// +// client.SetRootCertificateWatcher(&WatcherOptions{PemFilePath: "root-ca.crt"}) +func (c *Client) SetRootCertificateWatcher(options *WatcherOptions) *Client { + c.handleCAsWatcher("root", options) + return c +} + // SetRootCertificateFromString method helps to add one or more root certificates // into the Resty client // @@ -1392,6 +1401,15 @@ func (c *Client) SetClientRootCertificate(pemFilePath string) *Client { return c } +// SetClientRootCertificateWatcher enables dynamic reloading of one or more root certificates. +// It is designed for scenarios involving long-running Resty clients where certificates may be renewed. +// +// client.SetClientRootCertificateWatcher(&WatcherOptions{PemFilePath: "root-ca.crt"}) +func (c *Client) SetClientRootCertificateWatcher(options *WatcherOptions) *Client { + c.handleCAsWatcher("client", options) + return c +} + // SetClientRootCertificateFromString method helps to add one or more clients // root certificates into the Resty client // diff --git a/pem_watcher.go b/pem_watcher.go new file mode 100644 index 0000000..a42dee2 --- /dev/null +++ b/pem_watcher.go @@ -0,0 +1,124 @@ +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// resty source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package resty + +import ( + "crypto/x509" + "errors" + "os" + "time" +) + +const ( + defaultWatcherPoolingInterval = 1 * time.Minute +) + +// WatcherOptions struct is used to enable TLS Certificate hot reloading. +type WatcherOptions struct { + // PemFilePath is the path of the PEM file + PemFilePath string + + // PoolingInterval is the frequency at which resty will check if the PEM file needs to be reloaded. + // Default is 1 min. + PoolingInterval time.Duration +} + +type pemWatcher struct { + opt *WatcherOptions + + certPool *x509.CertPool + modTime time.Time + lastChecked time.Time + log Logger + debug bool +} + +func newPemWatcher(options *WatcherOptions, log Logger, debug bool) (*pemWatcher, error) { + if options.PemFilePath == "" { + return nil, errors.New("PemFilePath is required") + } + + if options.PoolingInterval == 0 { + options.PoolingInterval = defaultWatcherPoolingInterval + } + + cw := &pemWatcher{ + opt: options, + log: log, + debug: debug, + } + + if err := cw.checkRefresh(); err != nil { + return nil, err + } + + return cw, nil +} + +func (pw *pemWatcher) CertPool() (*x509.CertPool, error) { + if err := pw.checkRefresh(); err != nil { + return nil, err + } + + return pw.certPool, nil +} + +func (pw *pemWatcher) checkRefresh() error { + if time.Since(pw.lastChecked) <= pw.opt.PoolingInterval { + return nil + } + + pw.Debugf("Checking if cert has changed...") + + newModTime, err := pw.getModTime() + if err != nil { + return err + } + + if pw.modTime.Equal(newModTime) { + pw.lastChecked = time.Now().UTC() + pw.Debugf("No change") + return nil + } + + if err := pw.refreshCertPool(); err != nil { + return err + } + + pw.modTime = newModTime + pw.lastChecked = time.Now().UTC() + + pw.Debugf("Cert refreshed") + + return nil +} + +func (pw *pemWatcher) getModTime() (time.Time, error) { + info, err := os.Stat(pw.opt.PemFilePath) + if err != nil { + return time.Time{}, err + } + + return info.ModTime().UTC(), nil +} + +func (pw *pemWatcher) refreshCertPool() error { + pemCert, err := os.ReadFile(pw.opt.PemFilePath) + if err != nil { + return nil + } + + pw.certPool = x509.NewCertPool() + pw.certPool.AppendCertsFromPEM(pemCert) + return nil +} + +func (pw *pemWatcher) Debugf(format string, v ...interface{}) { + if !pw.debug { + return + } + + pw.log.Debugf(format, v...) +} diff --git a/pem_watcher_test.go b/pem_watcher_test.go new file mode 100644 index 0000000..b1c2343 --- /dev/null +++ b/pem_watcher_test.go @@ -0,0 +1,250 @@ +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// resty source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package resty + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "net/http" + "os" + "path/filepath" + "testing" + "time" +) + +type certPaths struct { + RootCAKey string + RootCACert string + TLSKey string + TLSCert string +} + +func TestClient_SetRootCertificateWatcher(t *testing.T) { + // For this test, we want to: + // - Generate root CA + // - Generate TLS cert signed with root CA + // - Start an HTTPS server + // - Create a resty client with SetRootCertificateWatcher + // - Send multiple request and re-generate the certs periodically to reproduce renewal + + certDir := t.TempDir() + paths := certPaths{ + RootCAKey: filepath.Join(certDir, "root-ca.key"), + RootCACert: filepath.Join(certDir, "root-ca.crt"), + TLSKey: filepath.Join(certDir, "tls.key"), + TLSCert: filepath.Join(certDir, "tls.crt"), + } + + port := findAvailablePort(t) + generateCerts(t, paths) + startHTTPSServer(fmt.Sprintf(":%d", port), paths) + + client := New().SetDebug(true).SetRootCertificateWatcher(&WatcherOptions{ + PemFilePath: paths.RootCACert, + PoolingInterval: time.Second * 1, + }) + + url := fmt.Sprintf("https://localhost:%d/", port) + + for i := 0; i < 5; i++ { + time.Sleep(1 * time.Second) + res, err := client.R().Get(url) + if err != nil { + t.Fatal(err) + } + + assertEqual(t, res.StatusCode(), http.StatusOK) + + if i%2 == 1 { + // Re-generate certs to simulate renewal scenario + generateCerts(t, paths) + } + } +} + +func startHTTPSServer(addr string, path certPaths) { + tlsConfig := &tls.Config{ + GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + cert, err := tls.LoadX509KeyPair(path.TLSCert, path.TLSKey) + if err != nil { + return nil, err + } + return &cert, nil + }, + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + srv := &http.Server{ + Addr: addr, + TLSConfig: tlsConfig, + } + + go func() { + err := srv.ListenAndServeTLS("", "") + if err != nil { + panic(err) + } + }() +} + +func findAvailablePort(t *testing.T) int { + port := -1 + + for port == -1 { + listener, err := net.Listen("tcp", ":0") + if err != nil { + continue + } + port = listener.Addr().(*net.TCPAddr).Port + if err := listener.Close(); err != nil { + t.Fatal(err) + } + } + + return port +} + +func generateCerts(t *testing.T, paths certPaths) { + rootKey, rootCert, err := generateRootCA(paths.RootCAKey, paths.RootCACert) + if err != nil { + t.Fatal(err) + } + + if err := generateTLSCert(paths.TLSKey, paths.TLSCert, rootKey, rootCert); err != nil { + t.Fatal(err) + } +} + +// Generate a Root Certificate Authority (CA) +func generateRootCA(keyPath, certPath string) (*rsa.PrivateKey, []byte, error) { + // Generate the key for the Root CA + rootKey, err := generateKey() + if err != nil { + return nil, nil, err + } + + // Create the root certificate template + rootCertTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"YourOrg"}, + Country: []string{"US"}, + Province: []string{"State"}, + Locality: []string{"City"}, + CommonName: "YourRootCA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), // 10 years validity + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + IsCA: true, + BasicConstraintsValid: true, + } + + // Self-sign the root certificate + rootCert, err := x509.CreateCertificate(rand.Reader, rootCertTemplate, rootCertTemplate, &rootKey.PublicKey, rootKey) + if err != nil { + return nil, nil, err + } + + // Save the Root CA key and certificate + if err := savePEMKey(keyPath, rootKey); err != nil { + return nil, nil, err + } + if err := savePEMCert(certPath, rootCert); err != nil { + return nil, nil, err + } + + return rootKey, rootCert, nil +} + +// Generate a TLS Certificate signed by the Root CA +func generateTLSCert(keyPath, certPath string, rootKey *rsa.PrivateKey, rootCert []byte) error { + // Generate a key for the server + serverKey, err := generateKey() + if err != nil { + return err + } + + // Parse the Root CA certificate + parsedRootCert, err := x509.ParseCertificate(rootCert) + if err != nil { + return err + } + + // Create the server certificate template + serverCertTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + Organization: []string{"YourOrg"}, + CommonName: "localhost", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), // 1 year validity + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + DNSNames: []string{"localhost"}, + } + + // Sign the server certificate with the Root CA + serverCert, err := x509.CreateCertificate(rand.Reader, serverCertTemplate, parsedRootCert, &serverKey.PublicKey, rootKey) + if err != nil { + return err + } + + // Save the server key and certificate + if err := savePEMKey(keyPath, serverKey); err != nil { + return err + } + if err := savePEMCert(certPath, serverCert); err != nil { + return err + } + + return nil +} + +func generateKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, 2048) +} + +func savePEMKey(fileName string, key *rsa.PrivateKey) error { + keyFile, err := os.Create(fileName) + if err != nil { + return err + } + defer keyFile.Close() + + privateKeyPEM := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + return pem.Encode(keyFile, privateKeyPEM) +} + +func savePEMCert(fileName string, cert []byte) error { + certFile, err := os.Create(fileName) + if err != nil { + return err + } + defer certFile.Close() + + certPEM := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + } + + return pem.Encode(certFile, certPEM) +} From 257e958e7a71610fc1fd000e0d591a67e83ab06f Mon Sep 17 00:00:00 2001 From: Clement Doucy Date: Tue, 15 Oct 2024 20:03:40 +0200 Subject: [PATCH 2/7] refactored --- client.go | 183 ++++++++++++++++++++++++++++++++++++++++++-- pem_watcher.go | 124 ------------------------------ pem_watcher_test.go | 36 +++++++-- 3 files changed, 204 insertions(+), 139 deletions(-) delete mode 100644 pem_watcher.go diff --git a/client.go b/client.go index a040263..55935a4 100644 --- a/client.go +++ b/client.go @@ -48,6 +48,10 @@ const ( MethodTrace = "TRACE" ) +const ( + defaultWatcherPoolingInterval = 24 * time.Hour +) + var ( ErrNotHttpTransportType = errors.New("resty: not a http.Transport type") ErrUnsupportedRequestBodyKind = errors.New("resty: unsupported request body kind") @@ -208,6 +212,8 @@ type Client struct { contentTypeDecoders map[string]ContentTypeDecoder contentDecompressorKeys []string contentDecompressors map[string]ContentDecompressor + stopChan chan bool + certLock *sync.Mutex // TODO don't put mutex now, it may go away preReqHook PreRequestHook @@ -1372,9 +1378,12 @@ func (c *Client) SetRootCertificate(pemFilePath string) *Client { // SetRootCertificateWatcher enables dynamic reloading of one or more root certificates. // It is designed for scenarios involving long-running Resty clients where certificates may be renewed. // -// client.SetRootCertificateWatcher(&WatcherOptions{PemFilePath: "root-ca.crt"}) -func (c *Client) SetRootCertificateWatcher(options *WatcherOptions) *Client { - c.handleCAsWatcher("root", options) +// client.SetRootCertificateWatcher("root-ca.crt", &CertWatcherOptions{ +// PoolInterval: time.Hours * 24, +// }) +func (c *Client) SetRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client { + c.SetRootCertificate(pemFilePath) + c.initCertWatcher(pemFilePath, "root", options) return c } @@ -1401,15 +1410,85 @@ func (c *Client) SetClientRootCertificate(pemFilePath string) *Client { return c } -// SetClientRootCertificateWatcher enables dynamic reloading of one or more root certificates. +type CertWatcherOptions struct { + // PoolInterval is the frequency at which resty will check if the PEM file needs to be reloaded. + // Default is 24 hours. + PoolInterval time.Duration +} + +// SetClientRootCertificateWatcher enables dynamic reloading of one or more rclient oot certificates. // It is designed for scenarios involving long-running Resty clients where certificates may be renewed. // -// client.SetClientRootCertificateWatcher(&WatcherOptions{PemFilePath: "root-ca.crt"}) -func (c *Client) SetClientRootCertificateWatcher(options *WatcherOptions) *Client { - c.handleCAsWatcher("client", options) +// client.SetClientRootCertificateWatcher("root-ca.crt", &CertWatcherOptions{ +// PoolInterval: time.Hours * 24, +// }) +func (c *Client) SetClientRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client { + c.SetClientRootCertificate(pemFilePath) + c.initCertWatcher(pemFilePath, "client", options) + return c } +func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcherOptions) { + tickerDuration := defaultWatcherPoolingInterval + if options != nil && options.PoolInterval > 0 { + tickerDuration = options.PoolInterval + } + + go func() { + ticker := time.NewTicker(tickerDuration) + st, err := os.Stat(pemFilePath) + if err != nil { + c.log.Errorf("%v", err) + return + } + + modTime := st.ModTime().UTC() + + for { + select { + case <-c.stopChan: + ticker.Stop() + return + case <-ticker.C: + + c.debugf("Checking if cert %s has changed...", pemFilePath) + + st, err = os.Stat(pemFilePath) + if err != nil { + c.log.Errorf("%v", err) + continue + } + newModTime := st.ModTime().UTC() + + if modTime.Equal(newModTime) { + c.debugf("Cert %s hasn't changed.", pemFilePath) + continue + } + + modTime = newModTime + + c.debugf("Reloading cert %s ...", pemFilePath) + + c.certLock.Lock() + switch scope { + case "root": + c.SetRootCertificate(pemFilePath) + case "client": + c.SetClientRootCertificate(pemFilePath) + } + c.certLock.Unlock() + + c.debugf("Cert %s reloaded.", pemFilePath) + } + } + }() +} + +func (c *Client) Stop() { + close(c.stopChan) +} + // SetClientRootCertificateFromString method helps to add one or more clients // root certificates into the Resty client // @@ -2042,3 +2121,93 @@ func (c *Client) onInvalidHooks(req *Request, err error) { h(req, err) } } + +func (c *Client) debugf(format string, v ...interface{}) { + if !c.Debug { + return + } + + c.log.Debugf(format, v...) +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// File struct and its methods +//_______________________________________________________________________ + +// File struct represents file information for multipart request +type File struct { + Name string + ParamName string + io.Reader +} + +// String method returns the string value of current file details +func (f *File) String() string { + return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name) +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// MultipartField struct +//_______________________________________________________________________ + +// MultipartField struct represents the custom data part for a multipart request +type MultipartField struct { + Param string + FileName string + ContentType string + io.Reader +} + +func createClient(hc *http.Client) *Client { + if hc.Transport == nil { + hc.Transport = createTransport(nil) + } + + c := &Client{ // not setting lang default values + QueryParam: url.Values{}, + FormData: url.Values{}, + Header: http.Header{}, + Cookies: make([]*http.Cookie, 0), + RetryWaitTime: defaultWaitTime, + RetryMaxWaitTime: defaultMaxWaitTime, + PathParams: make(map[string]string), + RawPathParams: make(map[string]string), + JSONMarshal: json.Marshal, + JSONUnmarshal: json.Unmarshal, + XMLMarshal: xml.Marshal, + XMLUnmarshal: xml.Unmarshal, + HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"), + + jsonEscapeHTML: true, + httpClient: hc, + debugBodySizeLimit: math.MaxInt32, + udBeforeRequestLock: &sync.RWMutex{}, + afterResponseLock: &sync.RWMutex{}, + certLock: &sync.Mutex{}, + stopChan: make(chan bool), + } + + // Logger + c.SetLogger(createLogger()) + + // default before request middlewares + c.beforeRequest = []RequestMiddleware{ + parseRequestURL, + parseRequestHeader, + parseRequestBody, + createHTTPRequest, + addCredentials, + createCurlCmd, + } + + // user defined request middlewares + c.udBeforeRequest = []RequestMiddleware{} + + // default after response middlewares + c.afterResponse = []ResponseMiddleware{ + parseResponseBody, + saveResponseIntoFile, + } + + return c +} diff --git a/pem_watcher.go b/pem_watcher.go deleted file mode 100644 index a42dee2..0000000 --- a/pem_watcher.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. -// resty source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package resty - -import ( - "crypto/x509" - "errors" - "os" - "time" -) - -const ( - defaultWatcherPoolingInterval = 1 * time.Minute -) - -// WatcherOptions struct is used to enable TLS Certificate hot reloading. -type WatcherOptions struct { - // PemFilePath is the path of the PEM file - PemFilePath string - - // PoolingInterval is the frequency at which resty will check if the PEM file needs to be reloaded. - // Default is 1 min. - PoolingInterval time.Duration -} - -type pemWatcher struct { - opt *WatcherOptions - - certPool *x509.CertPool - modTime time.Time - lastChecked time.Time - log Logger - debug bool -} - -func newPemWatcher(options *WatcherOptions, log Logger, debug bool) (*pemWatcher, error) { - if options.PemFilePath == "" { - return nil, errors.New("PemFilePath is required") - } - - if options.PoolingInterval == 0 { - options.PoolingInterval = defaultWatcherPoolingInterval - } - - cw := &pemWatcher{ - opt: options, - log: log, - debug: debug, - } - - if err := cw.checkRefresh(); err != nil { - return nil, err - } - - return cw, nil -} - -func (pw *pemWatcher) CertPool() (*x509.CertPool, error) { - if err := pw.checkRefresh(); err != nil { - return nil, err - } - - return pw.certPool, nil -} - -func (pw *pemWatcher) checkRefresh() error { - if time.Since(pw.lastChecked) <= pw.opt.PoolingInterval { - return nil - } - - pw.Debugf("Checking if cert has changed...") - - newModTime, err := pw.getModTime() - if err != nil { - return err - } - - if pw.modTime.Equal(newModTime) { - pw.lastChecked = time.Now().UTC() - pw.Debugf("No change") - return nil - } - - if err := pw.refreshCertPool(); err != nil { - return err - } - - pw.modTime = newModTime - pw.lastChecked = time.Now().UTC() - - pw.Debugf("Cert refreshed") - - return nil -} - -func (pw *pemWatcher) getModTime() (time.Time, error) { - info, err := os.Stat(pw.opt.PemFilePath) - if err != nil { - return time.Time{}, err - } - - return info.ModTime().UTC(), nil -} - -func (pw *pemWatcher) refreshCertPool() error { - pemCert, err := os.ReadFile(pw.opt.PemFilePath) - if err != nil { - return nil - } - - pw.certPool = x509.NewCertPool() - pw.certPool.AppendCertsFromPEM(pemCert) - return nil -} - -func (pw *pemWatcher) Debugf(format string, v ...interface{}) { - if !pw.debug { - return - } - - pw.log.Debugf(format, v...) -} diff --git a/pem_watcher_test.go b/pem_watcher_test.go index b1c2343..9cf06c7 100644 --- a/pem_watcher_test.go +++ b/pem_watcher_test.go @@ -48,15 +48,23 @@ func TestClient_SetRootCertificateWatcher(t *testing.T) { generateCerts(t, paths) startHTTPSServer(fmt.Sprintf(":%d", port), paths) - client := New().SetDebug(true).SetRootCertificateWatcher(&WatcherOptions{ - PemFilePath: paths.RootCACert, - PoolingInterval: time.Second * 1, - }) + //client := New().SetRootCertificate(paths.RootCACert).SetDebug(true) + client := New().SetRootCertificateWatcher(paths.RootCACert, &CertWatcherOptions{ + PoolInterval: time.Second * 1, + }).SetDebug(true) + + tr, err := client.Transport() + if err != nil { + t.Fatal(err) + } + // Make sure that TLS handshake happens for all request + // (otherwise, test may succeed because 1st TLS session is re-used) + tr.DisableKeepAlives = true url := fmt.Sprintf("https://localhost:%d/", port) for i := 0; i < 5; i++ { - time.Sleep(1 * time.Second) + t.Logf("i = %d", i) res, err := client.R().Get(url) if err != nil { t.Fatal(err) @@ -68,7 +76,10 @@ func TestClient_SetRootCertificateWatcher(t *testing.T) { // Re-generate certs to simulate renewal scenario generateCerts(t, paths) } + time.Sleep(time.Second * 1) } + + client.Stop() } func startHTTPSServer(addr string, path certPaths) { @@ -135,9 +146,18 @@ func generateRootCA(keyPath, certPath string) (*rsa.PrivateKey, []byte, error) { return nil, nil, err } + // Define the maximum value you want for the random big integer + max := new(big.Int).Lsh(big.NewInt(1), 256) // Example: 256 bits + + // Generate a random big.Int + randomBigInt, err := rand.Int(rand.Reader, max) + if err != nil { + return nil, nil, err + } + // Create the root certificate template rootCertTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(1), + SerialNumber: randomBigInt, Subject: pkix.Name{ Organization: []string{"YourOrg"}, Country: []string{"US"}, @@ -146,7 +166,7 @@ func generateRootCA(keyPath, certPath string) (*rsa.PrivateKey, []byte, error) { CommonName: "YourRootCA", }, NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), // 10 years validity + NotAfter: time.Now().Add(time.Hour * 10), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, IsCA: true, BasicConstraintsValid: true, @@ -191,7 +211,7 @@ func generateTLSCert(keyPath, certPath string, rootKey *rsa.PrivateKey, rootCert CommonName: "localhost", }, NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), // 1 year validity + NotAfter: time.Now().Add(time.Hour * 10), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, From bf8630ecfd173728193972be5658b995dc6c3f20 Mon Sep 17 00:00:00 2001 From: Clement Doucy Date: Tue, 15 Oct 2024 20:18:46 +0200 Subject: [PATCH 3/7] fixed conflicts --- client.go | 93 ++------------------------------------------- pem_watcher_test.go | 3 +- resty.go | 2 + 3 files changed, 8 insertions(+), 90 deletions(-) diff --git a/client.go b/client.go index 55935a4..e8aca7f 100644 --- a/client.go +++ b/client.go @@ -212,8 +212,8 @@ type Client struct { contentTypeDecoders map[string]ContentTypeDecoder contentDecompressorKeys []string contentDecompressors map[string]ContentDecompressor - stopChan chan bool - certLock *sync.Mutex + stopChan chan bool + certLock *sync.Mutex // TODO don't put mutex now, it may go away preReqHook PreRequestHook @@ -1485,10 +1485,6 @@ func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcher }() } -func (c *Client) Stop() { - close(c.stopChan) -} - // SetClientRootCertificateFromString method helps to add one or more clients // root certificates into the Resty client // @@ -1941,6 +1937,7 @@ func (c *Client) Close() error { if c.LoadBalancer() != nil { silently(c.LoadBalancer().Close()) } + close(c.stopChan) return nil } @@ -2123,91 +2120,9 @@ func (c *Client) onInvalidHooks(req *Request, err error) { } func (c *Client) debugf(format string, v ...interface{}) { - if !c.Debug { + if !c.debug { return } c.log.Debugf(format, v...) } - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// File struct and its methods -//_______________________________________________________________________ - -// File struct represents file information for multipart request -type File struct { - Name string - ParamName string - io.Reader -} - -// String method returns the string value of current file details -func (f *File) String() string { - return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name) -} - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// MultipartField struct -//_______________________________________________________________________ - -// MultipartField struct represents the custom data part for a multipart request -type MultipartField struct { - Param string - FileName string - ContentType string - io.Reader -} - -func createClient(hc *http.Client) *Client { - if hc.Transport == nil { - hc.Transport = createTransport(nil) - } - - c := &Client{ // not setting lang default values - QueryParam: url.Values{}, - FormData: url.Values{}, - Header: http.Header{}, - Cookies: make([]*http.Cookie, 0), - RetryWaitTime: defaultWaitTime, - RetryMaxWaitTime: defaultMaxWaitTime, - PathParams: make(map[string]string), - RawPathParams: make(map[string]string), - JSONMarshal: json.Marshal, - JSONUnmarshal: json.Unmarshal, - XMLMarshal: xml.Marshal, - XMLUnmarshal: xml.Unmarshal, - HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"), - - jsonEscapeHTML: true, - httpClient: hc, - debugBodySizeLimit: math.MaxInt32, - udBeforeRequestLock: &sync.RWMutex{}, - afterResponseLock: &sync.RWMutex{}, - certLock: &sync.Mutex{}, - stopChan: make(chan bool), - } - - // Logger - c.SetLogger(createLogger()) - - // default before request middlewares - c.beforeRequest = []RequestMiddleware{ - parseRequestURL, - parseRequestHeader, - parseRequestBody, - createHTTPRequest, - addCredentials, - createCurlCmd, - } - - // user defined request middlewares - c.udBeforeRequest = []RequestMiddleware{} - - // default after response middlewares - c.afterResponse = []ResponseMiddleware{ - parseResponseBody, - saveResponseIntoFile, - } - - return c -} diff --git a/pem_watcher_test.go b/pem_watcher_test.go index 9cf06c7..a5d2ff0 100644 --- a/pem_watcher_test.go +++ b/pem_watcher_test.go @@ -79,7 +79,8 @@ func TestClient_SetRootCertificateWatcher(t *testing.T) { time.Sleep(time.Second * 1) } - client.Stop() + err = client.Close() + assertNil(t, err) } func startHTTPSServer(addr string, path certPaths) { diff --git a/resty.go b/resty.go index fd65873..f659481 100644 --- a/resty.go +++ b/resty.go @@ -175,6 +175,8 @@ func createClient(hc *http.Client) *Client { contentTypeDecoders: make(map[string]ContentTypeDecoder), contentDecompressorKeys: make([]string, 0), contentDecompressors: make(map[string]ContentDecompressor), + stopChan: make(chan bool), + certLock: &sync.Mutex{}, } // Logger From 61a2f60a429221f2217256afdec3ff264a82d1ca Mon Sep 17 00:00:00 2001 From: Clement Doucy Date: Tue, 15 Oct 2024 20:25:38 +0200 Subject: [PATCH 4/7] comments --- pem_watcher_test.go => cert_watcher_test.go | 0 client.go | 13 +++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) rename pem_watcher_test.go => cert_watcher_test.go (100%) diff --git a/pem_watcher_test.go b/cert_watcher_test.go similarity index 100% rename from pem_watcher_test.go rename to cert_watcher_test.go diff --git a/client.go b/client.go index e8aca7f..f9b64e1 100644 --- a/client.go +++ b/client.go @@ -1377,10 +1377,13 @@ func (c *Client) SetRootCertificate(pemFilePath string) *Client { // SetRootCertificateWatcher 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.Hours * 24, +// PoolInterval: time.Hour * 24, // }) +// +// defer client.Close() func (c *Client) SetRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client { c.SetRootCertificate(pemFilePath) c.initCertWatcher(pemFilePath, "root", options) @@ -1416,12 +1419,14 @@ type CertWatcherOptions struct { PoolInterval time.Duration } -// SetClientRootCertificateWatcher enables dynamic reloading of one or more rclient oot certificates. +// SetClientRootCertificateWatcher 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.Hours * 24, -// }) +// PoolInterval: time.Hour * 24, +// }) +// defer client.Close() func (c *Client) SetClientRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client { c.SetClientRootCertificate(pemFilePath) c.initCertWatcher(pemFilePath, "client", options) From 0e58c65334eed7ae806bca68e972bda78400837f Mon Sep 17 00:00:00 2001 From: Clement Doucy Date: Wed, 16 Oct 2024 19:43:16 +0200 Subject: [PATCH 5/7] removed lock --- client.go | 3 --- resty.go | 1 - 2 files changed, 4 deletions(-) diff --git a/client.go b/client.go index f9b64e1..0c52157 100644 --- a/client.go +++ b/client.go @@ -213,7 +213,6 @@ type Client struct { contentDecompressorKeys []string contentDecompressors map[string]ContentDecompressor stopChan chan bool - certLock *sync.Mutex // TODO don't put mutex now, it may go away preReqHook PreRequestHook @@ -1475,14 +1474,12 @@ func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcher c.debugf("Reloading cert %s ...", pemFilePath) - c.certLock.Lock() switch scope { case "root": c.SetRootCertificate(pemFilePath) case "client": c.SetClientRootCertificate(pemFilePath) } - c.certLock.Unlock() c.debugf("Cert %s reloaded.", pemFilePath) } diff --git a/resty.go b/resty.go index f659481..8375de6 100644 --- a/resty.go +++ b/resty.go @@ -176,7 +176,6 @@ func createClient(hc *http.Client) *Client { contentDecompressorKeys: make([]string, 0), contentDecompressors: make(map[string]ContentDecompressor), stopChan: make(chan bool), - certLock: &sync.Mutex{}, } // Logger From 0959aa0b37ed4ff126d1c3d9ea330d3ba789a619 Mon Sep 17 00:00:00 2001 From: Clement Doucy Date: Sun, 20 Oct 2024 12:27:51 +0200 Subject: [PATCH 6/7] fixed datarace --- client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client.go b/client.go index 0c52157..3b6631f 100644 --- a/client.go +++ b/client.go @@ -2122,6 +2122,8 @@ func (c *Client) onInvalidHooks(req *Request, err error) { } func (c *Client) debugf(format string, v ...interface{}) { + c.lock.RLock() + defer c.lock.RUnlock() if !c.debug { return } From a79d65ffc24f9c46de787fdef08fd4fc6336c319 Mon Sep 17 00:00:00 2001 From: Clement Doucy Date: Sun, 20 Oct 2024 12:39:56 +0200 Subject: [PATCH 7/7] moved otpions struct --- client.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client.go b/client.go index 3b6631f..19d3219 100644 --- a/client.go +++ b/client.go @@ -223,6 +223,13 @@ type User struct { Username, Password string } +// CertWatcherOptions allows configuring a watcher that reloads dynamically TLS certs. +type CertWatcherOptions struct { + // PoolInterval is the frequency at which resty will check if the PEM file needs to be reloaded. + // Default is 24 hours. + PoolInterval time.Duration +} + // Clone method returns deep copy of u. func (u *User) Clone() *User { uu := new(User) @@ -1412,12 +1419,6 @@ func (c *Client) SetClientRootCertificate(pemFilePath string) *Client { return c } -type CertWatcherOptions struct { - // PoolInterval is the frequency at which resty will check if the PEM file needs to be reloaded. - // Default is 24 hours. - PoolInterval time.Duration -} - // SetClientRootCertificateWatcher 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.