mirror of
https://github.com/hanzo-fi/webhooks.git
synced 2026-10-30 20:32:55 +00:00
252 lines
6.0 KiB
Go
252 lines
6.0 KiB
Go
// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
|
|
|
|
package hooks
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/hanzo-fi/webhooks/pkg/client/models/components"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type session struct {
|
|
Credentials *credentials
|
|
Token string
|
|
ExpiresAt *int64
|
|
Scopes []string
|
|
}
|
|
|
|
type tokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn *int64 `json:"expires_in"`
|
|
}
|
|
|
|
type credentials struct {
|
|
ClientID string
|
|
ClientSecret string
|
|
TokenURL string
|
|
}
|
|
|
|
type clientCredentialsHook struct {
|
|
baseURL string
|
|
client HTTPClient
|
|
sessions map[string]*session
|
|
}
|
|
|
|
var (
|
|
_ sdkInitHook = (*clientCredentialsHook)(nil)
|
|
_ beforeRequestHook = (*clientCredentialsHook)(nil)
|
|
_ afterErrorHook = (*clientCredentialsHook)(nil)
|
|
)
|
|
|
|
func NewClientCredentialsHook() *clientCredentialsHook {
|
|
return &clientCredentialsHook{
|
|
sessions: make(map[string]*session),
|
|
}
|
|
}
|
|
|
|
func (c *clientCredentialsHook) SDKInit(baseURL string, client HTTPClient) (string, HTTPClient) {
|
|
c.baseURL = baseURL
|
|
c.client = client
|
|
return baseURL, client
|
|
}
|
|
|
|
func (c *clientCredentialsHook) BeforeRequest(ctx BeforeRequestContext, req *http.Request) (*http.Request, error) {
|
|
if ctx.OAuth2Scopes == nil {
|
|
// OAuth2 not in use
|
|
return req, nil
|
|
}
|
|
|
|
credentials, err := c.getCredentials(ctx.Context, ctx.SecuritySource)
|
|
if err != nil {
|
|
return nil, &FailEarly{Cause: err}
|
|
}
|
|
if credentials == nil {
|
|
return req, err
|
|
}
|
|
|
|
sessionKey := getSessionKey(credentials.ClientID, credentials.ClientSecret)
|
|
sess, ok := c.sessions[sessionKey]
|
|
if !ok || !hasRequiredScopes(sess.Scopes, ctx.OAuth2Scopes) || hasTokenExpired(sess.ExpiresAt) {
|
|
s, err := c.doTokenRequest(ctx.Context, credentials, getScopes(ctx.OAuth2Scopes, sess))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get token: %w", err)
|
|
}
|
|
|
|
c.sessions[sessionKey] = s
|
|
sess = s
|
|
}
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sess.Token))
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c *clientCredentialsHook) AfterError(ctx AfterErrorContext, res *http.Response, err error) (*http.Response, error) {
|
|
if ctx.OAuth2Scopes == nil {
|
|
// OAuth2 not in use
|
|
return res, err
|
|
}
|
|
|
|
// We don't want to refresh the token if the error is not related to the token
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
|
|
credentials, err := c.getCredentials(ctx.Context, ctx.SecuritySource)
|
|
if err != nil {
|
|
return nil, &FailEarly{Cause: err}
|
|
}
|
|
if credentials == nil {
|
|
return res, err
|
|
}
|
|
|
|
if res != nil && res.StatusCode == http.StatusUnauthorized {
|
|
sessionKey := getSessionKey(credentials.ClientID, credentials.ClientSecret)
|
|
delete(c.sessions, sessionKey)
|
|
}
|
|
|
|
return res, err
|
|
}
|
|
|
|
func (c *clientCredentialsHook) doTokenRequest(ctx context.Context, credentials *credentials, scopes []string) (*session, error) {
|
|
values := url.Values{}
|
|
values.Set("grant_type", "client_credentials")
|
|
values.Set("client_id", credentials.ClientID)
|
|
values.Set("client_secret", credentials.ClientSecret)
|
|
|
|
if len(scopes) > 0 {
|
|
values.Set("scope", strings.Join(scopes, " "))
|
|
}
|
|
|
|
tokenURL := credentials.TokenURL
|
|
u, err := url.Parse(tokenURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse token URL: %w", err)
|
|
}
|
|
if !u.IsAbs() {
|
|
tokenURL, err = url.JoinPath(c.baseURL, tokenURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse token URL: %w", err)
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, bytes.NewBufferString(values.Encode()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create token request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
res, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send token request: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(res.Body)
|
|
return nil, fmt.Errorf("unexpected status code: %d: %s", res.StatusCode, body)
|
|
}
|
|
|
|
var tokenRes tokenResponse
|
|
if err := json.NewDecoder(res.Body).Decode(&tokenRes); err != nil {
|
|
return nil, fmt.Errorf("failed to decode token response: %w", err)
|
|
}
|
|
|
|
if tokenRes.TokenType != "Bearer" {
|
|
return nil, fmt.Errorf("unexpected token type: %s", tokenRes.TokenType)
|
|
}
|
|
|
|
var expiresAt *int64
|
|
if tokenRes.ExpiresIn != nil {
|
|
expiresAt = new(int64)
|
|
*expiresAt = time.Now().Unix() + *tokenRes.ExpiresIn
|
|
}
|
|
|
|
return &session{
|
|
Credentials: credentials,
|
|
Token: tokenRes.AccessToken,
|
|
ExpiresAt: expiresAt,
|
|
Scopes: scopes,
|
|
}, nil
|
|
}
|
|
|
|
func (c *clientCredentialsHook) getCredentials(ctx context.Context, source func(ctx context.Context) (interface{}, error)) (*credentials, error) {
|
|
if source == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
sec, err := source(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
security, ok := sec.(components.Security)
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected security type: %T", sec)
|
|
}
|
|
|
|
return &credentials{
|
|
ClientID: security.ClientID,
|
|
ClientSecret: security.ClientSecret,
|
|
TokenURL: security.GetTokenURL(),
|
|
}, nil
|
|
}
|
|
|
|
func getSessionKey(clientID, clientSecret string) string {
|
|
key := fmt.Sprintf("%s:%s", clientID, clientSecret)
|
|
hash := md5.Sum([]byte(key))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
func hasRequiredScopes(scopes []string, requiredScopes []string) bool {
|
|
for _, requiredScope := range requiredScopes {
|
|
found := false
|
|
for _, scope := range scopes {
|
|
if scope == requiredScope {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func getScopes(requiredScopes []string, sess *session) []string {
|
|
scopes := requiredScopes
|
|
if sess != nil {
|
|
for _, scope := range sess.Scopes {
|
|
found := false
|
|
for _, requiredScope := range requiredScopes {
|
|
if scope == requiredScope {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
scopes = append(scopes, scope)
|
|
}
|
|
}
|
|
}
|
|
|
|
return scopes
|
|
}
|
|
|
|
func hasTokenExpired(expiresAt *int64) bool {
|
|
return expiresAt == nil || time.Now().Unix()+60 >= *expiresAt
|
|
}
|