diff --git a/api/api.go b/api/api.go index 41fdd06..e45f69c 100644 --- a/api/api.go +++ b/api/api.go @@ -1,6 +1,6 @@ /* * - * Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + * Copyright © 2020-2023 Dell Inc. or its subsidiaries. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,9 @@ import ( "errors" "fmt" "io" + "log" "net/http" + "net/http/cookiejar" "net/http/httputil" "net/url" "os" @@ -35,14 +37,16 @@ import ( "regexp" "strconv" "strings" + "sync" "time" ) -var ( - debug = false -) +var debug = false -const paginationHeader = "content-range" +const ( + paginationHeader = "content-range" + dellEmcToken = "DELL-EMC-TOKEN" // #nosec G101 +) // RequestConfig provide options for the request type RequestConfig struct { @@ -121,11 +125,14 @@ type ClientIMPL struct { customHTTPHeaders http.Header logger Logger apiThrottle TimeoutSemaphoreInterface + loginMutex sync.Mutex + token string } // New creates and initialize API client func New(apiURL string, username string, - password string, insecure bool, defaultTimeout, rateLimit uint64, requestIDKey string) (*ClientIMPL, error) { + password string, insecure bool, defaultTimeout, rateLimit uint64, requestIDKey string, +) (*ClientIMPL, error) { debug, _ = strconv.ParseBool(os.Getenv("GOPOWERSTORE_DEBUG")) if apiURL == "" || username == "" || password == "" { return nil, errors.New("API ApiClient can't be initialized: " + @@ -146,9 +153,19 @@ func New(apiURL string, username string, client = &http.Client{} } + // Set cookie jar to enable session management via auth_cookie + jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: nil}) + if err != nil { + log.Printf("Failed to set cookie jar. error: %s", err) + log.Print("Session management is disabled.") + } else { + client.Jar = jar + log.Print("Session management is enabled.") + } + throttle := NewTimeoutSemaphore(int(defaultTimeout), int(rateLimit), &defaultLogger{}) - return &ClientIMPL{ + clientImpl := &ClientIMPL{ apiURL: apiURL, insecure: insecure, username: username, @@ -158,7 +175,12 @@ func New(apiURL string, username string, requestIDKey: requestIDKey, logger: &defaultLogger{}, apiThrottle: throttle, - }, nil + } + + // Create a login session after the client is initialized + clientImpl.login(context.Background()) // #nosec G104 + + return clientImpl, nil } const errorSeverity = "Error" @@ -191,8 +213,10 @@ func buildError(r *http.Response) *ErrorMsg { s := buf.String() errMsg = fmt.Sprintf("%s: %s", errMsg, s) } - return &ErrorMsg{StatusCode: r.StatusCode, Severity: errorSeverity, - Message: errMsg} + return &ErrorMsg{ + StatusCode: r.StatusCode, Severity: errorSeverity, + Message: errMsg, + } } firstErrMsg := (*apiErrorMsg.Messages)[0] firstErrMsg.StatusCode = r.StatusCode @@ -219,8 +243,8 @@ func (c *ClientIMPL) SetLogger(logger Logger) { func (c *ClientIMPL) Query( ctx context.Context, cfg RequestConfigRenderer, - resp interface{}) (RespMeta, error) { - + resp interface{}, +) (RespMeta, error) { config := cfg.RenderRequestConfig() meta := RespMeta{} var cancelFuncPtr *func() @@ -262,16 +286,48 @@ func (c *ClientIMPL) Query( case resp == nil: return meta, nil case r.StatusCode >= 200 && r.StatusCode < 300: + // Save DELL-EMC-TOKEN if it was a successful response. + token := r.Header.Get(dellEmcToken) + if len(token) != 0 { + c.token = token + } + c.updatePaginationInfoInMeta(&meta, r) err = json.NewDecoder(r.Body).Decode(resp) if err == io.EOF { return meta, nil } return meta, err + case r.StatusCode == http.StatusForbidden: + loginResp, err := c.login(ctx) + // Invalid credentials - No need to retry if response of login api was 401 Unauthorized. + if err != nil || loginResp.Status == http.StatusUnauthorized { + return meta, buildError(r) + } + + // login successful - resend the failed request + return c.Query(ctx, cfg, resp) default: return meta, buildError(r) } +} + +func (c *ClientIMPL) login(ctx context.Context) (RespMeta, error) { + c.loginMutex.Lock() + defer c.loginMutex.Unlock() + + type loginDetails []struct { + ID string `json:"id"` + } + var login loginDetails + resp, err := c.Query(ctx, + RequestConfig{ + Method: "GET", + Endpoint: "login_session", + }, &login) + + return resp, err } func addMetaData(req *http.Request, body interface{}) { @@ -303,7 +359,8 @@ func (c *ClientIMPL) QueryParamsWithFields(fp FieldProvider) QueryParamsEncoder } func (c *ClientIMPL) prepareRequestURL(endpoint, id string, action string, - queryParams QueryParamsEncoder) (string, error) { + queryParams QueryParamsEncoder, +) (string, error) { requestURL, err := url.Parse(c.apiURL) if err != nil { return "", err @@ -325,7 +382,8 @@ func (c *ClientIMPL) prepareRequestURL(endpoint, id string, action string, } func (c *ClientIMPL) prepareRequest(ctx context.Context, method, requestURL, traceMsg string, - body interface{}) (*http.Request, error) { + body interface{}, +) (*http.Request, error) { var req *http.Request var err error if body != nil && !(reflect.ValueOf(body).Kind() == reflect.Ptr && reflect.ValueOf(body).IsNil()) { @@ -345,6 +403,9 @@ func (c *ClientIMPL) prepareRequest(ctx context.Context, method, requestURL, tra } req = req.WithContext(ctx) req.SetBasicAuth(c.username, c.password) + if len(c.token) != 0 { + req.Header.Add(dellEmcToken, c.token) + } for key, values := range c.customHTTPHeaders { for _, elem := range values { req.Header.Add(key, elem)