go-resty / resty

Simple HTTP and REST client library for Go
MIT License
10.01k stars 706 forks source link

feat: Implemented ability to dynamically reload TLS certificates #866

Open cdoucy opened 3 weeks ago

cdoucy commented 3 weeks ago

Fixes https://github.com/go-resty/resty/issues/856

cdoucy commented 3 weeks ago

Hey @jeevatkm !

As discussed, here is the PR.

If you are happy with the changes, I can update doc.

cdoucy commented 3 weeks ago

Hi @jeevatkm thanks for the feedback!

Make use of https://pkg.go.dev/time#Ticker to execute the function to reload based on a timer

yes, but that means that ticker.Stop() needs to be called at some point. For this, SetRootCertificateWatcher may accept a context.Context, and ticker.Stop() will be called when the context is called. What do you think?

keep the loading directly into the client instance TLS config

Note sure to understand what you mean here. To avoid using OnBeforeRequest, I can implement an http.RoundTripper that would get the refreshed Certificate and pass it to the TLSClientConfig of the underlying http.Transporter. What do you think of this approach?

P.S. : I wish I could leverage tls.Config.GetCertificate, but it only works on the server side.

jeevatkm commented 3 weeks ago

@cdoucy I'm not 100% sure, do we really need a context for this use case?

I may try to explain an approach based on what I learned from your PR and currently what is coming to my mind. This is not perfect; I am just dumping my thoughts.

type CertWatcherOptions struct {
    // Default interval is 24 hours
    PoolInterval time.Duration
}

func (c *Client) SetRootCertificateWatcher(certPath string, options *CertWatcherOptions) *Client {
    c.SetRootCertificate(certPath)
    c.initRootCertWatcher(options)
    return c
}

func (c *Client) SetClientRootCertificateWatcher(certPath string, options *CertWatcherOptions) *Client {
    c.SetClientRootCertificate(certPath)
    c.initClientRootCertWatcher(options)
    return c
}

func (c *Client) initRootCertWatcher() {
    c.rootCAWatcher = &pemWatcher{
        // ...
    }

    // ...
}

func (c *Client) initClientRootCertWatcher() {
    c.clientRootCAWatcher = &pemWatcher{
        // ...
    }

    // ...  
}

So, that user could call SetRootCertificateWatcher method to set N number cert files, and those get added to the watch list ... Let's assume there is a method c.refreshRootCerts() and c.refreshClientRootCerts() gets triggered by time.Ticker and reloads the certs into c.tlsConfig().RootCAs and c.tlsConfig().ClientCAs, respectively.

We can add a method to the Client Stop cert watcher ticker and add documentation for users to call that method from their shutdown hook scenarios.

What do you think?

jeevatkm commented 1 week ago

@cdoucy, I thought I would check with you about the last comment.

cdoucy commented 1 week ago

Hello @jeevatkm,

Thanks for your feedback! Sorry for the delayed response. Your approach makes sense to me. If a Stop() method can be introduced in the client, then we don't need a context.

I'll work on applying your suggestion.

jeevatkm commented 4 days ago

@cdoucy, Can you please implement it against the main branch for Resty v3? I plan to introduce the method Close at the client level to tackle all the channels, tickers, etc. Clean-up could be done on that.

Then the user would do something like this v3 onwards -

client := resty.New()
defer client.Close()
...
cdoucy commented 4 days ago

Hi @jeevatkm , sure! I'll work on this over the weekend.