package jwk import ( "context" "fmt" "io" "math" "os" "strconv" "sync" "sync/atomic" "github.com/lestrrat-go/httprc" ) type Fetcher interface { Fetch(context.Context, string, ...FetchOption) (Set, error) } type FetchFunc func(context.Context, string, ...FetchOption) (Set, error) func (f FetchFunc) Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) { return f(ctx, u, options...) } var globalFetcher httprc.Fetcher var muGlobalFetcher sync.Mutex var fetcherChanged uint32 func init() { atomic.StoreUint32(&fetcherChanged, 1) } func getGlobalFetcher() httprc.Fetcher { if v := atomic.LoadUint32(&fetcherChanged); v == 0 { return globalFetcher } muGlobalFetcher.Lock() defer muGlobalFetcher.Unlock() if globalFetcher == nil { var nworkers int v := os.Getenv(`JWK_FETCHER_WORKER_COUNT`) if c, err := strconv.ParseInt(v, 10, 64); err == nil { if c > math.MaxInt { nworkers = math.MaxInt } else { nworkers = int(c) } } if nworkers < 1 { nworkers = 3 } globalFetcher = httprc.NewFetcher(context.Background(), httprc.WithFetcherWorkerCount(nworkers)) } atomic.StoreUint32(&fetcherChanged, 0) return globalFetcher } // SetGlobalFetcher allows users to specify a custom global fetcher, // which is used by the `Fetch` function. Assigning `nil` forces // the default fetcher to be (re)created when the next call to // `jwk.Fetch` occurs // // You only need to call this function when you want to // either change the fetching behavior (for example, you want to change // how the default whitelist is handled), or when you want to control // the lifetime of the global fetcher, for example for tests // that require a clean shutdown. // // If you do use this function to set a custom fetcher and you // control its termination, make sure that you call `jwk.SetGlobalFetcher()` // one more time (possibly with `nil`) to assign a valid fetcher. // Otherwise, once the fetcher is invalidated, subsequent calls to `jwk.Fetch` // may hang, causing very hard to debug problems. // // If you are sure you no longer need `jwk.Fetch` after terminating the // fetcher, then you the above caution is not necessary. func SetGlobalFetcher(f httprc.Fetcher) { muGlobalFetcher.Lock() globalFetcher = f muGlobalFetcher.Unlock() atomic.StoreUint32(&fetcherChanged, 1) } // Fetch fetches a JWK resource specified by a URL. The url must be // pointing to a resource that is supported by `net/http`. // // If you are using the same `jwk.Set` for long periods of time during // the lifecycle of your program, and would like to periodically refresh the // contents of the object with the data at the remote resource, // consider using `jwk.Cache`, which automatically refreshes // jwk.Set objects asynchronously. // // Please note that underneath the `jwk.Fetch` function, it uses a global // object that spawns goroutines that are present until the go runtime // exits. Initially this global variable is uninitialized, but upon // calling `jwk.Fetch` once, it is initialized and goroutines are spawned. // If you want to control the lifetime of these goroutines, you can // call `jwk.SetGlobalFetcher` with a custom fetcher which is tied to // a `context.Context` object that you can control. func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) { var hrfopts []httprc.FetchOption var parseOptions []ParseOption for _, option := range options { if parseOpt, ok := option.(ParseOption); ok { parseOptions = append(parseOptions, parseOpt) continue } //nolint:forcetypeassert switch option.Ident() { case identHTTPClient{}: hrfopts = append(hrfopts, httprc.WithHTTPClient(option.Value().(HTTPClient))) case identFetchWhitelist{}: hrfopts = append(hrfopts, httprc.WithWhitelist(option.Value().(httprc.Whitelist))) } } res, err := getGlobalFetcher().Fetch(ctx, u, hrfopts...) if err != nil { return nil, fmt.Errorf(`failed to fetch %q: %w`, u, err) } buf, err := io.ReadAll(res.Body) defer res.Body.Close() if err != nil { return nil, fmt.Errorf(`failed to read response body for %q: %w`, u, err) } return Parse(buf, parseOptions...) }