#205: Work on supporting OpenID Connect for auth
This commit is contained in:
1
vendor/github.com/TwiN/g8/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/TwiN/g8/.gitattributes
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=lf
|
2
vendor/github.com/TwiN/g8/.gitignore
generated
vendored
Normal file
2
vendor/github.com/TwiN/g8/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.idea
|
||||
*.iml
|
9
vendor/github.com/TwiN/g8/LICENSE.md
generated
vendored
Normal file
9
vendor/github.com/TwiN/g8/LICENSE.md
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 TwiN
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
238
vendor/github.com/TwiN/g8/README.md
generated
vendored
Normal file
238
vendor/github.com/TwiN/g8/README.md
generated
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
# g8
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/TwiN/g8)
|
||||
[](https://codecov.io/gh/TwiN/g8)
|
||||
[](https://github.com/TwiN/g8)
|
||||
[](https://pkg.go.dev/github.com/TwiN/g8)
|
||||
[](https://github.com/TwiN)
|
||||
|
||||
g8, pronounced gate, is a simple Go library for protecting HTTP handlers.
|
||||
|
||||
Tired of constantly re-implementing a security layer for each application? Me too, that's why I made g8.
|
||||
|
||||
|
||||
## Installation
|
||||
```console
|
||||
go get -u github.com/TwiN/g8
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
Because the entire purpose of g8 is to NOT waste time configuring the layer of security, the primary emphasis is to
|
||||
keep it as simple as possible.
|
||||
|
||||
|
||||
### Simple
|
||||
Just want a simple layer of security without the need for advanced permissions? This configuration is what you're
|
||||
looking for.
|
||||
|
||||
```go
|
||||
authorizationService := g8.NewAuthorizationService().WithToken("mytoken")
|
||||
gate := g8.New().WithAuthorizationService(authorizationService)
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/unprotected", yourHandler)
|
||||
router.Handle("/protected", gate.Protect(yourHandler))
|
||||
|
||||
http.ListenAndServe(":8080", router)
|
||||
```
|
||||
|
||||
The endpoint `/protected` is now only accessible if you pass the header `Authorization: Bearer mytoken`.
|
||||
|
||||
If you use `http.HandleFunc` instead of `http.Handle`, you may use `gate.ProtectFunc(yourHandler)` instead.
|
||||
|
||||
If you're not using the `Authorization` header, you can specify a custom token extractor.
|
||||
This enables use cases like [Protecting a handler using session cookie](#protecting-a-handler-using-session-cookie)
|
||||
|
||||
|
||||
### Advanced permissions
|
||||
If you have tokens with more permissions than others, g8's permission system will make managing authorization a breeze.
|
||||
|
||||
Rather than registering tokens, think of it as registering clients, the only difference being that clients may be
|
||||
configured with permissions while tokens cannot.
|
||||
|
||||
```go
|
||||
authorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermission("admin"))
|
||||
gate := g8.New().WithAuthorizationService(authorizationService)
|
||||
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/unprotected", yourHandler)
|
||||
router.Handle("/protected-with-admin", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
|
||||
|
||||
http.ListenAndServe(":8080", router)
|
||||
```
|
||||
|
||||
The endpoint `/protected-with-admin` is now only accessible if you pass the header `Authorization: Bearer mytoken`,
|
||||
because the client with the token `mytoken` has the permission `admin`. Note that the following handler would also be
|
||||
accessible with that token:
|
||||
```go
|
||||
router.Handle("/protected", gate.Protect(yourHandler))
|
||||
```
|
||||
|
||||
To clarify, both clients and tokens have access to handlers that aren't protected with extra permissions, and
|
||||
essentially, tokens are registered as clients with no extra permissions in the background.
|
||||
|
||||
Creating a token like so:
|
||||
```go
|
||||
authorizationService := g8.NewAuthorizationService().WithToken("mytoken")
|
||||
```
|
||||
is the equivalent of creating the following client:
|
||||
```go
|
||||
authorizationService := g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken"))
|
||||
```
|
||||
|
||||
|
||||
### With client provider
|
||||
A client provider's task is to retrieve a Client from an external source (e.g. a database) when provided with a token.
|
||||
You should use a client provider when you have a lot of tokens and it wouldn't make sense to register all of them using
|
||||
`AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`.
|
||||
|
||||
Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4
|
||||
aforementioned functions, the client provider will not be used.
|
||||
|
||||
```go
|
||||
clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
|
||||
// We'll assume that the following function calls your database and returns a struct "User" that
|
||||
// has the user's token as well as the permissions granted to said user
|
||||
user := database.GetUserByToken(token)
|
||||
if user != nil {
|
||||
return g8.NewClient(user.Token).WithPermissions(user.Permissions)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
|
||||
gate := g8.New().WithAuthorizationService(authorizationService)
|
||||
```
|
||||
|
||||
You can also configure the client provider to cache the output of the function you provide to retrieve clients by token:
|
||||
```go
|
||||
clientProvider := g8.NewClientProvider(...).WithCache(ttl, maxSize)
|
||||
```
|
||||
|
||||
Since g8 leverages [TwiN/gocache](https://github.com/TwiN/gocache), you can also use gocache's
|
||||
constants for configuring the TTL and the maximum size:
|
||||
- Setting the TTL to `gocache.NoExpiration` (-1) will disable the TTL.
|
||||
- Setting the maximum size to `gocache.NoMaxSize` (0) will disable the maximum cache size
|
||||
|
||||
If you're using a TTL and have a lot of tokens (100k+), you may want to use `clientProvider.StartJanitor()` to allow
|
||||
the cache to passively delete expired entries. If you have to re-initialize the client provider after the janitor has
|
||||
been started, make sure to stop the janitor first (`clientProvider.StopJanitor()`). This is because the janitor runs on
|
||||
a separate goroutine, thus, if you were to re-create a client provider and re-assign it, the old client provider would
|
||||
still exist in memory with the old cache. I'm only specifying this for completeness, because for the overwhelming
|
||||
majority of people, the gate will be created on application start and never modified again until the application shuts
|
||||
down, in which case, you don't even need to worry about stopping the janitor.
|
||||
|
||||
To avoid any misunderstandings, using a client provider is not mandatory. If you only have a few tokens and you can load
|
||||
them on application start, you can just leverage `AuthorizationService`'s `WithToken`/`WithTokens`/`WithClient`/`WithClients`.
|
||||
|
||||
|
||||
## AuthorizationService
|
||||
As the previous examples may have hinted, there are several ways to create clients. The one thing they have
|
||||
in common is that they all go through AuthorizationService, which is in charge of both managing clients and determining
|
||||
whether a request should be blocked or allowed through.
|
||||
|
||||
| Function | Description |
|
||||
|:-------------------|:---------------------------------------------------------------------------------------------------------------------------------|
|
||||
| WithToken | Creates a single static client with no extra permissions |
|
||||
| WithTokens | Creates a slice of static clients with no extra permissions |
|
||||
| WithClient | Creates a single static client |
|
||||
| WithClients | Creates a slice of static clients |
|
||||
| WithClientProvider | Creates a client provider which will allow a fallback to a dynamic source (e.g. to a database) when a static client is not found |
|
||||
|
||||
Except for `WithClientProvider`, every functions listed above can be called more than once.
|
||||
As a result, you may safely perform actions like this:
|
||||
```go
|
||||
authorizationService := g8.NewAuthorizationService().
|
||||
WithToken("123").
|
||||
WithToken("456").
|
||||
WithClient(g8.NewClient("789").WithPermission("admin"))
|
||||
gate := g8.New().WithAuthorizationService(authorizationService)
|
||||
```
|
||||
|
||||
Be aware that g8.Client supports a list of permissions as well. You may call `WithPermission` several times, or call
|
||||
`WithPermissions` with a slice of permissions instead.
|
||||
|
||||
|
||||
### Permissions
|
||||
Unlike client permissions, handler permissions are requirements.
|
||||
|
||||
A client may have as many permissions as you want, but for said client to have access to a handler protected by
|
||||
permissions, the client must have all permissions defined by said handler in order to have access to it.
|
||||
|
||||
In other words, a client with the permissions `create`, `read`, `update` and `delete` would have access to all of these handlers:
|
||||
```go
|
||||
gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("mytoken").WithPermissions([]string{"create", "read", "update", "delete"})))
|
||||
router := http.NewServeMux()
|
||||
router.Handle("/", gate.Protect(homeHandler)) // equivalent of gate.ProtectWithPermissions(homeHandler, []string{})
|
||||
router.Handle("/create", gate.ProtectWithPermissions(createHandler, []string{"create"}))
|
||||
router.Handle("/read", gate.ProtectWithPermissions(readHandler, []string{"read"}))
|
||||
router.Handle("/update", gate.ProtectWithPermissions(updateHandler, []string{"update"}))
|
||||
router.Handle("/delete", gate.ProtectWithPermissions(deleteHandler, []string{"delete"}))
|
||||
router.Handle("/crud", gate.ProtectWithPermissions(crudHandler, []string{"create", "read", "update", "delete"}))
|
||||
```
|
||||
But it would not have access to the following handler, because while `mytoken` has the `read` permission, it does not
|
||||
have the `backup` permission:
|
||||
```go
|
||||
router.Handle("/backup", gate.ProtectWithPermissions(&testHandler{}, []string{"read", "backup"}))
|
||||
```
|
||||
|
||||
## Rate limiting
|
||||
To add a rate limit of 100 requests per second:
|
||||
```
|
||||
gate := g8.New().WithRateLimit(100)
|
||||
```
|
||||
|
||||
## Special use cases
|
||||
### Protecting a handler using session cookie
|
||||
If you want to only allow authenticated users to access a handler, you can use a custom token extractor function
|
||||
combined with a client provider.
|
||||
|
||||
First, we'll create a function to extract the session ID from the session cookie. While a session ID does not
|
||||
theoretically refer to a token, g8 uses the term `token` as a blanket term to refer to any string that can be used to
|
||||
identify a client.
|
||||
```go
|
||||
customTokenExtractorFunc := func(request *http.Request) string {
|
||||
sessionCookie, err := request.Cookie("session")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return sessionCookie.Value
|
||||
}
|
||||
```
|
||||
|
||||
Next, we need to create a client provider that will validate our token, which refers to the session ID in this case.
|
||||
```go
|
||||
clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
|
||||
// We'll assume that the following function calls your database and validates whether the session is valid.
|
||||
isSessionValid := database.CheckIfSessionIsValid(token)
|
||||
if !isSessionValid {
|
||||
return nil // Returning nil will cause the gate to return a 401 Unauthorized.
|
||||
}
|
||||
// You could also retrieve the user and their permissions if you wanted instead, but for this example,
|
||||
// all we care about is confirming whether the session is valid or not.
|
||||
return g8.NewClient(token)
|
||||
})
|
||||
```
|
||||
|
||||
Keep in mind that you can get really creative with the client provider above.
|
||||
For instance, you could refresh the session's expiration time, which will allow the user to stay logged in for
|
||||
as long as they're active.
|
||||
|
||||
You're also not limited to using something stateful like the example above. You could use a JWT and have your client
|
||||
provider validate said JWT.
|
||||
|
||||
Finally, we can create the authorization service and the gate:
|
||||
```go
|
||||
authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
|
||||
gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
|
||||
```
|
||||
|
||||
If you need to access the token (session ID in this case) from the protected handlers, you can retrieve it from the
|
||||
request context by using the key `g8.TokenContextKey`:
|
||||
```go
|
||||
http.Handle("/handle", gate.ProtectFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID, _ := r.Context().Value(g8.TokenContextKey).(string)
|
||||
// ...
|
||||
}))
|
||||
```
|
122
vendor/github.com/TwiN/g8/authorization.go
generated
vendored
Normal file
122
vendor/github.com/TwiN/g8/authorization.go
generated
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
package g8
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// AuthorizationService is the service that manages client/token registry and client fallback as well as the service
|
||||
// that determines whether a token meets the specific requirements to be authorized by a Gate or not.
|
||||
type AuthorizationService struct {
|
||||
clients map[string]*Client
|
||||
clientProvider *ClientProvider
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewAuthorizationService creates a new AuthorizationService
|
||||
func NewAuthorizationService() *AuthorizationService {
|
||||
return &AuthorizationService{
|
||||
clients: make(map[string]*Client),
|
||||
}
|
||||
}
|
||||
|
||||
// WithToken is used to specify a single token for which authorization will be granted
|
||||
//
|
||||
// The client that will be created from this token will have access to all handlers that are not protected with a
|
||||
// specific permission.
|
||||
//
|
||||
// In other words, if you were to do the following:
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("12345"))
|
||||
//
|
||||
// The following handler would be accessible with the token 12345:
|
||||
// router.Handle("/1st-handler", gate.Protect(yourHandler))
|
||||
//
|
||||
// But not this one would not be accessible with the token 12345:
|
||||
// router.Handle("/2nd-handler", gate.ProtectWithPermissions(yourOtherHandler, []string{"admin"}))
|
||||
//
|
||||
// Calling this function multiple times will add multiple clients, though you may want to use WithTokens instead
|
||||
// if you plan to add multiple clients
|
||||
//
|
||||
// If you wish to configure advanced permissions, consider using WithClient instead.
|
||||
//
|
||||
func (authorizationService *AuthorizationService) WithToken(token string) *AuthorizationService {
|
||||
authorizationService.mutex.Lock()
|
||||
authorizationService.clients[token] = NewClient(token)
|
||||
authorizationService.mutex.Unlock()
|
||||
return authorizationService
|
||||
}
|
||||
|
||||
// WithTokens is used to specify a slice of tokens for which authorization will be granted
|
||||
func (authorizationService *AuthorizationService) WithTokens(tokens []string) *AuthorizationService {
|
||||
authorizationService.mutex.Lock()
|
||||
for _, token := range tokens {
|
||||
authorizationService.clients[token] = NewClient(token)
|
||||
}
|
||||
authorizationService.mutex.Unlock()
|
||||
return authorizationService
|
||||
}
|
||||
|
||||
// WithClient is used to specify a single client for which authorization will be granted
|
||||
//
|
||||
// When compared to WithToken, the advantage of using this function is that you may specify the client's
|
||||
// permissions and thus, be a lot more granular with what endpoint a token has access to.
|
||||
//
|
||||
// In other words, if you were to do the following:
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("12345").WithPermission("mod")))
|
||||
//
|
||||
// The following handlers would be accessible with the token 12345:
|
||||
// router.Handle("/1st-handler", gate.ProtectWithPermissions(yourHandler, []string{"mod"}))
|
||||
// router.Handle("/2nd-handler", gate.Protect(yourOtherHandler))
|
||||
//
|
||||
// But not this one, because the user does not have the permission "admin":
|
||||
// router.Handle("/3rd-handler", gate.ProtectWithPermissions(yetAnotherHandler, []string{"admin"}))
|
||||
//
|
||||
// Calling this function multiple times will add multiple clients, though you may want to use WithClients instead
|
||||
// if you plan to add multiple clients
|
||||
func (authorizationService *AuthorizationService) WithClient(client *Client) *AuthorizationService {
|
||||
authorizationService.mutex.Lock()
|
||||
authorizationService.clients[client.Token] = client
|
||||
authorizationService.mutex.Unlock()
|
||||
return authorizationService
|
||||
}
|
||||
|
||||
// WithClients is used to specify a slice of clients for which authorization will be granted
|
||||
func (authorizationService *AuthorizationService) WithClients(clients []*Client) *AuthorizationService {
|
||||
authorizationService.mutex.Lock()
|
||||
for _, client := range clients {
|
||||
authorizationService.clients[client.Token] = client
|
||||
}
|
||||
authorizationService.mutex.Unlock()
|
||||
return authorizationService
|
||||
}
|
||||
|
||||
// WithClientProvider allows specifying a custom provider to fetch clients by token.
|
||||
//
|
||||
// For example, you can use it to fallback to making a call in your database when a request is made with a token that
|
||||
// hasn't been specified via WithToken, WithTokens, WithClient or WithClients.
|
||||
func (authorizationService *AuthorizationService) WithClientProvider(provider *ClientProvider) *AuthorizationService {
|
||||
authorizationService.clientProvider = provider
|
||||
return authorizationService
|
||||
}
|
||||
|
||||
// IsAuthorized checks whether a client with a given token exists and has the permissions required.
|
||||
//
|
||||
// If permissionsRequired is nil or empty and a client with the given token exists, said client will have access to all
|
||||
// handlers that are not protected by a given permission.
|
||||
func (authorizationService *AuthorizationService) IsAuthorized(token string, permissionsRequired []string) bool {
|
||||
if len(token) == 0 {
|
||||
return false
|
||||
}
|
||||
authorizationService.mutex.RLock()
|
||||
client, _ := authorizationService.clients[token]
|
||||
authorizationService.mutex.RUnlock()
|
||||
// If there's no clients with the given token directly stored in the AuthorizationService, fall back to the
|
||||
// client provider, if there's one configured.
|
||||
if client == nil && authorizationService.clientProvider != nil {
|
||||
client = authorizationService.clientProvider.GetClientByToken(token)
|
||||
}
|
||||
if client != nil {
|
||||
return client.HasPermissions(permissionsRequired)
|
||||
}
|
||||
return false
|
||||
}
|
58
vendor/github.com/TwiN/g8/client.go
generated
vendored
Normal file
58
vendor/github.com/TwiN/g8/client.go
generated
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
package g8
|
||||
|
||||
// Client is a struct containing both a Token and a slice of extra Permissions that said token has.
|
||||
type Client struct {
|
||||
// Token is the value used to authenticate with the API.
|
||||
Token string
|
||||
|
||||
// Permissions is a slice of extra permissions that may be used for more granular access control.
|
||||
//
|
||||
// If you only wish to use Gate.Protect and Gate.ProtectFunc, you do not have to worry about this,
|
||||
// since they're only used by Gate.ProtectWithPermissions and Gate.ProtectFuncWithPermissions
|
||||
Permissions []string
|
||||
}
|
||||
|
||||
// NewClient creates a Client with a given token
|
||||
func NewClient(token string) *Client {
|
||||
return &Client{
|
||||
Token: token,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWithPermissions creates a Client with a slice of permissions
|
||||
// Equivalent to using NewClient and WithPermissions
|
||||
func NewClientWithPermissions(token string, permissions []string) *Client {
|
||||
return NewClient(token).WithPermissions(permissions)
|
||||
}
|
||||
|
||||
// WithPermissions adds a slice of permissions to a client
|
||||
func (client *Client) WithPermissions(permissions []string) *Client {
|
||||
client.Permissions = append(client.Permissions, permissions...)
|
||||
return client
|
||||
}
|
||||
|
||||
// WithPermission adds a permission to a client
|
||||
func (client *Client) WithPermission(permission string) *Client {
|
||||
client.Permissions = append(client.Permissions, permission)
|
||||
return client
|
||||
}
|
||||
|
||||
// HasPermission checks whether a client has a given permission
|
||||
func (client Client) HasPermission(permissionRequired string) bool {
|
||||
for _, permission := range client.Permissions {
|
||||
if permissionRequired == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPermissions checks whether a client has the all permissions passed
|
||||
func (client Client) HasPermissions(permissionsRequired []string) bool {
|
||||
for _, permissionRequired := range permissionsRequired {
|
||||
if !client.HasPermission(permissionRequired) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
140
vendor/github.com/TwiN/g8/clientprovider.go
generated
vendored
Normal file
140
vendor/github.com/TwiN/g8/clientprovider.go
generated
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
package g8
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gocache/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoExpiration is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start the
|
||||
// janitor despite no expiration being configured.
|
||||
// To clarify, this is because the cache janitor is only useful when an expiration is set.
|
||||
ErrNoExpiration = errors.New("no point starting the janitor if the TTL is set to not expire")
|
||||
|
||||
// ErrCacheNotInitialized is the error returned by ClientProvider.StartCacheJanitor if there was an attempt to start
|
||||
// the janitor despite the cache not having been initialized using ClientProvider.WithCache
|
||||
ErrCacheNotInitialized = errors.New("cannot start janitor because cache is not configured")
|
||||
)
|
||||
|
||||
// ClientProvider has the task of retrieving a Client from an external source (e.g. a database) when provided with a
|
||||
// token. It should be used when you have a lot of tokens, and it wouldn't make sense to register all of them using
|
||||
// AuthorizationService's WithToken, WithTokens, WithClient or WithClients.
|
||||
//
|
||||
// Note that the provider is used as a fallback source. As such, if a token is explicitly registered using one of the 4
|
||||
// aforementioned functions, the client provider will not be used by the AuthorizationService when a request is made
|
||||
// with said token. It will, however, be called upon if a token that is not explicitly registered in
|
||||
// AuthorizationService is sent alongside a request going through the Gate.
|
||||
//
|
||||
// clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
|
||||
// // We'll assume that the following function calls your database and returns a struct "User" that
|
||||
// // has the user's token as well as the permissions granted to said user
|
||||
// user := database.GetUserByToken(token)
|
||||
// if user != nil {
|
||||
// return g8.NewClient(user.Token).WithPermissions(user.Permissions)
|
||||
// }
|
||||
// return nil
|
||||
// })
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider))
|
||||
//
|
||||
type ClientProvider struct {
|
||||
getClientByTokenFunc func(token string) *Client
|
||||
|
||||
cache *gocache.Cache
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewClientProvider creates a ClientProvider
|
||||
// The parameter that must be passed is a function that the provider will use to retrieve a client by a given token
|
||||
//
|
||||
// Example:
|
||||
// clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
|
||||
// // We'll assume that the following function calls your database and returns a struct "User" that
|
||||
// // has the user's token as well as the permissions granted to said user
|
||||
// user := database.GetUserByToken(token)
|
||||
// if user == nil {
|
||||
// return nil
|
||||
// }
|
||||
// return g8.NewClient(user.Token).WithPermissions(user.Permissions)
|
||||
// })
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider))
|
||||
//
|
||||
func NewClientProvider(getClientByTokenFunc func(token string) *Client) *ClientProvider {
|
||||
return &ClientProvider{
|
||||
getClientByTokenFunc: getClientByTokenFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// WithCache adds cache options to the ClientProvider.
|
||||
//
|
||||
// ttl is the time until the cache entry will expire. A TTL of gocache.NoExpiration (-1) means no expiration
|
||||
// maxSize is the maximum amount of entries that can be in the cache at any given time.
|
||||
// If a value of gocache.NoMaxSize (0) or less is provided for maxSize, there will be no maximum size.
|
||||
//
|
||||
// Example:
|
||||
// clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
|
||||
// // We'll assume that the following function calls your database and returns a struct "User" that
|
||||
// // has the user's token as well as the permissions granted to said user
|
||||
// user := database.GetUserByToken(token)
|
||||
// if user != nil {
|
||||
// return g8.NewClient(user.Token).WithPermissions(user.Permissions)
|
||||
// }
|
||||
// return nil
|
||||
// })
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClientProvider(clientProvider.WithCache(time.Hour, 70000)))
|
||||
//
|
||||
func (provider *ClientProvider) WithCache(ttl time.Duration, maxSize int) *ClientProvider {
|
||||
provider.cache = gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed).WithMaxSize(maxSize)
|
||||
provider.ttl = ttl
|
||||
return provider
|
||||
}
|
||||
|
||||
// StartCacheJanitor starts the cache janitor, which passively deletes expired cache entries in the background.
|
||||
//
|
||||
// Not really necessary unless you have a lot of clients (100000+).
|
||||
//
|
||||
// Even without the janitor, active eviction will still happen (i.e. when GetClientByToken is called, but the cache
|
||||
// entry for the given token has expired, the cache entry will be automatically deleted and re-fetched from the
|
||||
// user-defined getClientByTokenFunc)
|
||||
func (provider *ClientProvider) StartCacheJanitor() error {
|
||||
if provider.cache == nil {
|
||||
// Can't start the cache janitor if there's no cache
|
||||
return ErrCacheNotInitialized
|
||||
}
|
||||
if provider.ttl != gocache.NoExpiration {
|
||||
return provider.cache.StartJanitor()
|
||||
}
|
||||
return ErrNoExpiration
|
||||
}
|
||||
|
||||
// StopCacheJanitor stops the cache janitor
|
||||
//
|
||||
// Not required unless your application initializes multiple providers over the course of its lifecycle.
|
||||
// In English, that means if you initialize a ClientProvider only once on application start and it stays up
|
||||
// until your application shuts down, you don't need to call this function.
|
||||
func (provider *ClientProvider) StopCacheJanitor() {
|
||||
if provider.cache != nil {
|
||||
provider.cache.StopJanitor()
|
||||
}
|
||||
}
|
||||
|
||||
// GetClientByToken retrieves a client by its token through the provided getClientByTokenFunc.
|
||||
func (provider *ClientProvider) GetClientByToken(token string) *Client {
|
||||
if provider.cache == nil {
|
||||
return provider.getClientByTokenFunc(token)
|
||||
}
|
||||
if cachedClient, exists := provider.cache.Get(token); exists {
|
||||
if cachedClient == nil {
|
||||
return nil
|
||||
}
|
||||
// Safely typecast the client.
|
||||
// Regardless of whether the typecast is successful or not, we return client since it'll be either client or
|
||||
// nil. Technically, it should never be nil, but it's better to be safe than sorry.
|
||||
client, _ := cachedClient.(*Client)
|
||||
return client
|
||||
}
|
||||
client := provider.getClientByTokenFunc(token)
|
||||
provider.cache.SetWithTTL(token, client, provider.ttl)
|
||||
return client
|
||||
}
|
212
vendor/github.com/TwiN/g8/gate.go
generated
vendored
Normal file
212
vendor/github.com/TwiN/g8/gate.go
generated
vendored
Normal file
@ -0,0 +1,212 @@
|
||||
package g8
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// AuthorizationHeader is the header in which g8 looks for the authorization bearer token
|
||||
AuthorizationHeader = "Authorization"
|
||||
|
||||
// DefaultUnauthorizedResponseBody is the default response body returned if a request was sent with a missing or invalid token
|
||||
DefaultUnauthorizedResponseBody = "token is missing or invalid"
|
||||
|
||||
// DefaultTooManyRequestsResponseBody is the default response body returned if a request exceeded the allowed rate limit
|
||||
DefaultTooManyRequestsResponseBody = "too many requests"
|
||||
|
||||
// TokenContextKey is the key used to store the token in the context.
|
||||
TokenContextKey = "g8.token"
|
||||
)
|
||||
|
||||
// Gate is lock to the front door of your API, letting only those you allow through.
|
||||
type Gate struct {
|
||||
authorizationService *AuthorizationService
|
||||
unauthorizedResponseBody []byte
|
||||
|
||||
customTokenExtractorFunc func(request *http.Request) string
|
||||
|
||||
rateLimiter *RateLimiter
|
||||
tooManyRequestsResponseBody []byte
|
||||
}
|
||||
|
||||
// Deprecated: use New instead.
|
||||
func NewGate(authorizationService *AuthorizationService) *Gate {
|
||||
return &Gate{
|
||||
authorizationService: authorizationService,
|
||||
unauthorizedResponseBody: []byte(DefaultUnauthorizedResponseBody),
|
||||
tooManyRequestsResponseBody: []byte(DefaultTooManyRequestsResponseBody),
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Gate.
|
||||
func New() *Gate {
|
||||
return &Gate{
|
||||
unauthorizedResponseBody: []byte(DefaultUnauthorizedResponseBody),
|
||||
tooManyRequestsResponseBody: []byte(DefaultTooManyRequestsResponseBody),
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuthorizationService sets the authorization service to use.
|
||||
//
|
||||
// If there is no authorization service, Gate will not enforce authorization.
|
||||
func (gate *Gate) WithAuthorizationService(authorizationService *AuthorizationService) *Gate {
|
||||
gate.authorizationService = authorizationService
|
||||
return gate
|
||||
}
|
||||
|
||||
// WithCustomUnauthorizedResponseBody sets a custom response body when Gate determines that a request must be blocked
|
||||
func (gate *Gate) WithCustomUnauthorizedResponseBody(unauthorizedResponseBody []byte) *Gate {
|
||||
gate.unauthorizedResponseBody = unauthorizedResponseBody
|
||||
return gate
|
||||
}
|
||||
|
||||
// WithCustomTokenExtractor allows the specification of a custom function to extract a token from a request.
|
||||
// If a custom token extractor is not specified, the token will be extracted from the Authorization header.
|
||||
//
|
||||
// For instance, if you're using a session cookie, you can extract the token from the cookie like so:
|
||||
// authorizationService := g8.NewAuthorizationService()
|
||||
// customTokenExtractorFunc := func(request *http.Request) string {
|
||||
// sessionCookie, err := request.Cookie("session")
|
||||
// if err != nil {
|
||||
// return ""
|
||||
// }
|
||||
// return sessionCookie.Value
|
||||
// }
|
||||
// gate := g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
|
||||
//
|
||||
// You would normally use this with a client provider that matches whatever need you have.
|
||||
// For example, if you're using a session cookie, your client provider would retrieve the user from the session ID
|
||||
// extracted by this custom token extractor.
|
||||
//
|
||||
// Note that for the sake of convenience, the token extracted from the request is passed the protected handlers request
|
||||
// context under the key TokenContextKey. This is especially useful if the token is in fact a session ID.
|
||||
func (gate *Gate) WithCustomTokenExtractor(customTokenExtractorFunc func(request *http.Request) string) *Gate {
|
||||
gate.customTokenExtractorFunc = customTokenExtractorFunc
|
||||
return gate
|
||||
}
|
||||
|
||||
// WithRateLimit adds rate limiting to the Gate
|
||||
//
|
||||
// If you just want to use a gate for rate limiting purposes:
|
||||
// gate := g8.New().WithRateLimit(50)
|
||||
//
|
||||
func (gate *Gate) WithRateLimit(maximumRequestsPerSecond int) *Gate {
|
||||
gate.rateLimiter = NewRateLimiter(maximumRequestsPerSecond)
|
||||
return gate
|
||||
}
|
||||
|
||||
// Protect secures a handler, requiring requests going through to have a valid Authorization Bearer token.
|
||||
// Unlike ProtectWithPermissions, Protect will allow access to any registered tokens, regardless of their permissions
|
||||
// or lack thereof.
|
||||
//
|
||||
// Example:
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
|
||||
// router := http.NewServeMux()
|
||||
// // Without protection
|
||||
// router.Handle("/handle", yourHandler)
|
||||
// // With protection
|
||||
// router.Handle("/handle", gate.Protect(yourHandler))
|
||||
//
|
||||
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
|
||||
func (gate *Gate) Protect(handler http.Handler) http.Handler {
|
||||
return gate.ProtectWithPermissions(handler, nil)
|
||||
}
|
||||
|
||||
// ProtectWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer token
|
||||
// as well as a slice of permissions that must be met.
|
||||
//
|
||||
// Example:
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
|
||||
// router := http.NewServeMux()
|
||||
// // Without protection
|
||||
// router.Handle("/handle", yourHandler)
|
||||
// // With protection
|
||||
// router.Handle("/handle", gate.ProtectWithPermissions(yourHandler, []string{"admin"}))
|
||||
//
|
||||
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
|
||||
func (gate *Gate) ProtectWithPermissions(handler http.Handler, permissions []string) http.Handler {
|
||||
return gate.ProtectFuncWithPermissions(func(writer http.ResponseWriter, request *http.Request) {
|
||||
handler.ServeHTTP(writer, request)
|
||||
}, permissions)
|
||||
}
|
||||
|
||||
// ProtectWithPermission does the same thing as ProtectWithPermissions, but for a single permission instead of a
|
||||
// slice of permissions
|
||||
//
|
||||
// See ProtectWithPermissions for further documentation
|
||||
func (gate *Gate) ProtectWithPermission(handler http.Handler, permission string) http.Handler {
|
||||
return gate.ProtectFuncWithPermissions(func(writer http.ResponseWriter, request *http.Request) {
|
||||
handler.ServeHTTP(writer, request)
|
||||
}, []string{permission})
|
||||
}
|
||||
|
||||
// ProtectFunc secures a handlerFunc, requiring requests going through to have a valid Authorization Bearer token.
|
||||
// Unlike ProtectFuncWithPermissions, ProtectFunc will allow access to any registered tokens, regardless of their
|
||||
// permissions or lack thereof.
|
||||
//
|
||||
// Example:
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithToken("token"))
|
||||
// router := http.NewServeMux()
|
||||
// // Without protection
|
||||
// router.HandleFunc("/handle", yourHandlerFunc)
|
||||
// // With protection
|
||||
// router.HandleFunc("/handle", gate.ProtectFunc(yourHandlerFunc))
|
||||
//
|
||||
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
|
||||
func (gate *Gate) ProtectFunc(handlerFunc http.HandlerFunc) http.HandlerFunc {
|
||||
return gate.ProtectFuncWithPermissions(handlerFunc, nil)
|
||||
}
|
||||
|
||||
// ProtectFuncWithPermissions secures a handler, requiring requests going through to have a valid Authorization Bearer
|
||||
// token as well as a slice of permissions that must be met.
|
||||
//
|
||||
// Example:
|
||||
// gate := g8.New().WithAuthorizationService(g8.NewAuthorizationService().WithClient(g8.NewClient("token").WithPermission("admin")))
|
||||
// router := http.NewServeMux()
|
||||
// // Without protection
|
||||
// router.HandleFunc("/handle", yourHandlerFunc)
|
||||
// // With protection
|
||||
// router.HandleFunc("/handle", gate.ProtectFuncWithPermissions(yourHandlerFunc, []string{"admin"}))
|
||||
//
|
||||
// The token extracted from the request is passed to the handlerFunc request context under the key TokenContextKey
|
||||
func (gate *Gate) ProtectFuncWithPermissions(handlerFunc http.HandlerFunc, permissions []string) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
if gate.rateLimiter != nil {
|
||||
if !gate.rateLimiter.Try() {
|
||||
writer.WriteHeader(http.StatusTooManyRequests)
|
||||
_, _ = writer.Write(gate.tooManyRequestsResponseBody)
|
||||
return
|
||||
}
|
||||
}
|
||||
if gate.authorizationService != nil {
|
||||
var token string
|
||||
if gate.customTokenExtractorFunc != nil {
|
||||
token = gate.customTokenExtractorFunc(request)
|
||||
} else {
|
||||
token = extractTokenFromRequest(request)
|
||||
}
|
||||
if !gate.authorizationService.IsAuthorized(token, permissions) {
|
||||
writer.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = writer.Write(gate.unauthorizedResponseBody)
|
||||
return
|
||||
}
|
||||
request = request.WithContext(context.WithValue(request.Context(), TokenContextKey, token))
|
||||
}
|
||||
handlerFunc(writer, request)
|
||||
}
|
||||
}
|
||||
|
||||
// ProtectFuncWithPermission does the same thing as ProtectFuncWithPermissions, but for a single permission instead of a
|
||||
// slice of permissions
|
||||
//
|
||||
// See ProtectFuncWithPermissions for further documentation
|
||||
func (gate *Gate) ProtectFuncWithPermission(handlerFunc http.HandlerFunc, permission string) http.HandlerFunc {
|
||||
return gate.ProtectFuncWithPermissions(handlerFunc, []string{permission})
|
||||
}
|
||||
|
||||
// extractTokenFromRequest extracts the bearer token from the AuthorizationHeader
|
||||
func extractTokenFromRequest(request *http.Request) string {
|
||||
return strings.TrimPrefix(request.Header.Get(AuthorizationHeader), "Bearer ")
|
||||
}
|
42
vendor/github.com/TwiN/g8/ratelimiter.go
generated
vendored
Normal file
42
vendor/github.com/TwiN/g8/ratelimiter.go
generated
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
package g8
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RateLimiter is a fixed rate limiter
|
||||
type RateLimiter struct {
|
||||
maximumExecutionsPerSecond int
|
||||
executionsLeftInWindow int
|
||||
windowStartTime time.Time
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewRateLimiter creates a RateLimiter
|
||||
func NewRateLimiter(maximumExecutionsPerSecond int) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
windowStartTime: time.Now(),
|
||||
executionsLeftInWindow: maximumExecutionsPerSecond,
|
||||
maximumExecutionsPerSecond: maximumExecutionsPerSecond,
|
||||
}
|
||||
}
|
||||
|
||||
// Try updates the number of executions if the rate limit quota hasn't been reached and returns whether the
|
||||
// attempt was successful or not.
|
||||
//
|
||||
// Returns false if the execution was not successful (rate limit quota has been reached)
|
||||
// Returns true if the execution was successful (rate limit quota has not been reached)
|
||||
func (r *RateLimiter) Try() bool {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
if time.Now().Add(-time.Second).After(r.windowStartTime) {
|
||||
r.windowStartTime = time.Now()
|
||||
r.executionsLeftInWindow = r.maximumExecutionsPerSecond
|
||||
}
|
||||
if r.executionsLeftInWindow == 0 {
|
||||
return false
|
||||
}
|
||||
r.executionsLeftInWindow--
|
||||
return true
|
||||
}
|
1
vendor/github.com/TwiN/gocache/v2/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/TwiN/gocache/v2/.gitattributes
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=lf
|
1
vendor/github.com/TwiN/gocache/v2/.gitignore
generated
vendored
Normal file
1
vendor/github.com/TwiN/gocache/v2/.gitignore
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
.idea
|
9
vendor/github.com/TwiN/gocache/v2/LICENSE.md
generated
vendored
Normal file
9
vendor/github.com/TwiN/gocache/v2/LICENSE.md
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 TwiN
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
370
vendor/github.com/TwiN/gocache/v2/README.md
generated
vendored
Normal file
370
vendor/github.com/TwiN/gocache/v2/README.md
generated
vendored
Normal file
@ -0,0 +1,370 @@
|
||||
# gocache
|
||||

|
||||
[](https://goreportcard.com/report/github.com/TwiN/gocache)
|
||||
[](https://codecov.io/gh/TwiN/gocache)
|
||||
[](https://github.com/TwiN/gocache)
|
||||
[](https://pkg.go.dev/github.com/TwiN/gocache)
|
||||
[](https://github.com/TwiN)
|
||||
|
||||
gocache is an easy-to-use, high-performance, lightweight and thread-safe (goroutine-safe) in-memory key-value cache
|
||||
with support for LRU and FIFO eviction policies as well as expiration, bulk operations and even retrieval of keys by pattern.
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Usage](#usage)
|
||||
- [Initializing the cache](#initializing-the-cache)
|
||||
- [Functions](#functions)
|
||||
- [Examples](#examples)
|
||||
- [Creating or updating an entry](#creating-or-updating-an-entry)
|
||||
- [Getting an entry](#getting-an-entry)
|
||||
- [Deleting an entry](#deleting-an-entry)
|
||||
- [Complex example](#complex-example)
|
||||
- [Persistence](#persistence)
|
||||
- [Eviction](#eviction)
|
||||
- [MaxSize](#maxsize)
|
||||
- [MaxMemoryUsage](#maxmemoryusage)
|
||||
- [Expiration](#expiration)
|
||||
- [Performance](#performance)
|
||||
- [Summary](#summary)
|
||||
- [Results](#results)
|
||||
- [FAQ](#faq)
|
||||
- [How can I persist the data on application termination?](#how-can-i-persist-the-data-on-application-termination)
|
||||
|
||||
|
||||
## Features
|
||||
gocache supports the following cache eviction policies:
|
||||
- First in first out (FIFO)
|
||||
- Least recently used (LRU)
|
||||
|
||||
It also supports cache entry TTL, which is both active and passive. Active expiration means that if you attempt
|
||||
to retrieve a cache key that has already expired, it will delete it on the spot and the behavior will be as if
|
||||
the cache key didn't exist. As for passive expiration, there's a background task that will take care of deleting
|
||||
expired keys.
|
||||
|
||||
It also includes what you'd expect from a cache, like GET/SET, bulk operations and get by pattern.
|
||||
|
||||
|
||||
## Usage
|
||||
```
|
||||
go get -u github.com/TwiN/gocache/v2
|
||||
```
|
||||
|
||||
|
||||
### Initializing the cache
|
||||
```go
|
||||
cache := gocache.NewCache().WithMaxSize(1000).WithEvictionPolicy(gocache.LeastRecentlyUsed)
|
||||
```
|
||||
|
||||
If you're planning on using expiration (`SetWithTTL` or `Expire`) and you want expired entries to be automatically deleted
|
||||
in the background, make sure to start the janitor when you instantiate the cache:
|
||||
|
||||
```go
|
||||
cache.StartJanitor()
|
||||
```
|
||||
|
||||
### Functions
|
||||
| Function | Description |
|
||||
| --------------------------------- | ----------- |
|
||||
| WithMaxSize | Sets the max size of the cache. `gocache.NoMaxSize` means there is no limit. If not set, the default max size is `gocache.DefaultMaxSize`.
|
||||
| WithMaxMemoryUsage | Sets the max memory usage of the cache. `gocache.NoMaxMemoryUsage` means there is no limit. The default behavior is to not evict based on memory usage.
|
||||
| WithEvictionPolicy | Sets the eviction algorithm to be used when the cache reaches the max size. If not set, the default eviction policy is `gocache.FirstInFirstOut` (FIFO).
|
||||
| WithForceNilInterfaceOnNilPointer | Configures whether values with a nil pointer passed to write functions should be forcefully set to nil. Defaults to true.
|
||||
| StartJanitor | Starts the janitor, which is in charge of deleting expired cache entries in the background.
|
||||
| StopJanitor | Stops the janitor.
|
||||
| Set | Same as `SetWithTTL`, but with no expiration (`gocache.NoExpiration`)
|
||||
| SetAll | Same as `Set`, but in bulk
|
||||
| SetWithTTL | Creates or updates a cache entry with the given key, value and expiration time. If the max size after the aforementioned operation is above the configured max size, the tail will be evicted. Depending on the eviction policy, the tail is defined as the oldest
|
||||
| Get | Gets a cache entry by its key.
|
||||
| GetByKeys | Gets a map of entries by their keys. The resulting map will contain all keys, even if some of the keys in the slice passed as parameter were not present in the cache.
|
||||
| GetAll | Gets all cache entries.
|
||||
| GetKeysByPattern | Retrieves a slice of keys that matches a given pattern.
|
||||
| Delete | Removes a key from the cache.
|
||||
| DeleteAll | Removes multiple keys from the cache.
|
||||
| Count | Gets the size of the cache. This includes cache keys which may have already expired, but have not been removed yet.
|
||||
| Clear | Wipes the cache.
|
||||
| TTL | Gets the time until a cache key expires.
|
||||
| Expire | Sets the expiration time of an existing cache key.
|
||||
|
||||
For further documentation, please refer to [Go Reference](https://pkg.go.dev/github.com/TwiN/gocache)
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
#### Creating or updating an entry
|
||||
```go
|
||||
cache.Set("key", "value")
|
||||
cache.Set("key", 1)
|
||||
cache.Set("key", struct{ Text string }{Test: "value"})
|
||||
cache.SetWithTTL("key", []byte("value"), 24*time.Hour)
|
||||
```
|
||||
|
||||
#### Getting an entry
|
||||
```go
|
||||
value, exists := cache.Get("key")
|
||||
```
|
||||
You can also get multiple entries by using `cache.GetByKeys([]string{"key1", "key2"})`
|
||||
|
||||
#### Deleting an entry
|
||||
```go
|
||||
cache.Delete("key")
|
||||
```
|
||||
You can also delete multiple entries by using `cache.DeleteAll([]string{"key1", "key2"})`
|
||||
|
||||
#### Complex example
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gocache/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cache := gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed).WithMaxSize(10000)
|
||||
cache.StartJanitor() // Passively manages expired entries
|
||||
defer cache.StopJanitor()
|
||||
|
||||
cache.Set("key", "value")
|
||||
cache.SetWithTTL("key-with-ttl", "value", 60*time.Minute)
|
||||
cache.SetAll(map[string]interface{}{"k1": "v1", "k2": "v2", "k3": "v3"})
|
||||
|
||||
fmt.Println("[Count] Cache size:", cache.Count())
|
||||
|
||||
value, exists := cache.Get("key")
|
||||
fmt.Printf("[Get] key=key; value=%s; exists=%v\n", value, exists)
|
||||
for key, value := range cache.GetByKeys([]string{"k1", "k2", "k3"}) {
|
||||
fmt.Printf("[GetByKeys] key=%s; value=%s\n", key, value)
|
||||
}
|
||||
for _, key := range cache.GetKeysByPattern("key*", 0) {
|
||||
fmt.Printf("[GetKeysByPattern] pattern=key*; key=%s\n", key)
|
||||
}
|
||||
|
||||
cache.Expire("key", time.Hour)
|
||||
time.Sleep(500*time.Millisecond)
|
||||
timeUntilExpiration, _ := cache.TTL("key")
|
||||
fmt.Println("[TTL] Number of minutes before 'key' expires:", int(timeUntilExpiration.Seconds()))
|
||||
|
||||
cache.Delete("key")
|
||||
cache.DeleteAll([]string{"k1", "k2", "k3"})
|
||||
|
||||
cache.Clear()
|
||||
fmt.Println("[Count] Cache size after clearing the cache:", cache.Count())
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
|
||||
```
|
||||
[Count] Cache size: 5
|
||||
[Get] key=key; value=value; exists=true
|
||||
[GetByKeys] key=k1; value=v1
|
||||
[GetByKeys] key=k2; value=v2
|
||||
[GetByKeys] key=k3; value=v3
|
||||
[GetKeysByPattern] pattern=key*; key=key-with-ttl
|
||||
[GetKeysByPattern] pattern=key*; key=key
|
||||
[TTL] Number of minutes before 'key' expires: 3599
|
||||
[Count] Cache size after clearing the cache: 0
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
## Persistence
|
||||
Prior to v2, gocache supported persistence out of the box.
|
||||
|
||||
After some thinking, I decided that persistence added too many dependencies, and given than this is a cache library
|
||||
and most people wouldn't be interested in persistence, I decided to get rid of it.
|
||||
|
||||
That being said, you can use the `GetAll` and `SetAll` methods of `gocache.Cache` to implement persistence yourself.
|
||||
|
||||
|
||||
## Eviction
|
||||
### MaxSize
|
||||
Eviction by MaxSize is the default behavior, and is also the most efficient.
|
||||
|
||||
The code below will create a cache that has a maximum size of 1000:
|
||||
```go
|
||||
cache := gocache.NewCache().WithMaxSize(1000)
|
||||
```
|
||||
This means that whenever an operation causes the total size of the cache to go above 1000, the tail will be evicted.
|
||||
|
||||
### MaxMemoryUsage
|
||||
Eviction by MaxMemoryUsage is **disabled by default**, and is in alpha.
|
||||
|
||||
The code below will create a cache that has a maximum memory usage of 50MB:
|
||||
```go
|
||||
cache := gocache.NewCache().WithMaxSize(0).WithMaxMemoryUsage(50*gocache.Megabyte)
|
||||
```
|
||||
This means that whenever an operation causes the total memory usage of the cache to go above 50MB, one or more tails
|
||||
will be evicted.
|
||||
|
||||
Unlike evictions caused by reaching the MaxSize, evictions triggered by MaxMemoryUsage may lead to multiple entries
|
||||
being evicted in a row. The reason for this is that if, for instance, you had 100 entries of 0.1MB each and you suddenly added
|
||||
a single entry of 10MB, 100 entries would need to be evicted to make enough space for that new big entry.
|
||||
|
||||
It's very important to keep in mind that eviction by MaxMemoryUsage is approximate.
|
||||
|
||||
**The only memory taken into consideration is the size of the cache, not the size of the entire application.**
|
||||
If you pass along 100MB worth of data in a matter of seconds, even though the cache's memory usage will remain
|
||||
under 50MB (or whatever you configure the MaxMemoryUsage to), the memory footprint generated by that 100MB will
|
||||
still exist until the next GC cycle.
|
||||
|
||||
As previously mentioned, this is a work in progress, and here's a list of the things you should keep in mind:
|
||||
- The memory usage of structs are a gross estimation and may not reflect the actual memory usage.
|
||||
- Native types (string, int, bool, []byte, etc.) are the most accurate for calculating the memory usage.
|
||||
- Adding an entry bigger than the configured MaxMemoryUsage will work, but it will evict all other entries.
|
||||
|
||||
|
||||
## Expiration
|
||||
There are two ways that the deletion of expired keys can take place:
|
||||
- Active
|
||||
- Passive
|
||||
|
||||
**Active deletion of expired keys** happens when an attempt is made to access the value of a cache entry that expired.
|
||||
`Get`, `GetByKeys` and `GetAll` are the only functions that can trigger active deletion of expired keys.
|
||||
|
||||
**Passive deletion of expired keys** runs in the background and is managed by the janitor.
|
||||
If you do not start the janitor, there will be no passive deletion of expired keys.
|
||||
|
||||
|
||||
## Performance
|
||||
### Summary
|
||||
- **Set**: Both map and gocache have the same performance.
|
||||
- **Get**: Map is faster than gocache.
|
||||
|
||||
This is because gocache keeps track of the head and the tail for eviction and expiration/TTL.
|
||||
|
||||
Ultimately, the difference is negligible.
|
||||
|
||||
We could add a way to disable eviction or disable expiration altogether just to match the map's performance,
|
||||
but if you're looking into using a library like gocache, odds are, you want more than just a map.
|
||||
|
||||
|
||||
### Results
|
||||
| key | value |
|
||||
|:------ |:-------- |
|
||||
| goos | windows |
|
||||
| goarch | amd64 |
|
||||
| cpu | i7-9700K |
|
||||
| mem | 32G DDR4 |
|
||||
|
||||
```
|
||||
// Normal map
|
||||
BenchmarkMap_Get
|
||||
BenchmarkMap_Get-8 46087372 26.7 ns/op
|
||||
BenchmarkMap_Set
|
||||
BenchmarkMap_Set/small_value-8 3841911 389 ns/op
|
||||
BenchmarkMap_Set/medium_value-8 3887074 391 ns/op
|
||||
BenchmarkMap_Set/large_value-8 3921956 393 ns/op
|
||||
// Gocache
|
||||
BenchmarkCache_Get
|
||||
BenchmarkCache_Get/FirstInFirstOut-8 27273036 46.4 ns/op
|
||||
BenchmarkCache_Get/LeastRecentlyUsed-8 26648248 46.3 ns/op
|
||||
BenchmarkCache_Set
|
||||
BenchmarkCache_Set/FirstInFirstOut_small_value-8 2919584 405 ns/op
|
||||
BenchmarkCache_Set/FirstInFirstOut_medium_value-8 2990841 391 ns/op
|
||||
BenchmarkCache_Set/FirstInFirstOut_large_value-8 2970513 391 ns/op
|
||||
BenchmarkCache_Set/LeastRecentlyUsed_small_value-8 2962939 402 ns/op
|
||||
BenchmarkCache_Set/LeastRecentlyUsed_medium_value-8 2962963 390 ns/op
|
||||
BenchmarkCache_Set/LeastRecentlyUsed_large_value-8 2962928 394 ns/op
|
||||
BenchmarkCache_SetUsingMaxMemoryUsage
|
||||
BenchmarkCache_SetUsingMaxMemoryUsage/small_value-8 2683356 447 ns/op
|
||||
BenchmarkCache_SetUsingMaxMemoryUsage/medium_value-8 2637578 441 ns/op
|
||||
BenchmarkCache_SetUsingMaxMemoryUsage/large_value-8 2672434 443 ns/op
|
||||
BenchmarkCache_SetWithMaxSize
|
||||
BenchmarkCache_SetWithMaxSize/100_small_value-8 4782966 252 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/10000_small_value-8 4067967 296 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/100000_small_value-8 3762055 328 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/100_medium_value-8 4760479 252 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/10000_medium_value-8 4081050 295 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/100000_medium_value-8 3785050 330 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/100_large_value-8 4732909 254 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/10000_large_value-8 4079533 297 ns/op
|
||||
BenchmarkCache_SetWithMaxSize/100000_large_value-8 3712820 331 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/100_small_value-8 4761732 254 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/10000_small_value-8 4084474 296 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/100000_small_value-8 3761402 329 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/100_medium_value-8 4783075 254 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/10000_medium_value-8 4103980 296 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/100000_medium_value-8 3646023 331 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/100_large_value-8 4779025 254 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/10000_large_value-8 4096192 296 ns/op
|
||||
BenchmarkCache_SetWithMaxSizeAndLRU/100000_large_value-8 3726823 331 ns/op
|
||||
BenchmarkCache_GetSetMultipleConcurrent
|
||||
BenchmarkCache_GetSetMultipleConcurrent-8 707142 1698 ns/op
|
||||
BenchmarkCache_GetSetConcurrentWithFrequentEviction
|
||||
BenchmarkCache_GetSetConcurrentWithFrequentEviction/FirstInFirstOut-8 3616256 334 ns/op
|
||||
BenchmarkCache_GetSetConcurrentWithFrequentEviction/LeastRecentlyUsed-8 3636367 331 ns/op
|
||||
BenchmarkCache_GetConcurrentWithLRU
|
||||
BenchmarkCache_GetConcurrentWithLRU/FirstInFirstOut-8 4405557 268 ns/op
|
||||
BenchmarkCache_GetConcurrentWithLRU/LeastRecentlyUsed-8 4445475 269 ns/op
|
||||
BenchmarkCache_WithForceNilInterfaceOnNilPointer
|
||||
BenchmarkCache_WithForceNilInterfaceOnNilPointer/true_with_nil_struct_pointer-8 6184591 191 ns/op
|
||||
BenchmarkCache_WithForceNilInterfaceOnNilPointer/true-8 6090482 191 ns/op
|
||||
BenchmarkCache_WithForceNilInterfaceOnNilPointer/false_with_nil_struct_pointer-8 6184629 187 ns/op
|
||||
BenchmarkCache_WithForceNilInterfaceOnNilPointer/false-8 6281781 186 ns/op
|
||||
(Trimmed "BenchmarkCache_" for readability)
|
||||
WithForceNilInterfaceOnNilPointerWithConcurrency
|
||||
WithForceNilInterfaceOnNilPointerWithConcurrency/true_with_nil_struct_pointer-8 4379564 268 ns/op
|
||||
WithForceNilInterfaceOnNilPointerWithConcurrency/true-8 4379558 265 ns/op
|
||||
WithForceNilInterfaceOnNilPointerWithConcurrency/false_with_nil_struct_pointer-8 4444456 261 ns/op
|
||||
WithForceNilInterfaceOnNilPointerWithConcurrency/false-8 4493896 262 ns/op
|
||||
```
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### How can I persist the data on application termination?
|
||||
While creating your own auto save feature might come in handy, it may still lead to loss of data if the application
|
||||
automatically saves every 10 minutes and your application crashes 9 minutes after the previous save.
|
||||
|
||||
To increase your odds of not losing any data, you can use Go's `signal` package, more specifically its `Notify` function
|
||||
which allows listening for termination signals like SIGTERM and SIGINT. Once a termination signal is caught, you can
|
||||
add the necessary logic for a graceful shutdown.
|
||||
|
||||
In the following example, the code that would usually be present in the `main` function is moved to a different function
|
||||
named `Start` which is launched on a different goroutine so that listening for a termination signals is what blocks the
|
||||
main goroutine instead:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/TwiN/gocache/v2"
|
||||
)
|
||||
|
||||
var cache = gocache.NewCache()
|
||||
|
||||
func main() {
|
||||
data := retrieveCacheEntriesUsingWhateverMeanYouUsedToPersistIt()
|
||||
cache.SetAll(data)
|
||||
// Start everything else on another goroutine to prevent blocking the main goroutine
|
||||
go Start()
|
||||
// Wait for termination signal
|
||||
sig := make(chan os.Signal, 1)
|
||||
done := make(chan bool, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sig
|
||||
log.Println("Received termination signal, attempting to gracefully shut down")
|
||||
// Persist the cache entries
|
||||
cacheEntries := cache.GetAll()
|
||||
persistCacheEntriesHoweverYouWant(cacheEntries)
|
||||
// Tell the main goroutine that we're done
|
||||
done <- true
|
||||
}()
|
||||
<-done
|
||||
log.Println("Shutting down")
|
||||
}
|
||||
```
|
||||
|
||||
Note that this won't protect you from a SIGKILL, as this signal cannot be caught.
|
108
vendor/github.com/TwiN/gocache/v2/entry.go
generated
vendored
Normal file
108
vendor/github.com/TwiN/gocache/v2/entry.go
generated
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
package gocache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Entry is a cache entry
|
||||
type Entry struct {
|
||||
// Key is the name of the cache entry
|
||||
Key string
|
||||
|
||||
// Value is the value of the cache entry
|
||||
Value interface{}
|
||||
|
||||
// RelevantTimestamp is the variable used to store either:
|
||||
// - creation timestamp, if the Cache's EvictionPolicy is FirstInFirstOut
|
||||
// - last access timestamp, if the Cache's EvictionPolicy is LeastRecentlyUsed
|
||||
//
|
||||
// Note that updating an existing entry will also update this value
|
||||
RelevantTimestamp time.Time
|
||||
|
||||
// Expiration is the unix time in nanoseconds at which the entry will expire (-1 means no expiration)
|
||||
Expiration int64
|
||||
|
||||
next *Entry
|
||||
previous *Entry
|
||||
}
|
||||
|
||||
// Accessed updates the Entry's RelevantTimestamp to now
|
||||
func (entry *Entry) Accessed() {
|
||||
entry.RelevantTimestamp = time.Now()
|
||||
}
|
||||
|
||||
// Expired returns whether the Entry has expired
|
||||
func (entry Entry) Expired() bool {
|
||||
if entry.Expiration > 0 {
|
||||
if time.Now().UnixNano() > entry.Expiration {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SizeInBytes returns the size of an entry in bytes, approximately.
|
||||
func (entry *Entry) SizeInBytes() int {
|
||||
return toBytes(entry.Key) + toBytes(entry.Value) + 32
|
||||
}
|
||||
|
||||
func toBytes(value interface{}) int {
|
||||
switch value.(type) {
|
||||
case string:
|
||||
return int(unsafe.Sizeof(value)) + len(value.(string))
|
||||
case int8, uint8, bool:
|
||||
return int(unsafe.Sizeof(value)) + 1
|
||||
case int16, uint16:
|
||||
return int(unsafe.Sizeof(value)) + 2
|
||||
case int32, uint32, float32, complex64:
|
||||
return int(unsafe.Sizeof(value)) + 4
|
||||
case int64, uint64, int, uint, float64, complex128:
|
||||
return int(unsafe.Sizeof(value)) + 8
|
||||
case []interface{}:
|
||||
size := 0
|
||||
for _, v := range value.([]interface{}) {
|
||||
size += toBytes(v)
|
||||
}
|
||||
return int(unsafe.Sizeof(value)) + size
|
||||
case []string:
|
||||
size := 0
|
||||
for _, v := range value.([]string) {
|
||||
size += toBytes(v)
|
||||
}
|
||||
return int(unsafe.Sizeof(value)) + size
|
||||
case []int8:
|
||||
return int(unsafe.Sizeof(value)) + len(value.([]int8))
|
||||
case []uint8:
|
||||
return int(unsafe.Sizeof(value)) + len(value.([]uint8))
|
||||
case []bool:
|
||||
return int(unsafe.Sizeof(value)) + len(value.([]bool))
|
||||
case []int16:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]int16)) * 2)
|
||||
case []uint16:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]uint16)) * 2)
|
||||
case []int32:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]int32)) * 4)
|
||||
case []uint32:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]uint32)) * 4)
|
||||
case []float32:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]float32)) * 4)
|
||||
case []complex64:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]complex64)) * 4)
|
||||
case []int64:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]int64)) * 8)
|
||||
case []uint64:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]uint64)) * 8)
|
||||
case []int:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]int)) * 8)
|
||||
case []uint:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]uint)) * 8)
|
||||
case []float64:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]float64)) * 8)
|
||||
case []complex128:
|
||||
return int(unsafe.Sizeof(value)) + (len(value.([]complex128)) * 8)
|
||||
default:
|
||||
return int(unsafe.Sizeof(value)) + len(fmt.Sprintf("%v", value))
|
||||
}
|
||||
}
|
567
vendor/github.com/TwiN/gocache/v2/gocache.go
generated
vendored
Normal file
567
vendor/github.com/TwiN/gocache/v2/gocache.go
generated
vendored
Normal file
@ -0,0 +1,567 @@
|
||||
package gocache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Debug = false
|
||||
)
|
||||
|
||||
const (
|
||||
// NoMaxSize means that the cache has no maximum number of entries in the cache
|
||||
// Setting Cache.maxSize to this value also means there will be no eviction
|
||||
NoMaxSize = 0
|
||||
|
||||
// NoMaxMemoryUsage means that the cache has no maximum number of entries in the cache
|
||||
NoMaxMemoryUsage = 0
|
||||
|
||||
// DefaultMaxSize is the max size set if no max size is specified
|
||||
DefaultMaxSize = 100000
|
||||
|
||||
// NoExpiration is the value that must be used as TTL to specify that the given key should never expire
|
||||
NoExpiration = -1
|
||||
|
||||
Kilobyte = 1024
|
||||
Megabyte = 1024 * Kilobyte
|
||||
Gigabyte = 1024 * Megabyte
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyDoesNotExist = errors.New("key does not exist") // Returned when a cache key does not exist
|
||||
ErrKeyHasNoExpiration = errors.New("key has no expiration") // Returned when a cache key has no expiration
|
||||
ErrJanitorAlreadyRunning = errors.New("janitor is already running") // Returned when the janitor has already been started
|
||||
)
|
||||
|
||||
// Cache is the core struct of gocache which contains the data as well as all relevant configuration fields
|
||||
type Cache struct {
|
||||
// maxSize is the maximum amount of entries that can be in the cache at any given time
|
||||
// By default, this is set to DefaultMaxSize
|
||||
maxSize int
|
||||
|
||||
// maxMemoryUsage is the maximum amount of memory that can be taken up by the cache at any time
|
||||
// By default, this is set to NoMaxMemoryUsage, meaning that the default behavior is to not evict
|
||||
// based on maximum memory usage
|
||||
maxMemoryUsage int
|
||||
|
||||
// evictionPolicy is the eviction policy
|
||||
evictionPolicy EvictionPolicy
|
||||
|
||||
// stats is the object that contains cache statistics/metrics
|
||||
stats *Statistics
|
||||
|
||||
// entries is the content of the cache
|
||||
entries map[string]*Entry
|
||||
|
||||
// mutex is the lock for making concurrent operations on the cache
|
||||
mutex sync.RWMutex
|
||||
|
||||
// head is the cache entry at the head of the cache
|
||||
head *Entry
|
||||
|
||||
// tail is the last cache node and also the next entry that will be evicted
|
||||
tail *Entry
|
||||
|
||||
// stopJanitor is the channel used to stop the janitor
|
||||
stopJanitor chan bool
|
||||
|
||||
// memoryUsage is the approximate memory usage of the cache (dataset only) in bytes
|
||||
memoryUsage int
|
||||
|
||||
// forceNilInterfaceOnNilPointer determines whether all Set-like functions should set a value as nil if the
|
||||
// interface passed has a nil value but not a nil type.
|
||||
//
|
||||
// By default, interfaces are only nil when both their type and value is nil.
|
||||
// This means that when you pass a pointer to a nil value, the type of the interface
|
||||
// will still show as nil, which means that if you don't cast the interface after
|
||||
// retrieving it, a nil check will return that the value is not false.
|
||||
forceNilInterfaceOnNilPointer bool
|
||||
}
|
||||
|
||||
// MaxSize returns the maximum amount of keys that can be present in the cache before
|
||||
// new entries trigger the eviction of the tail
|
||||
func (cache *Cache) MaxSize() int {
|
||||
return cache.maxSize
|
||||
}
|
||||
|
||||
// MaxMemoryUsage returns the configured maxMemoryUsage of the cache
|
||||
func (cache *Cache) MaxMemoryUsage() int {
|
||||
return cache.maxMemoryUsage
|
||||
}
|
||||
|
||||
// EvictionPolicy returns the EvictionPolicy of the Cache
|
||||
func (cache *Cache) EvictionPolicy() EvictionPolicy {
|
||||
return cache.evictionPolicy
|
||||
}
|
||||
|
||||
// Stats returns statistics from the cache
|
||||
func (cache *Cache) Stats() Statistics {
|
||||
cache.mutex.RLock()
|
||||
stats := Statistics{
|
||||
EvictedKeys: cache.stats.EvictedKeys,
|
||||
ExpiredKeys: cache.stats.ExpiredKeys,
|
||||
Hits: cache.stats.Hits,
|
||||
Misses: cache.stats.Misses,
|
||||
}
|
||||
cache.mutex.RUnlock()
|
||||
return stats
|
||||
}
|
||||
|
||||
// MemoryUsage returns the current memory usage of the cache's dataset in bytes
|
||||
// If MaxMemoryUsage is set to NoMaxMemoryUsage, this will return 0
|
||||
func (cache *Cache) MemoryUsage() int {
|
||||
return cache.memoryUsage
|
||||
}
|
||||
|
||||
// WithMaxSize sets the maximum amount of entries that can be in the cache at any given time
|
||||
// A maxSize of 0 or less means infinite
|
||||
func (cache *Cache) WithMaxSize(maxSize int) *Cache {
|
||||
if maxSize < 0 {
|
||||
maxSize = NoMaxSize
|
||||
}
|
||||
if maxSize != NoMaxSize && cache.Count() == 0 {
|
||||
cache.entries = make(map[string]*Entry, maxSize)
|
||||
}
|
||||
cache.maxSize = maxSize
|
||||
return cache
|
||||
}
|
||||
|
||||
// WithMaxMemoryUsage sets the maximum amount of memory that can be used by the cache at any given time
|
||||
//
|
||||
// NOTE: This is approximate.
|
||||
//
|
||||
// Setting this to NoMaxMemoryUsage will disable eviction by memory usage
|
||||
func (cache *Cache) WithMaxMemoryUsage(maxMemoryUsageInBytes int) *Cache {
|
||||
if maxMemoryUsageInBytes < 0 {
|
||||
maxMemoryUsageInBytes = NoMaxMemoryUsage
|
||||
}
|
||||
cache.maxMemoryUsage = maxMemoryUsageInBytes
|
||||
return cache
|
||||
}
|
||||
|
||||
// WithEvictionPolicy sets eviction algorithm.
|
||||
// Defaults to FirstInFirstOut (FIFO)
|
||||
func (cache *Cache) WithEvictionPolicy(policy EvictionPolicy) *Cache {
|
||||
cache.evictionPolicy = policy
|
||||
return cache
|
||||
}
|
||||
|
||||
// WithForceNilInterfaceOnNilPointer sets whether all Set-like functions should set a value as nil if the
|
||||
// interface passed has a nil value but not a nil type.
|
||||
//
|
||||
// In Go, an interface is only nil if both its type and value are nil, which means that a nil pointer
|
||||
// (e.g. (*Struct)(nil)) will retain its attribution to the type, and the unmodified value returned from
|
||||
// Cache.Get, for instance, would return false when compared with nil if this option is set to false.
|
||||
//
|
||||
// We can bypass this by detecting if the interface's value is nil and setting it to nil rather than
|
||||
// a nil pointer, which will make the value returned from Cache.Get return true when compared with nil.
|
||||
// This is exactly what passing true to WithForceNilInterfaceOnNilPointer does, and it's also the default behavior.
|
||||
//
|
||||
// Alternatively, you may pass false to WithForceNilInterfaceOnNilPointer, which will mean that you'll have
|
||||
// to cast the value returned from Cache.Get to its original type to check for whether the pointer returned
|
||||
// is nil or not.
|
||||
//
|
||||
// If set to true (default):
|
||||
// cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(true)
|
||||
// cache.Set("key", (*Struct)(nil))
|
||||
// value, _ := cache.Get("key")
|
||||
// // the following returns true, because the interface{} was forcefully set to nil
|
||||
// if value == nil {}
|
||||
// // the following will panic, because the value has been casted to its type (which is nil)
|
||||
// if value.(*Struct) == nil {}
|
||||
//
|
||||
// If set to false:
|
||||
// cache := gocache.NewCache().WithForceNilInterfaceOnNilPointer(false)
|
||||
// cache.Set("key", (*Struct)(nil))
|
||||
// value, _ := cache.Get("key")
|
||||
// // the following returns false, because the interface{} returned has a non-nil type (*Struct)
|
||||
// if value == nil {}
|
||||
// // the following returns true, because the value has been casted to its type
|
||||
// if value.(*Struct) == nil {}
|
||||
//
|
||||
// In other words, if set to true, you do not need to cast the value returned from the cache to
|
||||
// to check if the value is nil.
|
||||
//
|
||||
// Defaults to true
|
||||
func (cache *Cache) WithForceNilInterfaceOnNilPointer(forceNilInterfaceOnNilPointer bool) *Cache {
|
||||
cache.forceNilInterfaceOnNilPointer = forceNilInterfaceOnNilPointer
|
||||
return cache
|
||||
}
|
||||
|
||||
// NewCache creates a new Cache
|
||||
//
|
||||
// Should be used in conjunction with Cache.WithMaxSize, Cache.WithMaxMemoryUsage and/or Cache.WithEvictionPolicy
|
||||
// gocache.NewCache().WithMaxSize(10000).WithEvictionPolicy(gocache.LeastRecentlyUsed)
|
||||
//
|
||||
func NewCache() *Cache {
|
||||
return &Cache{
|
||||
maxSize: DefaultMaxSize,
|
||||
evictionPolicy: FirstInFirstOut,
|
||||
stats: &Statistics{},
|
||||
entries: make(map[string]*Entry),
|
||||
mutex: sync.RWMutex{},
|
||||
stopJanitor: nil,
|
||||
forceNilInterfaceOnNilPointer: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Set creates or updates a key with a given value
|
||||
func (cache *Cache) Set(key string, value interface{}) {
|
||||
cache.SetWithTTL(key, value, NoExpiration)
|
||||
}
|
||||
|
||||
// SetWithTTL creates or updates a key with a given value and sets an expiration time (-1 is NoExpiration)
|
||||
//
|
||||
// The TTL provided must be greater than 0, or NoExpiration (-1). If a negative value that isn't -1 (NoExpiration) is
|
||||
// provided, the entry will not be created if the key doesn't exist
|
||||
func (cache *Cache) SetWithTTL(key string, value interface{}, ttl time.Duration) {
|
||||
// An interface is only nil if both its value and its type are nil, however, passing a nil pointer as an interface{}
|
||||
// means that the interface itself is not nil, because the interface value is nil but not the type.
|
||||
if cache.forceNilInterfaceOnNilPointer {
|
||||
if value != nil && (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) {
|
||||
value = nil
|
||||
}
|
||||
}
|
||||
cache.mutex.Lock()
|
||||
entry, ok := cache.get(key)
|
||||
if !ok {
|
||||
// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
|
||||
// so might as well just not create it in the first place
|
||||
if ttl != NoExpiration && ttl < 1 {
|
||||
cache.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
// Cache entry doesn't exist, so we have to create a new one
|
||||
entry = &Entry{
|
||||
Key: key,
|
||||
Value: value,
|
||||
RelevantTimestamp: time.Now(),
|
||||
next: cache.head,
|
||||
}
|
||||
if cache.head == nil {
|
||||
cache.tail = entry
|
||||
} else {
|
||||
cache.head.previous = entry
|
||||
}
|
||||
cache.head = entry
|
||||
cache.entries[key] = entry
|
||||
if cache.maxMemoryUsage != NoMaxMemoryUsage {
|
||||
cache.memoryUsage += entry.SizeInBytes()
|
||||
}
|
||||
} else {
|
||||
// A negative TTL that isn't -1 (NoExpiration) or 0 is an entry that will expire instantly,
|
||||
// so might as well just delete it immediately instead of updating it
|
||||
if ttl != NoExpiration && ttl < 1 {
|
||||
cache.delete(key)
|
||||
cache.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
if cache.maxMemoryUsage != NoMaxMemoryUsage {
|
||||
// Subtract the old entry from the cache's memoryUsage
|
||||
cache.memoryUsage -= entry.SizeInBytes()
|
||||
}
|
||||
// Update existing entry's value
|
||||
entry.Value = value
|
||||
entry.RelevantTimestamp = time.Now()
|
||||
if cache.maxMemoryUsage != NoMaxMemoryUsage {
|
||||
// Add the memory usage of the new entry to the cache's memoryUsage
|
||||
cache.memoryUsage += entry.SizeInBytes()
|
||||
}
|
||||
// Because we just updated the entry, we need to move it back to HEAD
|
||||
cache.moveExistingEntryToHead(entry)
|
||||
}
|
||||
if ttl != NoExpiration {
|
||||
entry.Expiration = time.Now().Add(ttl).UnixNano()
|
||||
} else {
|
||||
entry.Expiration = NoExpiration
|
||||
}
|
||||
// If the cache doesn't have a maxSize/maxMemoryUsage, then there's no point
|
||||
// checking if we need to evict an entry, so we'll just return now
|
||||
if cache.maxSize == NoMaxSize && cache.maxMemoryUsage == NoMaxMemoryUsage {
|
||||
cache.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
// If there's a maxSize and the cache has more entries than the maxSize, evict
|
||||
if cache.maxSize != NoMaxSize && len(cache.entries) > cache.maxSize {
|
||||
cache.evict()
|
||||
}
|
||||
// If there's a maxMemoryUsage and the memoryUsage is above the maxMemoryUsage, evict
|
||||
if cache.maxMemoryUsage != NoMaxMemoryUsage && cache.memoryUsage > cache.maxMemoryUsage {
|
||||
for cache.memoryUsage > cache.maxMemoryUsage && len(cache.entries) > 0 {
|
||||
cache.evict()
|
||||
}
|
||||
}
|
||||
cache.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetAll creates or updates multiple values
|
||||
func (cache *Cache) SetAll(entries map[string]interface{}) {
|
||||
for key, value := range entries {
|
||||
cache.SetWithTTL(key, value, NoExpiration)
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves an entry using the key passed as parameter
|
||||
// If there is no such entry, the value returned will be nil and the boolean will be false
|
||||
// If there is an entry, the value returned will be the value cached and the boolean will be true
|
||||
func (cache *Cache) Get(key string) (interface{}, bool) {
|
||||
cache.mutex.Lock()
|
||||
entry, ok := cache.get(key)
|
||||
if !ok {
|
||||
cache.mutex.Unlock()
|
||||
cache.stats.Misses++
|
||||
return nil, false
|
||||
}
|
||||
if entry.Expired() {
|
||||
cache.stats.ExpiredKeys++
|
||||
cache.delete(key)
|
||||
cache.mutex.Unlock()
|
||||
return nil, false
|
||||
}
|
||||
cache.stats.Hits++
|
||||
if cache.evictionPolicy == LeastRecentlyUsed {
|
||||
entry.Accessed()
|
||||
if cache.head == entry {
|
||||
cache.mutex.Unlock()
|
||||
return entry.Value, true
|
||||
}
|
||||
// Because the eviction policy is LRU, we need to move the entry back to HEAD
|
||||
cache.moveExistingEntryToHead(entry)
|
||||
}
|
||||
cache.mutex.Unlock()
|
||||
return entry.Value, true
|
||||
}
|
||||
|
||||
// GetValue retrieves an entry using the key passed as parameter
|
||||
// Unlike Get, this function only returns the value
|
||||
func (cache *Cache) GetValue(key string) interface{} {
|
||||
value, _ := cache.Get(key)
|
||||
return value
|
||||
}
|
||||
|
||||
// GetByKeys retrieves multiple entries using the keys passed as parameter
|
||||
// All keys are returned in the map, regardless of whether they exist or not, however, entries that do not exist in the
|
||||
// cache will return nil, meaning that there is no way of determining whether a key genuinely has the value nil, or
|
||||
// whether it doesn't exist in the cache using only this function.
|
||||
func (cache *Cache) GetByKeys(keys []string) map[string]interface{} {
|
||||
entries := make(map[string]interface{})
|
||||
for _, key := range keys {
|
||||
entries[key], _ = cache.Get(key)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// GetAll retrieves all cache entries
|
||||
//
|
||||
// If the eviction policy is LeastRecentlyUsed, note that unlike Get and GetByKeys, this does not update the last access
|
||||
// timestamp. The reason for this is that since all cache entries will be accessed, updating the last access timestamp
|
||||
// would provide very little benefit while harming the ability to accurately determine the next key that will be evicted
|
||||
//
|
||||
// You should probably avoid using this if you have a lot of entries.
|
||||
//
|
||||
// GetKeysByPattern is a good alternative if you want to retrieve entries that you do not have the key for, as it only
|
||||
// retrieves the keys and does not trigger active eviction and has a parameter for setting a limit to the number of keys
|
||||
// you wish to retrieve.
|
||||
func (cache *Cache) GetAll() map[string]interface{} {
|
||||
entries := make(map[string]interface{})
|
||||
cache.mutex.Lock()
|
||||
for key, entry := range cache.entries {
|
||||
if entry.Expired() {
|
||||
cache.delete(key)
|
||||
continue
|
||||
}
|
||||
entries[key] = entry.Value
|
||||
}
|
||||
cache.stats.Hits += uint64(len(entries))
|
||||
cache.mutex.Unlock()
|
||||
return entries
|
||||
}
|
||||
|
||||
// GetKeysByPattern retrieves a slice of keys that match a given pattern
|
||||
// If the limit is set to 0, the entire cache will be searched for matching keys.
|
||||
// If the limit is above 0, the search will stop once the specified number of matching keys have been found.
|
||||
//
|
||||
// e.g.
|
||||
// cache.GetKeysByPattern("*some*", 0) will return all keys containing "some" in them
|
||||
// cache.GetKeysByPattern("*some*", 5) will return 5 keys (or less) containing "some" in them
|
||||
//
|
||||
// Note that GetKeysByPattern does not trigger active evictions, nor does it count as accessing the entry, the latter
|
||||
// only applying if the cache uses the LeastRecentlyUsed eviction policy.
|
||||
// The reason for that behavior is that these two (active eviction and access) only applies when you access the value
|
||||
// of the cache entry, and this function only returns the keys.
|
||||
func (cache *Cache) GetKeysByPattern(pattern string, limit int) []string {
|
||||
var matchingKeys []string
|
||||
cache.mutex.Lock()
|
||||
for key, value := range cache.entries {
|
||||
if value.Expired() {
|
||||
continue
|
||||
}
|
||||
if MatchPattern(pattern, key) {
|
||||
matchingKeys = append(matchingKeys, key)
|
||||
if limit > 0 && len(matchingKeys) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
cache.mutex.Unlock()
|
||||
return matchingKeys
|
||||
}
|
||||
|
||||
// Delete removes a key from the cache
|
||||
//
|
||||
// Returns false if the key did not exist.
|
||||
func (cache *Cache) Delete(key string) bool {
|
||||
cache.mutex.Lock()
|
||||
ok := cache.delete(key)
|
||||
cache.mutex.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// DeleteAll deletes multiple entries based on the keys passed as parameter
|
||||
//
|
||||
// Returns the number of keys deleted
|
||||
func (cache *Cache) DeleteAll(keys []string) int {
|
||||
numberOfKeysDeleted := 0
|
||||
cache.mutex.Lock()
|
||||
for _, key := range keys {
|
||||
if cache.delete(key) {
|
||||
numberOfKeysDeleted++
|
||||
}
|
||||
}
|
||||
cache.mutex.Unlock()
|
||||
return numberOfKeysDeleted
|
||||
}
|
||||
|
||||
// Count returns the total amount of entries in the cache, regardless of whether they're expired or not
|
||||
func (cache *Cache) Count() int {
|
||||
cache.mutex.RLock()
|
||||
count := len(cache.entries)
|
||||
cache.mutex.RUnlock()
|
||||
return count
|
||||
}
|
||||
|
||||
// Clear deletes all entries from the cache
|
||||
func (cache *Cache) Clear() {
|
||||
cache.mutex.Lock()
|
||||
cache.entries = make(map[string]*Entry)
|
||||
cache.memoryUsage = 0
|
||||
cache.head = nil
|
||||
cache.tail = nil
|
||||
cache.mutex.Unlock()
|
||||
}
|
||||
|
||||
// TTL returns the time until the cache entry specified by the key passed as parameter
|
||||
// will be deleted.
|
||||
func (cache *Cache) TTL(key string) (time.Duration, error) {
|
||||
cache.mutex.RLock()
|
||||
entry, ok := cache.get(key)
|
||||
cache.mutex.RUnlock()
|
||||
if !ok {
|
||||
return 0, ErrKeyDoesNotExist
|
||||
}
|
||||
if entry.Expiration == NoExpiration {
|
||||
return 0, ErrKeyHasNoExpiration
|
||||
}
|
||||
timeUntilExpiration := time.Until(time.Unix(0, entry.Expiration))
|
||||
if timeUntilExpiration < 0 {
|
||||
// The key has already expired but hasn't been deleted yet.
|
||||
// From the client's perspective, this means that the cache entry doesn't exist
|
||||
return 0, ErrKeyDoesNotExist
|
||||
}
|
||||
return timeUntilExpiration, nil
|
||||
}
|
||||
|
||||
// Expire sets a key's expiration time
|
||||
//
|
||||
// A TTL of -1 means that the key will never expire
|
||||
// A TTL of 0 means that the key will expire immediately
|
||||
// If using LRU, note that this does not reset the position of the key
|
||||
//
|
||||
// Returns true if the cache key exists and has had its expiration time altered
|
||||
func (cache *Cache) Expire(key string, ttl time.Duration) bool {
|
||||
entry, ok := cache.get(key)
|
||||
if !ok || entry.Expired() {
|
||||
return false
|
||||
}
|
||||
if ttl != NoExpiration {
|
||||
entry.Expiration = time.Now().Add(ttl).UnixNano()
|
||||
} else {
|
||||
entry.Expiration = NoExpiration
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// get retrieves an entry using the key passed as parameter, but unlike Get, it doesn't update the access time or
|
||||
// move the position of the entry to the head
|
||||
func (cache *Cache) get(key string) (*Entry, bool) {
|
||||
entry, ok := cache.entries[key]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
func (cache *Cache) delete(key string) bool {
|
||||
entry, ok := cache.entries[key]
|
||||
if ok {
|
||||
if cache.maxMemoryUsage != NoMaxMemoryUsage {
|
||||
cache.memoryUsage -= entry.SizeInBytes()
|
||||
}
|
||||
cache.removeExistingEntryReferences(entry)
|
||||
delete(cache.entries, key)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// moveExistingEntryToHead replaces the current cache head for an existing entry
|
||||
func (cache *Cache) moveExistingEntryToHead(entry *Entry) {
|
||||
if !(entry == cache.head && entry == cache.tail) {
|
||||
cache.removeExistingEntryReferences(entry)
|
||||
}
|
||||
if entry != cache.head {
|
||||
entry.next = cache.head
|
||||
entry.previous = nil
|
||||
if cache.head != nil {
|
||||
cache.head.previous = entry
|
||||
}
|
||||
cache.head = entry
|
||||
}
|
||||
}
|
||||
|
||||
// removeExistingEntryReferences modifies the next and previous reference of an existing entry and re-links
|
||||
// the next and previous entry accordingly, as well as the cache head or/and the cache tail if necessary.
|
||||
// Note that it does not remove the entry from the cache, only the references.
|
||||
func (cache *Cache) removeExistingEntryReferences(entry *Entry) {
|
||||
if cache.tail == entry && cache.head == entry {
|
||||
cache.tail = nil
|
||||
cache.head = nil
|
||||
} else if cache.tail == entry {
|
||||
cache.tail = cache.tail.previous
|
||||
} else if cache.head == entry {
|
||||
cache.head = cache.head.next
|
||||
}
|
||||
if entry.previous != nil {
|
||||
entry.previous.next = entry.next
|
||||
}
|
||||
if entry.next != nil {
|
||||
entry.next.previous = entry.previous
|
||||
}
|
||||
entry.next = nil
|
||||
entry.previous = nil
|
||||
}
|
||||
|
||||
// evict removes the tail from the cache
|
||||
func (cache *Cache) evict() {
|
||||
if cache.tail == nil || len(cache.entries) == 0 {
|
||||
return
|
||||
}
|
||||
if cache.tail != nil {
|
||||
oldTail := cache.tail
|
||||
cache.removeExistingEntryReferences(oldTail)
|
||||
delete(cache.entries, oldTail.Key)
|
||||
if cache.maxMemoryUsage != NoMaxMemoryUsage {
|
||||
cache.memoryUsage -= oldTail.SizeInBytes()
|
||||
}
|
||||
cache.stats.EvictedKeys++
|
||||
}
|
||||
}
|
146
vendor/github.com/TwiN/gocache/v2/janitor.go
generated
vendored
Normal file
146
vendor/github.com/TwiN/gocache/v2/janitor.go
generated
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
package gocache
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// JanitorShiftTarget is the target number of expired keys to find during passive clean up duty
|
||||
// before pausing the passive expired keys eviction process
|
||||
JanitorShiftTarget = 25
|
||||
|
||||
// JanitorMaxIterationsPerShift is the maximum number of nodes to traverse before pausing
|
||||
//
|
||||
// This is to prevent the janitor from traversing the entire cache, which could take a long time
|
||||
// to complete depending on the size of the cache.
|
||||
//
|
||||
// By limiting it to a small number, we are effectively reducing the impact of passive eviction.
|
||||
JanitorMaxIterationsPerShift = 1000
|
||||
|
||||
// JanitorMinShiftBackOff is the minimum interval between each iteration of steps
|
||||
// defined by JanitorMaxIterationsPerShift
|
||||
JanitorMinShiftBackOff = 50 * time.Millisecond
|
||||
|
||||
// JanitorMaxShiftBackOff is the maximum interval between each iteration of steps
|
||||
// defined by JanitorMaxIterationsPerShift
|
||||
JanitorMaxShiftBackOff = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
// StartJanitor starts the janitor on a different goroutine
|
||||
// The janitor's job is to delete expired keys in the background, in other words, it takes care of passive eviction.
|
||||
// It can be stopped by calling Cache.StopJanitor.
|
||||
// If you do not start the janitor, expired keys will only be deleted when they are accessed through Get, GetByKeys, or
|
||||
// GetAll.
|
||||
func (cache *Cache) StartJanitor() error {
|
||||
if cache.stopJanitor != nil {
|
||||
return ErrJanitorAlreadyRunning
|
||||
}
|
||||
cache.stopJanitor = make(chan bool)
|
||||
go func() {
|
||||
// rather than starting from the tail on every run, we can try to start from the last traversed entry
|
||||
var lastTraversedNode *Entry
|
||||
totalNumberOfExpiredKeysInPreviousRunFromTailToHead := 0
|
||||
backOff := JanitorMinShiftBackOff
|
||||
for {
|
||||
select {
|
||||
case <-time.After(backOff):
|
||||
// Passive clean up duty
|
||||
cache.mutex.Lock()
|
||||
if cache.tail != nil {
|
||||
start := time.Now()
|
||||
steps := 0
|
||||
expiredEntriesFound := 0
|
||||
current := cache.tail
|
||||
if lastTraversedNode != nil {
|
||||
// Make sure the lastTraversedNode is still in the cache, otherwise we might be traversing nodes that were already deleted.
|
||||
// Furthermore, we need to make sure that the entry from the cache has the same pointer as the lastTraversedNode
|
||||
// to verify that there isn't just a new cache entry with the same key (i.e. in case lastTraversedNode got evicted)
|
||||
if entryFromCache, isInCache := cache.get(lastTraversedNode.Key); isInCache && entryFromCache == lastTraversedNode {
|
||||
current = lastTraversedNode
|
||||
}
|
||||
}
|
||||
if current == cache.tail {
|
||||
if Debug {
|
||||
log.Printf("There are currently %d entries in the cache. The last walk resulted in finding %d expired keys", len(cache.entries), totalNumberOfExpiredKeysInPreviousRunFromTailToHead)
|
||||
}
|
||||
totalNumberOfExpiredKeysInPreviousRunFromTailToHead = 0
|
||||
}
|
||||
for current != nil {
|
||||
// since we're walking from the tail to the head, we get the previous reference
|
||||
var previous *Entry
|
||||
steps++
|
||||
if current.Expired() {
|
||||
expiredEntriesFound++
|
||||
// Because delete will remove the previous reference from the entry, we need to store the
|
||||
// previous reference before we delete it
|
||||
previous = current.previous
|
||||
cache.delete(current.Key)
|
||||
cache.stats.ExpiredKeys++
|
||||
}
|
||||
if current == cache.head {
|
||||
lastTraversedNode = nil
|
||||
break
|
||||
}
|
||||
// Travel to the current node's previous node only if no specific previous node has been specified
|
||||
if previous != nil {
|
||||
current = previous
|
||||
} else {
|
||||
current = current.previous
|
||||
}
|
||||
lastTraversedNode = current
|
||||
if steps == JanitorMaxIterationsPerShift || expiredEntriesFound >= JanitorShiftTarget {
|
||||
if expiredEntriesFound > 0 {
|
||||
backOff = JanitorMinShiftBackOff
|
||||
} else {
|
||||
if backOff*2 <= JanitorMaxShiftBackOff {
|
||||
backOff *= 2
|
||||
} else {
|
||||
backOff = JanitorMaxShiftBackOff
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if Debug {
|
||||
log.Printf("traversed %d nodes and found %d expired entries in %s before stopping\n", steps, expiredEntriesFound, time.Since(start))
|
||||
}
|
||||
totalNumberOfExpiredKeysInPreviousRunFromTailToHead += expiredEntriesFound
|
||||
} else {
|
||||
if backOff*2 < JanitorMaxShiftBackOff {
|
||||
backOff *= 2
|
||||
} else {
|
||||
backOff = JanitorMaxShiftBackOff
|
||||
}
|
||||
}
|
||||
cache.mutex.Unlock()
|
||||
case <-cache.stopJanitor:
|
||||
cache.stopJanitor <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
//if Debug {
|
||||
// go func() {
|
||||
// var m runtime.MemStats
|
||||
// for {
|
||||
// runtime.ReadMemStats(&m)
|
||||
// log.Printf("Alloc=%vMB; HeapReleased=%vMB; Sys=%vMB; HeapInUse=%vMB; HeapObjects=%v; HeapObjectsFreed=%v; GC=%v; cache.memoryUsage=%vMB; cacheSize=%d\n", m.Alloc/1024/1024, m.HeapReleased/1024/1024, m.Sys/1024/1024, m.HeapInuse/1024/1024, m.HeapObjects, m.Frees, m.NumGC, cache.memoryUsage/1024/1024, cache.Count())
|
||||
// time.Sleep(3 * time.Second)
|
||||
// }
|
||||
// }()
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopJanitor stops the janitor
|
||||
func (cache *Cache) StopJanitor() {
|
||||
if cache.stopJanitor != nil {
|
||||
// Tell the janitor to stop, and then wait for the janitor to reply on the same channel that it's stopping
|
||||
// This may seem a bit odd, but this allows us to avoid a data race condition when trying to set
|
||||
// cache.stopJanitor to nil
|
||||
cache.stopJanitor <- true
|
||||
<-cache.stopJanitor
|
||||
cache.stopJanitor = nil
|
||||
}
|
||||
}
|
12
vendor/github.com/TwiN/gocache/v2/pattern.go
generated
vendored
Normal file
12
vendor/github.com/TwiN/gocache/v2/pattern.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
package gocache
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// MatchPattern checks whether a string matches a pattern
|
||||
func MatchPattern(pattern, s string) bool {
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
matched, _ := filepath.Match(pattern, s)
|
||||
return matched
|
||||
}
|
33
vendor/github.com/TwiN/gocache/v2/policy.go
generated
vendored
Normal file
33
vendor/github.com/TwiN/gocache/v2/policy.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
package gocache
|
||||
|
||||
// EvictionPolicy is what dictates how evictions are handled
|
||||
type EvictionPolicy string
|
||||
|
||||
var (
|
||||
// LeastRecentlyUsed is an eviction policy that causes the most recently accessed cache entry to be moved to the
|
||||
// head of the cache. Effectively, this causes the cache entries that have not been accessed for some time to
|
||||
// gradually move closer and closer to the tail, and since the tail is the entry that gets deleted when an eviction
|
||||
// is required, it allows less used cache entries to be evicted while keeping recently accessed entries at or close
|
||||
// to the head.
|
||||
//
|
||||
// For instance, creating a Cache with a Cache.MaxSize of 3 and creating the entries 1, 2 and 3 in that order would
|
||||
// put 3 at the head and 1 at the tail:
|
||||
// 3 (head) -> 2 -> 1 (tail)
|
||||
// If the cache entry 1 was then accessed, 1 would become the head and 2 the tail:
|
||||
// 1 (head) -> 3 -> 2 (tail)
|
||||
// If a cache entry 4 was then created, because the Cache.MaxSize is 3, the tail (2) would then be evicted:
|
||||
// 4 (head) -> 1 -> 3 (tail)
|
||||
LeastRecentlyUsed EvictionPolicy = "LeastRecentlyUsed"
|
||||
|
||||
// FirstInFirstOut is an eviction policy that causes cache entries to be evicted in the same order that they are
|
||||
// created.
|
||||
//
|
||||
// For instance, creating a Cache with a Cache.MaxSize of 3 and creating the entries 1, 2 and 3 in that order would
|
||||
// put 3 at the head and 1 at the tail:
|
||||
// 3 (head) -> 2 -> 1 (tail)
|
||||
// If the cache entry 1 was then accessed, unlike with LeastRecentlyUsed, nothing would change:
|
||||
// 3 (head) -> 2 -> 1 (tail)
|
||||
// If a cache entry 4 was then created, because the Cache.MaxSize is 3, the tail (1) would then be evicted:
|
||||
// 4 (head) -> 3 -> 2 (tail)
|
||||
FirstInFirstOut EvictionPolicy = "FirstInFirstOut"
|
||||
)
|
15
vendor/github.com/TwiN/gocache/v2/statistics.go
generated
vendored
Normal file
15
vendor/github.com/TwiN/gocache/v2/statistics.go
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
package gocache
|
||||
|
||||
type Statistics struct {
|
||||
// EvictedKeys is the number of keys that were evicted
|
||||
EvictedKeys uint64
|
||||
|
||||
// ExpiredKeys is the number of keys that were automatically deleted as a result of expiring
|
||||
ExpiredKeys uint64
|
||||
|
||||
// Hits is the number of cache hits
|
||||
Hits uint64
|
||||
|
||||
// Misses is the number of cache misses
|
||||
Misses uint64
|
||||
}
|
202
vendor/github.com/coreos/go-oidc/v3/LICENSE
generated
vendored
Normal file
202
vendor/github.com/coreos/go-oidc/v3/LICENSE
generated
vendored
Normal file
@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
5
vendor/github.com/coreos/go-oidc/v3/NOTICE
generated
vendored
Normal file
5
vendor/github.com/coreos/go-oidc/v3/NOTICE
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
CoreOS Project
|
||||
Copyright 2014 CoreOS, Inc
|
||||
|
||||
This product includes software developed at CoreOS, Inc.
|
||||
(http://www.coreos.com/).
|
16
vendor/github.com/coreos/go-oidc/v3/oidc/jose.go
generated
vendored
Normal file
16
vendor/github.com/coreos/go-oidc/v3/oidc/jose.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
package oidc
|
||||
|
||||
// JOSE asymmetric signing algorithm values as defined by RFC 7518
|
||||
//
|
||||
// see: https://tools.ietf.org/html/rfc7518#section-3.1
|
||||
const (
|
||||
RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
|
||||
RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
|
||||
RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
|
||||
ES256 = "ES256" // ECDSA using P-256 and SHA-256
|
||||
ES384 = "ES384" // ECDSA using P-384 and SHA-384
|
||||
ES512 = "ES512" // ECDSA using P-521 and SHA-512
|
||||
PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
|
||||
PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
|
||||
PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
|
||||
)
|
208
vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go
generated
vendored
Normal file
208
vendor/github.com/coreos/go-oidc/v3/oidc/jwks.go
generated
vendored
Normal file
@ -0,0 +1,208 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
// NewRemoteKeySet returns a KeySet that can validate JSON web tokens by using HTTP
|
||||
// GETs to fetch JSON web token sets hosted at a remote URL. This is automatically
|
||||
// used by NewProvider using the URLs returned by OpenID Connect discovery, but is
|
||||
// exposed for providers that don't support discovery or to prevent round trips to the
|
||||
// discovery URL.
|
||||
//
|
||||
// The returned KeySet is a long lived verifier that caches keys based on any
|
||||
// keys change. Reuse a common remote key set instead of creating new ones as needed.
|
||||
func NewRemoteKeySet(ctx context.Context, jwksURL string) *RemoteKeySet {
|
||||
return newRemoteKeySet(ctx, jwksURL, time.Now)
|
||||
}
|
||||
|
||||
func newRemoteKeySet(ctx context.Context, jwksURL string, now func() time.Time) *RemoteKeySet {
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
return &RemoteKeySet{jwksURL: jwksURL, ctx: cloneContext(ctx), now: now}
|
||||
}
|
||||
|
||||
// RemoteKeySet is a KeySet implementation that validates JSON web tokens against
|
||||
// a jwks_uri endpoint.
|
||||
type RemoteKeySet struct {
|
||||
jwksURL string
|
||||
ctx context.Context
|
||||
now func() time.Time
|
||||
|
||||
// guard all other fields
|
||||
mu sync.RWMutex
|
||||
|
||||
// inflight suppresses parallel execution of updateKeys and allows
|
||||
// multiple goroutines to wait for its result.
|
||||
inflight *inflight
|
||||
|
||||
// A set of cached keys.
|
||||
cachedKeys []jose.JSONWebKey
|
||||
}
|
||||
|
||||
// inflight is used to wait on some in-flight request from multiple goroutines.
|
||||
type inflight struct {
|
||||
doneCh chan struct{}
|
||||
|
||||
keys []jose.JSONWebKey
|
||||
err error
|
||||
}
|
||||
|
||||
func newInflight() *inflight {
|
||||
return &inflight{doneCh: make(chan struct{})}
|
||||
}
|
||||
|
||||
// wait returns a channel that multiple goroutines can receive on. Once it returns
|
||||
// a value, the inflight request is done and result() can be inspected.
|
||||
func (i *inflight) wait() <-chan struct{} {
|
||||
return i.doneCh
|
||||
}
|
||||
|
||||
// done can only be called by a single goroutine. It records the result of the
|
||||
// inflight request and signals other goroutines that the result is safe to
|
||||
// inspect.
|
||||
func (i *inflight) done(keys []jose.JSONWebKey, err error) {
|
||||
i.keys = keys
|
||||
i.err = err
|
||||
close(i.doneCh)
|
||||
}
|
||||
|
||||
// result cannot be called until the wait() channel has returned a value.
|
||||
func (i *inflight) result() ([]jose.JSONWebKey, error) {
|
||||
return i.keys, i.err
|
||||
}
|
||||
|
||||
// VerifySignature validates a payload against a signature from the jwks_uri.
|
||||
//
|
||||
// Users MUST NOT call this method directly and should use an IDTokenVerifier
|
||||
// instead. This method skips critical validations such as 'alg' values and is
|
||||
// only exported to implement the KeySet interface.
|
||||
func (r *RemoteKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
|
||||
jws, err := jose.ParseSigned(jwt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||
}
|
||||
return r.verify(ctx, jws)
|
||||
}
|
||||
|
||||
func (r *RemoteKeySet) verify(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
|
||||
// We don't support JWTs signed with multiple signatures.
|
||||
keyID := ""
|
||||
for _, sig := range jws.Signatures {
|
||||
keyID = sig.Header.KeyID
|
||||
break
|
||||
}
|
||||
|
||||
keys := r.keysFromCache()
|
||||
for _, key := range keys {
|
||||
if keyID == "" || key.KeyID == keyID {
|
||||
if payload, err := jws.Verify(&key); err == nil {
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the kid doesn't match, check for new keys from the remote. This is the
|
||||
// strategy recommended by the spec.
|
||||
//
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys
|
||||
keys, err := r.keysFromRemote(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching keys %v", err)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if keyID == "" || key.KeyID == keyID {
|
||||
if payload, err := jws.Verify(&key); err == nil {
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to verify id token signature")
|
||||
}
|
||||
|
||||
func (r *RemoteKeySet) keysFromCache() (keys []jose.JSONWebKey) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.cachedKeys
|
||||
}
|
||||
|
||||
// keysFromRemote syncs the key set from the remote set, records the values in the
|
||||
// cache, and returns the key set.
|
||||
func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, error) {
|
||||
// Need to lock to inspect the inflight request field.
|
||||
r.mu.Lock()
|
||||
// If there's not a current inflight request, create one.
|
||||
if r.inflight == nil {
|
||||
r.inflight = newInflight()
|
||||
|
||||
// This goroutine has exclusive ownership over the current inflight
|
||||
// request. It releases the resource by nil'ing the inflight field
|
||||
// once the goroutine is done.
|
||||
go func() {
|
||||
// Sync keys and finish inflight when that's done.
|
||||
keys, err := r.updateKeys()
|
||||
|
||||
r.inflight.done(keys, err)
|
||||
|
||||
// Lock to update the keys and indicate that there is no longer an
|
||||
// inflight request.
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if err == nil {
|
||||
r.cachedKeys = keys
|
||||
}
|
||||
|
||||
// Free inflight so a different request can run.
|
||||
r.inflight = nil
|
||||
}()
|
||||
}
|
||||
inflight := r.inflight
|
||||
r.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-inflight.wait():
|
||||
return inflight.result()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) {
|
||||
req, err := http.NewRequest("GET", r.jwksURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: can't create request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := doRequest(r.ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: get keys failed %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
|
||||
}
|
||||
|
||||
var keySet jose.JSONWebKeySet
|
||||
err = unmarshalResp(resp, body, &keySet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)
|
||||
}
|
||||
return keySet.Keys, nil
|
||||
}
|
480
vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go
generated
vendored
Normal file
480
vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go
generated
vendored
Normal file
@ -0,0 +1,480 @@
|
||||
// Package oidc implements OpenID Connect client logic for the golang.org/x/oauth2 package.
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// ScopeOpenID is the mandatory scope for all OpenID Connect OAuth2 requests.
|
||||
ScopeOpenID = "openid"
|
||||
|
||||
// ScopeOfflineAccess is an optional scope defined by OpenID Connect for requesting
|
||||
// OAuth2 refresh tokens.
|
||||
//
|
||||
// Support for this scope differs between OpenID Connect providers. For instance
|
||||
// Google rejects it, favoring appending "access_type=offline" as part of the
|
||||
// authorization request instead.
|
||||
//
|
||||
// See: https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
ScopeOfflineAccess = "offline_access"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoAtHash = errors.New("id token did not have an access token hash")
|
||||
errInvalidAtHash = errors.New("access token hash does not match value in ID token")
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
var issuerURLKey contextKey
|
||||
|
||||
// ClientContext returns a new Context that carries the provided HTTP client.
|
||||
//
|
||||
// This method sets the same context key used by the golang.org/x/oauth2 package,
|
||||
// so the returned context works for that package too.
|
||||
//
|
||||
// myClient := &http.Client{}
|
||||
// ctx := oidc.ClientContext(parentContext, myClient)
|
||||
//
|
||||
// // This will use the custom client
|
||||
// provider, err := oidc.NewProvider(ctx, "https://accounts.example.com")
|
||||
//
|
||||
func ClientContext(ctx context.Context, client *http.Client) context.Context {
|
||||
return context.WithValue(ctx, oauth2.HTTPClient, client)
|
||||
}
|
||||
|
||||
// cloneContext copies a context's bag-of-values into a new context that isn't
|
||||
// associated with its cancellation. This is used to initialize remote keys sets
|
||||
// which run in the background and aren't associated with the initial context.
|
||||
func cloneContext(ctx context.Context) context.Context {
|
||||
cp := context.Background()
|
||||
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
|
||||
cp = ClientContext(cp, c)
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
// InsecureIssuerURLContext allows discovery to work when the issuer_url reported
|
||||
// by upstream is mismatched with the discovery URL. This is meant for integration
|
||||
// with off-spec providers such as Azure.
|
||||
//
|
||||
// discoveryBaseURL := "https://login.microsoftonline.com/organizations/v2.0"
|
||||
// issuerURL := "https://login.microsoftonline.com/my-tenantid/v2.0"
|
||||
//
|
||||
// ctx := oidc.InsecureIssuerURLContext(parentContext, issuerURL)
|
||||
//
|
||||
// // Provider will be discovered with the discoveryBaseURL, but use issuerURL
|
||||
// // for future issuer validation.
|
||||
// provider, err := oidc.NewProvider(ctx, discoveryBaseURL)
|
||||
//
|
||||
// This is insecure because validating the correct issuer is critical for multi-tenant
|
||||
// proivders. Any overrides here MUST be carefully reviewed.
|
||||
func InsecureIssuerURLContext(ctx context.Context, issuerURL string) context.Context {
|
||||
return context.WithValue(ctx, issuerURLKey, issuerURL)
|
||||
}
|
||||
|
||||
func doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
client := http.DefaultClient
|
||||
if c, ok := ctx.Value(oauth2.HTTPClient).(*http.Client); ok {
|
||||
client = c
|
||||
}
|
||||
return client.Do(req.WithContext(ctx))
|
||||
}
|
||||
|
||||
// Provider represents an OpenID Connect server's configuration.
|
||||
type Provider struct {
|
||||
issuer string
|
||||
authURL string
|
||||
tokenURL string
|
||||
userInfoURL string
|
||||
algorithms []string
|
||||
|
||||
// Raw claims returned by the server.
|
||||
rawClaims []byte
|
||||
|
||||
remoteKeySet KeySet
|
||||
}
|
||||
|
||||
type providerJSON struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthURL string `json:"authorization_endpoint"`
|
||||
TokenURL string `json:"token_endpoint"`
|
||||
JWKSURL string `json:"jwks_uri"`
|
||||
UserInfoURL string `json:"userinfo_endpoint"`
|
||||
Algorithms []string `json:"id_token_signing_alg_values_supported"`
|
||||
}
|
||||
|
||||
// supportedAlgorithms is a list of algorithms explicitly supported by this
|
||||
// package. If a provider supports other algorithms, such as HS256 or none,
|
||||
// those values won't be passed to the IDTokenVerifier.
|
||||
var supportedAlgorithms = map[string]bool{
|
||||
RS256: true,
|
||||
RS384: true,
|
||||
RS512: true,
|
||||
ES256: true,
|
||||
ES384: true,
|
||||
ES512: true,
|
||||
PS256: true,
|
||||
PS384: true,
|
||||
PS512: true,
|
||||
}
|
||||
|
||||
// NewProvider uses the OpenID Connect discovery mechanism to construct a Provider.
|
||||
//
|
||||
// The issuer is the URL identifier for the service. For example: "https://accounts.google.com"
|
||||
// or "https://login.salesforce.com".
|
||||
func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
|
||||
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
|
||||
req, err := http.NewRequest("GET", wellKnown, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := doRequest(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
var p providerJSON
|
||||
err = unmarshalResp(resp, body, &p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
||||
}
|
||||
|
||||
issuerURL, skipIssuerValidation := ctx.Value(issuerURLKey).(string)
|
||||
if !skipIssuerValidation {
|
||||
issuerURL = issuer
|
||||
}
|
||||
if p.Issuer != issuerURL && !skipIssuerValidation {
|
||||
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuer, p.Issuer)
|
||||
}
|
||||
var algs []string
|
||||
for _, a := range p.Algorithms {
|
||||
if supportedAlgorithms[a] {
|
||||
algs = append(algs, a)
|
||||
}
|
||||
}
|
||||
return &Provider{
|
||||
issuer: issuerURL,
|
||||
authURL: p.AuthURL,
|
||||
tokenURL: p.TokenURL,
|
||||
userInfoURL: p.UserInfoURL,
|
||||
algorithms: algs,
|
||||
rawClaims: body,
|
||||
remoteKeySet: NewRemoteKeySet(cloneContext(ctx), p.JWKSURL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Claims unmarshals raw fields returned by the server during discovery.
|
||||
//
|
||||
// var claims struct {
|
||||
// ScopesSupported []string `json:"scopes_supported"`
|
||||
// ClaimsSupported []string `json:"claims_supported"`
|
||||
// }
|
||||
//
|
||||
// if err := provider.Claims(&claims); err != nil {
|
||||
// // handle unmarshaling error
|
||||
// }
|
||||
//
|
||||
// For a list of fields defined by the OpenID Connect spec see:
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
func (p *Provider) Claims(v interface{}) error {
|
||||
if p.rawClaims == nil {
|
||||
return errors.New("oidc: claims not set")
|
||||
}
|
||||
return json.Unmarshal(p.rawClaims, v)
|
||||
}
|
||||
|
||||
// Endpoint returns the OAuth2 auth and token endpoints for the given provider.
|
||||
func (p *Provider) Endpoint() oauth2.Endpoint {
|
||||
return oauth2.Endpoint{AuthURL: p.authURL, TokenURL: p.tokenURL}
|
||||
}
|
||||
|
||||
// UserInfo represents the OpenID Connect userinfo claims.
|
||||
type UserInfo struct {
|
||||
Subject string `json:"sub"`
|
||||
Profile string `json:"profile"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
|
||||
claims []byte
|
||||
}
|
||||
|
||||
type userInfoRaw struct {
|
||||
Subject string `json:"sub"`
|
||||
Profile string `json:"profile"`
|
||||
Email string `json:"email"`
|
||||
// Handle providers that return email_verified as a string
|
||||
// https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁 and
|
||||
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
|
||||
EmailVerified stringAsBool `json:"email_verified"`
|
||||
}
|
||||
|
||||
// Claims unmarshals the raw JSON object claims into the provided object.
|
||||
func (u *UserInfo) Claims(v interface{}) error {
|
||||
if u.claims == nil {
|
||||
return errors.New("oidc: claims not set")
|
||||
}
|
||||
return json.Unmarshal(u.claims, v)
|
||||
}
|
||||
|
||||
// UserInfo uses the token source to query the provider's user info endpoint.
|
||||
func (p *Provider) UserInfo(ctx context.Context, tokenSource oauth2.TokenSource) (*UserInfo, error) {
|
||||
if p.userInfoURL == "" {
|
||||
return nil, errors.New("oidc: user info endpoint is not supported by this provider")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", p.userInfoURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: create GET request: %v", err)
|
||||
}
|
||||
|
||||
token, err := tokenSource.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: get access token: %v", err)
|
||||
}
|
||||
token.SetAuthHeader(req)
|
||||
|
||||
resp, err := doRequest(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
mediaType, _, parseErr := mime.ParseMediaType(ct)
|
||||
if parseErr == nil && mediaType == "application/jwt" {
|
||||
payload, err := p.remoteKeySet.VerifySignature(ctx, string(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: invalid userinfo jwt signature %v", err)
|
||||
}
|
||||
body = payload
|
||||
}
|
||||
|
||||
var userInfo userInfoRaw
|
||||
if err := json.Unmarshal(body, &userInfo); err != nil {
|
||||
return nil, fmt.Errorf("oidc: failed to decode userinfo: %v", err)
|
||||
}
|
||||
return &UserInfo{
|
||||
Subject: userInfo.Subject,
|
||||
Profile: userInfo.Profile,
|
||||
Email: userInfo.Email,
|
||||
EmailVerified: bool(userInfo.EmailVerified),
|
||||
claims: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IDToken is an OpenID Connect extension that provides a predictable representation
|
||||
// of an authorization event.
|
||||
//
|
||||
// The ID Token only holds fields OpenID Connect requires. To access additional
|
||||
// claims returned by the server, use the Claims method.
|
||||
type IDToken struct {
|
||||
// The URL of the server which issued this token. OpenID Connect
|
||||
// requires this value always be identical to the URL used for
|
||||
// initial discovery.
|
||||
//
|
||||
// Note: Because of a known issue with Google Accounts' implementation
|
||||
// this value may differ when using Google.
|
||||
//
|
||||
// See: https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo
|
||||
Issuer string
|
||||
|
||||
// The client ID, or set of client IDs, that this token is issued for. For
|
||||
// common uses, this is the client that initialized the auth flow.
|
||||
//
|
||||
// This package ensures the audience contains an expected value.
|
||||
Audience []string
|
||||
|
||||
// A unique string which identifies the end user.
|
||||
Subject string
|
||||
|
||||
// Expiry of the token. Ths package will not process tokens that have
|
||||
// expired unless that validation is explicitly turned off.
|
||||
Expiry time.Time
|
||||
// When the token was issued by the provider.
|
||||
IssuedAt time.Time
|
||||
|
||||
// Initial nonce provided during the authentication redirect.
|
||||
//
|
||||
// This package does NOT provided verification on the value of this field
|
||||
// and it's the user's responsibility to ensure it contains a valid value.
|
||||
Nonce string
|
||||
|
||||
// at_hash claim, if set in the ID token. Callers can verify an access token
|
||||
// that corresponds to the ID token using the VerifyAccessToken method.
|
||||
AccessTokenHash string
|
||||
|
||||
// signature algorithm used for ID token, needed to compute a verification hash of an
|
||||
// access token
|
||||
sigAlgorithm string
|
||||
|
||||
// Raw payload of the id_token.
|
||||
claims []byte
|
||||
|
||||
// Map of distributed claim names to claim sources
|
||||
distributedClaims map[string]claimSource
|
||||
}
|
||||
|
||||
// Claims unmarshals the raw JSON payload of the ID Token into a provided struct.
|
||||
//
|
||||
// idToken, err := idTokenVerifier.Verify(rawIDToken)
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// var claims struct {
|
||||
// Email string `json:"email"`
|
||||
// EmailVerified bool `json:"email_verified"`
|
||||
// }
|
||||
// if err := idToken.Claims(&claims); err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
func (i *IDToken) Claims(v interface{}) error {
|
||||
if i.claims == nil {
|
||||
return errors.New("oidc: claims not set")
|
||||
}
|
||||
return json.Unmarshal(i.claims, v)
|
||||
}
|
||||
|
||||
// VerifyAccessToken verifies that the hash of the access token that corresponds to the iD token
|
||||
// matches the hash in the id token. It returns an error if the hashes don't match.
|
||||
// It is the caller's responsibility to ensure that the optional access token hash is present for the ID token
|
||||
// before calling this method. See https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||
func (i *IDToken) VerifyAccessToken(accessToken string) error {
|
||||
if i.AccessTokenHash == "" {
|
||||
return errNoAtHash
|
||||
}
|
||||
var h hash.Hash
|
||||
switch i.sigAlgorithm {
|
||||
case RS256, ES256, PS256:
|
||||
h = sha256.New()
|
||||
case RS384, ES384, PS384:
|
||||
h = sha512.New384()
|
||||
case RS512, ES512, PS512:
|
||||
h = sha512.New()
|
||||
default:
|
||||
return fmt.Errorf("oidc: unsupported signing algorithm %q", i.sigAlgorithm)
|
||||
}
|
||||
h.Write([]byte(accessToken)) // hash documents that Write will never return an error
|
||||
sum := h.Sum(nil)[:h.Size()/2]
|
||||
actual := base64.RawURLEncoding.EncodeToString(sum)
|
||||
if actual != i.AccessTokenHash {
|
||||
return errInvalidAtHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type idToken struct {
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Audience audience `json:"aud"`
|
||||
Expiry jsonTime `json:"exp"`
|
||||
IssuedAt jsonTime `json:"iat"`
|
||||
NotBefore *jsonTime `json:"nbf"`
|
||||
Nonce string `json:"nonce"`
|
||||
AtHash string `json:"at_hash"`
|
||||
ClaimNames map[string]string `json:"_claim_names"`
|
||||
ClaimSources map[string]claimSource `json:"_claim_sources"`
|
||||
}
|
||||
|
||||
type claimSource struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
type stringAsBool bool
|
||||
|
||||
func (sb *stringAsBool) UnmarshalJSON(b []byte) error {
|
||||
switch string(b) {
|
||||
case "true", `"true"`:
|
||||
*sb = true
|
||||
case "false", `"false"`:
|
||||
*sb = false
|
||||
default:
|
||||
return errors.New("invalid value for boolean")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type audience []string
|
||||
|
||||
func (a *audience) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if json.Unmarshal(b, &s) == nil {
|
||||
*a = audience{s}
|
||||
return nil
|
||||
}
|
||||
var auds []string
|
||||
if err := json.Unmarshal(b, &auds); err != nil {
|
||||
return err
|
||||
}
|
||||
*a = auds
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonTime time.Time
|
||||
|
||||
func (j *jsonTime) UnmarshalJSON(b []byte) error {
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
return err
|
||||
}
|
||||
var unix int64
|
||||
|
||||
if t, err := n.Int64(); err == nil {
|
||||
unix = t
|
||||
} else {
|
||||
f, err := n.Float64()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unix = int64(f)
|
||||
}
|
||||
*j = jsonTime(time.Unix(unix, 0))
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalResp(r *http.Response, body []byte, v interface{}) error {
|
||||
err := json.Unmarshal(body, &v)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
ct := r.Header.Get("Content-Type")
|
||||
mediaType, _, parseErr := mime.ParseMediaType(ct)
|
||||
if parseErr == nil && mediaType == "application/json" {
|
||||
return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %v", err)
|
||||
}
|
||||
return fmt.Errorf("expected Content-Type = application/json, got %q: %v", ct, err)
|
||||
}
|
322
vendor/github.com/coreos/go-oidc/v3/oidc/verify.go
generated
vendored
Normal file
322
vendor/github.com/coreos/go-oidc/v3/oidc/verify.go
generated
vendored
Normal file
@ -0,0 +1,322 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
issuerGoogleAccounts = "https://accounts.google.com"
|
||||
issuerGoogleAccountsNoScheme = "accounts.google.com"
|
||||
)
|
||||
|
||||
// KeySet is a set of publc JSON Web Keys that can be used to validate the signature
|
||||
// of JSON web tokens. This is expected to be backed by a remote key set through
|
||||
// provider metadata discovery or an in-memory set of keys delivered out-of-band.
|
||||
type KeySet interface {
|
||||
// VerifySignature parses the JSON web token, verifies the signature, and returns
|
||||
// the raw payload. Header and claim fields are validated by other parts of the
|
||||
// package. For example, the KeySet does not need to check values such as signature
|
||||
// algorithm, issuer, and audience since the IDTokenVerifier validates these values
|
||||
// independently.
|
||||
//
|
||||
// If VerifySignature makes HTTP requests to verify the token, it's expected to
|
||||
// use any HTTP client associated with the context through ClientContext.
|
||||
VerifySignature(ctx context.Context, jwt string) (payload []byte, err error)
|
||||
}
|
||||
|
||||
// IDTokenVerifier provides verification for ID Tokens.
|
||||
type IDTokenVerifier struct {
|
||||
keySet KeySet
|
||||
config *Config
|
||||
issuer string
|
||||
}
|
||||
|
||||
// NewVerifier returns a verifier manually constructed from a key set and issuer URL.
|
||||
//
|
||||
// It's easier to use provider discovery to construct an IDTokenVerifier than creating
|
||||
// one directly. This method is intended to be used with provider that don't support
|
||||
// metadata discovery, or avoiding round trips when the key set URL is already known.
|
||||
//
|
||||
// This constructor can be used to create a verifier directly using the issuer URL and
|
||||
// JSON Web Key Set URL without using discovery:
|
||||
//
|
||||
// keySet := oidc.NewRemoteKeySet(ctx, "https://www.googleapis.com/oauth2/v3/certs")
|
||||
// verifier := oidc.NewVerifier("https://accounts.google.com", keySet, config)
|
||||
//
|
||||
// Since KeySet is an interface, this constructor can also be used to supply custom
|
||||
// public key sources. For example, if a user wanted to supply public keys out-of-band
|
||||
// and hold them statically in-memory:
|
||||
//
|
||||
// // Custom KeySet implementation.
|
||||
// keySet := newStatisKeySet(publicKeys...)
|
||||
//
|
||||
// // Verifier uses the custom KeySet implementation.
|
||||
// verifier := oidc.NewVerifier("https://auth.example.com", keySet, config)
|
||||
//
|
||||
func NewVerifier(issuerURL string, keySet KeySet, config *Config) *IDTokenVerifier {
|
||||
return &IDTokenVerifier{keySet: keySet, config: config, issuer: issuerURL}
|
||||
}
|
||||
|
||||
// Config is the configuration for an IDTokenVerifier.
|
||||
type Config struct {
|
||||
// Expected audience of the token. For a majority of the cases this is expected to be
|
||||
// the ID of the client that initialized the login flow. It may occasionally differ if
|
||||
// the provider supports the authorizing party (azp) claim.
|
||||
//
|
||||
// If not provided, users must explicitly set SkipClientIDCheck.
|
||||
ClientID string
|
||||
// If specified, only this set of algorithms may be used to sign the JWT.
|
||||
//
|
||||
// If the IDTokenVerifier is created from a provider with (*Provider).Verifier, this
|
||||
// defaults to the set of algorithms the provider supports. Otherwise this values
|
||||
// defaults to RS256.
|
||||
SupportedSigningAlgs []string
|
||||
|
||||
// If true, no ClientID check performed. Must be true if ClientID field is empty.
|
||||
SkipClientIDCheck bool
|
||||
// If true, token expiry is not checked.
|
||||
SkipExpiryCheck bool
|
||||
|
||||
// SkipIssuerCheck is intended for specialized cases where the the caller wishes to
|
||||
// defer issuer validation. When enabled, callers MUST independently verify the Token's
|
||||
// Issuer is a known good value.
|
||||
//
|
||||
// Mismatched issuers often indicate client mis-configuration. If mismatches are
|
||||
// unexpected, evaluate if the provided issuer URL is incorrect instead of enabling
|
||||
// this option.
|
||||
SkipIssuerCheck bool
|
||||
|
||||
// Time function to check Token expiry. Defaults to time.Now
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// Verifier returns an IDTokenVerifier that uses the provider's key set to verify JWTs.
|
||||
//
|
||||
// The returned IDTokenVerifier is tied to the Provider's context and its behavior is
|
||||
// undefined once the Provider's context is canceled.
|
||||
func (p *Provider) Verifier(config *Config) *IDTokenVerifier {
|
||||
if len(config.SupportedSigningAlgs) == 0 && len(p.algorithms) > 0 {
|
||||
// Make a copy so we don't modify the config values.
|
||||
cp := &Config{}
|
||||
*cp = *config
|
||||
cp.SupportedSigningAlgs = p.algorithms
|
||||
config = cp
|
||||
}
|
||||
return NewVerifier(p.issuer, p.remoteKeySet, config)
|
||||
}
|
||||
|
||||
func parseJWT(p string) ([]byte, error) {
|
||||
parts := strings.Split(p, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts))
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func contains(sli []string, ele string) bool {
|
||||
for _, s := range sli {
|
||||
if s == ele {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns the Claims from the distributed JWT token
|
||||
func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src claimSource) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", src.Endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed request: %v", err)
|
||||
}
|
||||
if src.AccessToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+src.AccessToken)
|
||||
}
|
||||
|
||||
resp, err := doRequest(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: Request to endpoint failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("oidc: request failed: %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
token, err := verifier.Verify(ctx, string(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed response body: %v", err)
|
||||
}
|
||||
|
||||
return token.claims, nil
|
||||
}
|
||||
|
||||
// Verify parses a raw ID Token, verifies it's been signed by the provider, performs
|
||||
// any additional checks depending on the Config, and returns the payload.
|
||||
//
|
||||
// Verify does NOT do nonce validation, which is the callers responsibility.
|
||||
//
|
||||
// See: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
//
|
||||
// oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// // Extract the ID Token from oauth2 token.
|
||||
// rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
// if !ok {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// token, err := verifier.Verify(ctx, rawIDToken)
|
||||
//
|
||||
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) {
|
||||
jws, err := jose.ParseSigned(rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||
}
|
||||
|
||||
// Throw out tokens with invalid claims before trying to verify the token. This lets
|
||||
// us do cheap checks before possibly re-syncing keys.
|
||||
payload, err := parseJWT(rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
|
||||
}
|
||||
var token idToken
|
||||
if err := json.Unmarshal(payload, &token); err != nil {
|
||||
return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err)
|
||||
}
|
||||
|
||||
distributedClaims := make(map[string]claimSource)
|
||||
|
||||
//step through the token to map claim names to claim sources"
|
||||
for cn, src := range token.ClaimNames {
|
||||
if src == "" {
|
||||
return nil, fmt.Errorf("oidc: failed to obtain source from claim name")
|
||||
}
|
||||
s, ok := token.ClaimSources[src]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("oidc: source does not exist")
|
||||
}
|
||||
distributedClaims[cn] = s
|
||||
}
|
||||
|
||||
t := &IDToken{
|
||||
Issuer: token.Issuer,
|
||||
Subject: token.Subject,
|
||||
Audience: []string(token.Audience),
|
||||
Expiry: time.Time(token.Expiry),
|
||||
IssuedAt: time.Time(token.IssuedAt),
|
||||
Nonce: token.Nonce,
|
||||
AccessTokenHash: token.AtHash,
|
||||
claims: payload,
|
||||
distributedClaims: distributedClaims,
|
||||
}
|
||||
|
||||
// Check issuer.
|
||||
if !v.config.SkipIssuerCheck && t.Issuer != v.issuer {
|
||||
// Google sometimes returns "accounts.google.com" as the issuer claim instead of
|
||||
// the required "https://accounts.google.com". Detect this case and allow it only
|
||||
// for Google.
|
||||
//
|
||||
// We will not add hooks to let other providers go off spec like this.
|
||||
if !(v.issuer == issuerGoogleAccounts && t.Issuer == issuerGoogleAccountsNoScheme) {
|
||||
return nil, fmt.Errorf("oidc: id token issued by a different provider, expected %q got %q", v.issuer, t.Issuer)
|
||||
}
|
||||
}
|
||||
|
||||
// If a client ID has been provided, make sure it's part of the audience. SkipClientIDCheck must be true if ClientID is empty.
|
||||
//
|
||||
// This check DOES NOT ensure that the ClientID is the party to which the ID Token was issued (i.e. Authorized party).
|
||||
if !v.config.SkipClientIDCheck {
|
||||
if v.config.ClientID != "" {
|
||||
if !contains(t.Audience, v.config.ClientID) {
|
||||
return nil, fmt.Errorf("oidc: expected audience %q got %q", v.config.ClientID, t.Audience)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("oidc: invalid configuration, clientID must be provided or SkipClientIDCheck must be set")
|
||||
}
|
||||
}
|
||||
|
||||
// If a SkipExpiryCheck is false, make sure token is not expired.
|
||||
if !v.config.SkipExpiryCheck {
|
||||
now := time.Now
|
||||
if v.config.Now != nil {
|
||||
now = v.config.Now
|
||||
}
|
||||
nowTime := now()
|
||||
|
||||
if t.Expiry.Before(nowTime) {
|
||||
return nil, fmt.Errorf("oidc: token is expired (Token Expiry: %v)", t.Expiry)
|
||||
}
|
||||
|
||||
// If nbf claim is provided in token, ensure that it is indeed in the past.
|
||||
if token.NotBefore != nil {
|
||||
nbfTime := time.Time(*token.NotBefore)
|
||||
leeway := 1 * time.Minute
|
||||
|
||||
if nowTime.Add(leeway).Before(nbfTime) {
|
||||
return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch len(jws.Signatures) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("oidc: id token not signed")
|
||||
case 1:
|
||||
default:
|
||||
return nil, fmt.Errorf("oidc: multiple signatures on id token not supported")
|
||||
}
|
||||
|
||||
sig := jws.Signatures[0]
|
||||
supportedSigAlgs := v.config.SupportedSigningAlgs
|
||||
if len(supportedSigAlgs) == 0 {
|
||||
supportedSigAlgs = []string{RS256}
|
||||
}
|
||||
|
||||
if !contains(supportedSigAlgs, sig.Header.Algorithm) {
|
||||
return nil, fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
|
||||
}
|
||||
|
||||
t.sigAlgorithm = sig.Header.Algorithm
|
||||
|
||||
gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify signature: %v", err)
|
||||
}
|
||||
|
||||
// Ensure that the payload returned by the square actually matches the payload parsed earlier.
|
||||
if !bytes.Equal(gotPayload, payload) {
|
||||
return nil, errors.New("oidc: internal error, payload parsed did not match previous payload")
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Nonce returns an auth code option which requires the ID Token created by the
|
||||
// OpenID Connect provider to contain the specified nonce.
|
||||
func Nonce(nonce string) oauth2.AuthCodeOption {
|
||||
return oauth2.SetAuthURLParam("nonce", nonce)
|
||||
}
|
Reference in New Issue
Block a user