package client import ( "context" "errors" "fmt" "net/http" "net/http/cookiejar" "net/url" "time" "github.com/shurcooL/graphql" "golang.org/x/net/publicsuffix" "edge-infra.dev/pkg/edge/api/graph/model" ) const ( // defaultEndpoint the default Edge API endpoint to use for requests. defaultEndpoint = "https://dev1.edge-preprod.dev/api" // defaultVersion the default API version to use for requests. defaultVersion = "v2" // defaultUseragent the default user agent for requests. defaultUseragent = "edge-go-client" + "/" + defaultVersion // defaultTimeout the default timeout for the http client. defaultTimeout = 20 * time.Second // userAgentHeader the user-agent header key. userAgentHeader = "user-agent" // edgeVersionHeader the edge-version header key. edgeVersionHeader = "edge-version" // authorizationHeader the authorization header key. authorizationHeader = "authorization" // bearerToken the bearer token prefix for authorization header. bearerToken = "bearer" // totpToken the totp token prefix for authorization header. totpToken = "totp" ) type EdgeClient struct { // Client is the graphql client for queries and mutations. Client *graphql.Client // HTTPClient is the http client used for the requests. HTTPClient *http.Client // jar is a cookiejar that holds http cookies and passes them with every request. jar *cookiejar.Jar // timeout is the request timeout duration. timeout time.Duration // userAgent is the user agent for the http request. userAgent string // version is the corresponding edge version. version string // sessionExpires is the date and time the authentication session expires // it is used to keep track of when to refresh the session. sessionExpires time.Time // BaseURL is the url to make the request to. BaseURL *url.URL // loginCredentials contains authetication credentials (username, password and organization). loginCredentials *LoginRequest // totpToken is the totp authentication token for the http request. totpToken string // bearerToken is the authentication bearer token for the http request. bearerToken string // headers is a list of http headers to append to the request. headers map[string]string } // New returns a new client to interact with the Edge API. func New(opts ...Option) (*EdgeClient, error) { cl := &EdgeClient{ headers: make(map[string]string), } for _, o := range opts { o(cl) } return cl.init() } // init initializes the Edge Client. func (c *EdgeClient) init() (*EdgeClient, error) { if c.jar == nil { jar, err := cookiejar.New(&cookiejar.Options{ PublicSuffixList: publicsuffix.List, }) if err != nil { return c, err } c.jar = jar } if c.timeout == 0 { c.timeout = defaultTimeout } if c.userAgent == "" { c.headers[userAgentHeader] = defaultUseragent } if c.version == "" { c.headers[edgeVersionHeader] = c.version } if c.HTTPClient == nil { c.HTTPClient = &http.Client{ Jar: c.jar, Timeout: c.timeout, Transport: &Transport{ T: http.DefaultTransport, Headers: c.headers, }, } } if c.BaseURL == nil { baseURL, err := url.Parse(fmt.Sprintf("%s/%s", defaultEndpoint, defaultVersion)) if err != nil { return c, err } c.BaseURL = baseURL } if c.Client == nil { c.Client = graphql.NewClient(c.BaseURL.String(), c.HTTPClient) } if c.multipleAuthMethods() { return c, errors.New("using multiple authentication methods is not supported") } if c.hasCredentials() { _, err := c.Login(context.Background(), c.loginCredentials) if err != nil { return c, err } c.sessionExpires = time.Now() } return c, nil } // Query calls bff for graphql queries and populate the mutation interface with the response. func (c EdgeClient) Query(ctx context.Context, query interface{}, variables map[string]interface{}) error { if err := c.handleRefresh(ctx); err != nil { return err } return c.Client.Query(ctx, query, variables) } // Mutate calls bff for graphql mutations and populate the mutation interface with the response. func (c EdgeClient) Mutate(ctx context.Context, mutation interface{}, variables map[string]interface{}) error { if err := c.handleRefresh(ctx); err != nil { return err } return c.Client.Mutate(ctx, mutation, variables) } // handleRefresh refreshes the Edge API session. func (c EdgeClient) handleRefresh(ctx context.Context) error { if c.hasCredentials() && !c.sessionExpires.IsZero() { //nolint diff := time.Since(c.sessionExpires) expires := diff.Minutes() if expires >= 13 && expires < 15 { _, err := c.SessionRefresh(ctx, model.AuthProviderBsl) if err != nil { // if an error occurs while refreshing session, then try to login instead _, err := c.Login(ctx, c.loginCredentials) if err != nil { return err } } c.sessionExpires = time.Now() } } return nil } // hasCredentials checks the authentication method being used is username and password. func (c EdgeClient) hasCredentials() bool { return c.loginCredentials != nil && c.loginCredentials.Username != "" && c.loginCredentials.Password != "" && c.loginCredentials.Organization != "" } // multipleAuthMethods checks if multiple authentication methods are being used. func (c EdgeClient) multipleAuthMethods() bool { switch { case c.bearerToken != "" && c.totpToken != "" && c.hasCredentials(): fallthrough case c.bearerToken != "" && c.hasCredentials(): fallthrough case c.totpToken != "" && c.hasCredentials(): fallthrough case c.bearerToken != "" && c.totpToken != "": return true default: return false } }