diff --git a/sdk/http_client/http_token_management.go b/sdk/http_client/http_client_auth_token_management.go similarity index 62% rename from sdk/http_client/http_token_management.go rename to sdk/http_client/http_client_auth_token_management.go index 6383ea40..f6cda96f 100644 --- a/sdk/http_client/http_token_management.go +++ b/sdk/http_client/http_client_auth_token_management.go @@ -1,7 +1,8 @@ -// http_token_management.go +// http_client_auth_token_management.go package http_client import ( + "fmt" "time" ) @@ -11,51 +12,42 @@ type TokenResponse struct { Expires time.Time `json:"expires"` } -// ValidAuthToken checks if the current token is valid and not close to expiry. +// ValidAuthTokenCheck checks if the current token is valid and not close to expiry. // If the token is invalid, it tries to refresh it. -func (c *Client) ValidAuthTokenCheck() bool { - +// It returns a boolean indicating the validity of the token and an error if there's a failure. +func (c *Client) ValidAuthTokenCheck() (bool, error) { // If token doesn't exist if c.Token == "" { if c.BearerTokenAuthCredentials.Username != "" && c.BearerTokenAuthCredentials.Password != "" { err := c.ObtainToken() if err != nil { - return false + return false, fmt.Errorf("failed to obtain bearer token: %w", err) } } else if c.OAuthCredentials.ClientID != "" && c.OAuthCredentials.ClientSecret != "" { err := c.ObtainOAuthToken(c.OAuthCredentials) if err != nil { - return false + return false, fmt.Errorf("failed to obtain OAuth token: %w", err) } } else { - c.logger.Error("No valid credentials provided. Unable to obtain a token.") - return false + return false, fmt.Errorf("no valid credentials provided. Unable to obtain a token") } } // If token exists and is close to expiry or already expired if time.Until(c.Expiry) < c.config.TokenRefreshBufferPeriod { - if c.config.DebugMode { - c.logger.Debug("Token is not valid or is close to expiry", "Expiry", c.Expiry) - } - var err error if c.BearerTokenAuthCredentials.Username != "" && c.BearerTokenAuthCredentials.Password != "" { err = c.RefreshToken() } else if c.OAuthCredentials.ClientID != "" && c.OAuthCredentials.ClientSecret != "" { err = c.RefreshOAuthToken() } else { - c.logger.Error("Unknown auth method", "AuthMethod", c.authMethod) - return false + return false, fmt.Errorf("unknown auth method: %s", c.authMethod) } if err != nil { - return false + return false, fmt.Errorf("failed to refresh token: %w", err) } } - if c.config.DebugMode { - c.logger.Debug("Token is valid", "Expiry", c.Expiry) - } - return true + return true, nil } diff --git a/sdk/http_client/http_request.go b/sdk/http_client/http_request.go index aaf4bdbb..536a6647 100644 --- a/sdk/http_client/http_request.go +++ b/sdk/http_client/http_request.go @@ -11,11 +11,9 @@ import ( func (c *Client) DoRequest(method, endpoint string, body, out interface{}) (*http.Response, error) { // Auth Token validation check - if !c.ValidAuthTokenCheck() { - if c.config.DebugMode { - c.logger.Debug("Failed to validate or refresh token.") - } - return nil, fmt.Errorf("failed to validate or refresh token. Stopping") + valid, err := c.ValidAuthTokenCheck() + if err != nil || !valid { + return nil, fmt.Errorf("validity of the authentication token failed with error: %w", err) } // Acquire a token for concurrency management with a timeout and measure its acquisition time diff --git a/sdk/http_client/http_request.go.back b/sdk/http_client/http_request.go.back deleted file mode 100644 index b2fbc661..00000000 --- a/sdk/http_client/http_request.go.back +++ /dev/null @@ -1,188 +0,0 @@ -// http_request.go -package http_client - -import ( - "bytes" - "context" - "fmt" - "net/http" - "time" -) - -func (c *Client) DoRequest(method, endpoint string, body, out interface{}) (*http.Response, error) { - // Auth Token validation check - if !c.ValidAuthTokenCheck() { - if c.config.DebugMode { - c.logger.Debug("Failed to validate or refresh token.") - } - return nil, fmt.Errorf("failed to validate or refresh token. Stopping") - } - - // Acquire a token for concurrency management with a timeout and measure its acquisition time - tokenAcquisitionStart := time.Now() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - requestID, err := c.ConcurrencyMgr.Acquire(ctx) - if err != nil { - return nil, err - } - defer c.ConcurrencyMgr.Release(requestID) - - tokenAcquisitionDuration := time.Since(tokenAcquisitionStart) - c.PerfMetrics.lock.Lock() - c.PerfMetrics.TokenWaitTime += tokenAcquisitionDuration - c.PerfMetrics.lock.Unlock() - - // Add the request ID to the context - ctx = context.WithValue(ctx, requestIDKey{}, requestID) - - // determine which set of encoding and content-type request rules to use - handler := GetAPIHandler(endpoint, c.config.DebugMode) - // Construct request - requestData, err := handler.MarshalRequest(body, method) - if err != nil { - return nil, err - } - - // Construct URL using the ConstructAPIResourceEndpoint function - url := c.ConstructAPIResourceEndpoint(endpoint) - - // Initialize total request counter - c.PerfMetrics.lock.Lock() - c.PerfMetrics.TotalRequests++ - c.PerfMetrics.lock.Unlock() - - // Perform Request - req, err := http.NewRequest(method, url, bytes.NewBuffer(requestData)) - if err != nil { - return nil, err - } - // Define header content type based on url and http method - contentType := handler.GetContentType(method) - // Set Request Headers - req.Header.Add("Authorization", "Bearer "+c.Token) - req.Header.Add("Content-Type", contentType) - req.Header.Add("Accept", contentType) - req.Header.Set("User-Agent", GetUserAgent()) - - // Define if request is retryable - retryableHTTPMethods := map[string]bool{ - http.MethodGet: true, // GET - http.MethodDelete: true, // DELETE - http.MethodPut: true, // PUT - http.MethodPatch: true, // PATCH - } - - if retryableHTTPMethods[method] { - // Define a deadline for total retries based on http client TotalRetryDuration config - totalRetryDeadline := time.Now().Add(c.config.TotalRetryDuration) - - i := 0 - for { - // Check if we've reached the maximum number of retries or if our total retry time has exceeded - if i > c.config.MaxRetryAttempts || time.Now().After(totalRetryDeadline) { - return nil, fmt.Errorf("max retry attempts reached or total retry duration exceeded") - } - - // This context is used to propagate cancellations and timeouts for the request. - // For example, if a request's context gets canceled or times out, the request will be terminated early. - req = req.WithContext(ctx) - - // Start response time measurement - responseTimeStart := time.Now() - - // Execute Request with context - resp, err := c.httpClient.Do(req) - if err != nil { - c.logger.Error("Failed to send request", "method", method, "endpoint", endpoint, "error", err) - return nil, err - } - - // After each request, compute and update response time - responseDuration := time.Since(responseTimeStart) - c.PerfMetrics.lock.Lock() - c.PerfMetrics.TotalResponseTime += responseDuration - c.PerfMetrics.lock.Unlock() - - // determine which set of decoding and content-type rules to use - handler := GetAPIHandler(resp.Request.URL.Path, c.config.DebugMode) - - // Checks for the presence of a deprecation header in the HTTP response and logs if found. - if i == 0 { - CheckDeprecationHeader(resp, c.logger) - } - - // Handle (unmarshall) response with API Handler - if err := handler.UnmarshalResponse(resp, out); err != nil { - c.logger.Error("Failed to unmarshal HTTP response", "method", method, "endpoint", endpoint, "error", err) - i++ // Increase the retry count - continue - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - c.logger.Info("HTTP request succeeded", "method", method, "endpoint", endpoint, "status_code", resp.StatusCode) - return resp, nil - } - - // Retry Logic - if isNonRetryableError(resp) { - c.logger.Warn("Encountered a non-retryable error", "status", resp.StatusCode, "description", translateStatusCode(resp.StatusCode)) - return resp, c.handleAPIError(resp) - } else if isRateLimitError(resp) { - waitDuration := parseRateLimitHeaders(resp) // Checks for the Retry-After, X-RateLimit-Remaining and X-RateLimit-Reset headers - c.logger.Warn("Encountered a rate limit error. Retrying after wait duration.", "wait_duration", waitDuration) - time.Sleep(waitDuration) - i++ // Increase the retry count - continue // This will restart the loop, effectively "retrying" the request - } else if isTransientError(resp) { - waitDuration := calculateBackoff(i) //uses exponential backoff (with jitter) - c.logger.Warn("Encountered a transient error. Retrying after backoff.", "wait_duration", waitDuration) - time.Sleep(waitDuration) - i++ // Increase the retry count - continue // This will restart the loop, effectively "retrying" the request - } else { - c.logger.Error("Received unexpected error status from HTTP request", "method", method, "endpoint", endpoint, "status_code", resp.StatusCode, "description", translateStatusCode(resp.StatusCode)) - return resp, c.handleAPIError(resp) - } - } - } else { - // Start response time measurement - responseTimeStart := time.Now() - // For non-retryable HTTP Methods (POST - Create) - req = req.WithContext(ctx) - resp, err := c.httpClient.Do(req) - - if err != nil { - c.logger.Error("Failed to send request", "method", method, "endpoint", endpoint, "error", err) - return nil, err - } - - // After the request, compute and update response time - responseDuration := time.Since(responseTimeStart) - c.PerfMetrics.lock.Lock() - c.PerfMetrics.TotalResponseTime += responseDuration - c.PerfMetrics.lock.Unlock() - - // Determine the appropriate API handler based on the given URL endpoint - handler := GetAPIHandler(resp.Request.URL.Path, c.config.DebugMode) - - // Checks for the presence of a deprecation header in the HTTP response and logs if found - CheckDeprecationHeader(resp, c.logger) - - // Unmarshal the response with the determined API Handler - err = handler.UnmarshalResponse(resp, out) - if err != nil { - c.logger.Error("Failed to unmarshal HTTP response", "method", method, "endpoint", endpoint, "error", err) - return resp, err - } - // Check if the response status code is within the success range - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return resp, nil - } else { - statusDescription := translateStatusCode(resp.StatusCode) - c.logger.Error("Received non-success status code from HTTP request", "method", method, "endpoint", endpoint, "status_code", resp.StatusCode, "description", statusDescription) - return resp, fmt.Errorf("Error status code: %d - %s", resp.StatusCode, statusDescription) - } - } -} diff --git a/sdk/http_client/jamfpro_api_handler.go.back b/sdk/http_client/jamfpro_api_handler.go.back deleted file mode 100644 index 381e92f2..00000000 --- a/sdk/http_client/jamfpro_api_handler.go.back +++ /dev/null @@ -1,255 +0,0 @@ -// jamfpro_api_handler.go -/* ------------------------------Summary---------------------------------------- -This is a api handler module for the http_client to accommodate specifics of -jamf's api(s). It handles the encoding (marshalling) and decoding (unmarshalling) -of data. It also sets the correct content headers for the various http methods. - -This module integrates with the http_client logger for wrapped error handling -for human readable return codes. It also supports the http_clients debugMode for -verbose logging. - -The logic of this module is defined as follows: -Classic API: - -For requests (GET, POST, PUT, DELETE): -- Encoding (Marshalling): Use XML format. -For responses (GET, POST, PUT): -- Decoding (Unmarshalling): Use XML format. -For responses (DELETE): -- Handle response codes as response body lacks anything useful. -Headers -- Set content header as application/xml - -JamfPro API: - -For requests (GET, POST, PUT, DELETE): -- Encoding (Marshalling): Use JSON format. -For responses (GET, POST, PUT): -- Decoding (Unmarshalling): Use JSON format. -For responses (DELETE): -- Handle response codes as response body lacks anything useful. -Headers -- Set content header as application/json -*/ -package http_client - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "io" - "net/http" - "strings" -) - -// Endpoint constants represent the URL suffixes used for Jamf API token interactions. -const ( - BaseDomain = ".jamfcloud.com" // BaseDomain represents the base domain for the jamf instance. - OAuthTokenEndpoint = "/api/oauth/token" // OAuthTokenEndpoint: The endpoint to obtain an OAuth token. - BearerTokenEndpoint = "/api/v1/auth/token" // BearerTokenEndpoint: The endpoint to obtain a bearer token. - TokenRefreshEndpoint = "/api/v1/auth/keep-alive" // TokenRefreshEndpoint: The endpoint to refresh an existing token. - TokenInvalidateEndpoint = "/api/v1/auth/invalidate-token" // TokenInvalidateEndpoint: The endpoint to invalidate an active token. -) - -// ClassicApiHandler handles the specifics of the Classic API. -type ClassicApiHandler struct { - logger Logger - debugMode bool -} - -// JamfProApiHandler handles the specifics of the JamfPro API. -type JamfProApiHandler struct { - logger Logger - debugMode bool -} - -// UnknownApiHandler provides default behavior for unrecognized API types. -type UnknownApiHandler struct { - logger Logger - debugMode bool -} - -// SetLogger assigns a logger instance to the ClassicApiHandler. -func (h *ClassicApiHandler) SetLogger(logger Logger) { - h.logger = logger -} - -// SetLogger assigns a logger instance to the JamfProApiHandler. -func (h *JamfProApiHandler) SetLogger(logger Logger) { - h.logger = logger -} - -// SetLogger assigns a logger instance to the UnknownApiHandler. -func (h *UnknownApiHandler) SetLogger(logger Logger) { - h.logger = logger -} - -func (h *ClassicApiHandler) SetDebugMode(debug bool) { - h.debugMode = debug -} - -func (h *JamfProApiHandler) SetDebugMode(debug bool) { - h.debugMode = debug -} - -func (h *UnknownApiHandler) SetDebugMode(debug bool) { - h.debugMode = debug -} - -// ConstructAPIResourceEndpoint returns the full URL for a Jamf API resource endpoint path. -func (c *Client) ConstructAPIResourceEndpoint(endpointPath string) string { - return fmt.Sprintf("https://%s%s%s", c.InstanceName, BaseDomain, endpointPath) -} - -// ConstructAPIAuthEndpoint returns the full URL for a Jamf API auth endpoint path. -func (c *Client) ConstructAPIAuthEndpoint(endpointPath string) string { - return fmt.Sprintf("https://%s%s%s", c.InstanceName, BaseDomain, endpointPath) -} - -// APIHandler is an interface for encoding, decoding, and determining content types for different API implementations. -// It encapsulates behavior for encoding and decoding requests and responses. -type APIHandler interface { - MarshalRequest(body interface{}, method string) ([]byte, error) - UnmarshalResponse(resp *http.Response, out interface{}) error - GetContentType(method string) string - SetLogger(logger Logger) - SetDebugMode(debug bool) -} - -// GetContentType for ClassicApiHandler always returns XML as the content type. -func (h *ClassicApiHandler) GetContentType(method string) string { - return "application/xml" -} - -// GetContentType for JamfProApiHandler always returns JSON as the content type. -func (h *JamfProApiHandler) GetContentType(method string) string { - return "application/json" -} - -func (h *UnknownApiHandler) GetContentType(method string) string { - // For an unknown API handler, defaults to JSON handling behavior. - return "application/json" -} - -// GetAPIHandler determines the appropriate APIHandler based on the endpoint. -// It identifies the type of API (Classic, JamfPro, or Unknown) and returns the corresponding handler. -func GetAPIHandler(endpoint string, debugMode bool) APIHandler { - var handler APIHandler - if strings.Contains(endpoint, "/JSSResource") { - handler = &ClassicApiHandler{} - } else if strings.Contains(endpoint, "/api") { - handler = &JamfProApiHandler{} - } else { - handler = &UnknownApiHandler{} - } - handler.SetLogger(NewDefaultLogger()) - handler.SetDebugMode(debugMode) // Set the debug mode for the handler - return handler -} - -// MarshalRequest encodes the request body in XML format for the Classic API. -func (h *ClassicApiHandler) MarshalRequest(body interface{}, method string) ([]byte, error) { - data, err := xml.Marshal(body) - if err != nil { - return nil, err - } - - // If in debug mode and the method is either POST (Create) or PUT (Update), log the full request body - if h.debugMode && (method == "POST" || method == "PUT") { - h.logger.Debug("Full Request Body:", string(data)) - } - - return data, nil -} - -// UnmarshalResponse decodes the response body from XML format for the Classic API. -func (h *ClassicApiHandler) UnmarshalResponse(resp *http.Response, out interface{}) error { - if resp.Request.Method == "DELETE" { - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return nil - } else { - return fmt.Errorf("DELETE request failed with status code: %d", resp.StatusCode) - } - } - - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - h.logger.Error("Failed reading response body", "error", err) - return err - } - - // Log raw response if in debug mode - if h.debugMode { - h.logger.Debug("Raw HTTP Response:", string(bodyBytes)) - } - - if h.debugMode { - h.logger.Debug("Unmarshaling response for Classic API using XML", "method", resp.Request.Method) - } - return xml.Unmarshal(bodyBytes, out) -} - -// MarshalRequest encodes the request body in JSON format for the JamfPro API. -func (h *JamfProApiHandler) MarshalRequest(body interface{}, method string) ([]byte, error) { - data, err := json.Marshal(body) - if err != nil { - h.logger.Error("Failed marshaling request for JamfPro API", "error", err) - return nil, err - } - - // If in debug mode and the method is either POST (Create) or PUT (Update), log the full request body - if h.debugMode { - h.logger.Debug("Marshaling request for JamfPro API", "method", method) - if method == "POST" || method == "PUT" { - h.logger.Debug("Full Request Body for JamfPro API:", string(data)) - } - } - - return data, nil -} - -// UnmarshalResponse decodes the response body from JSON format for the JamfPro API. -func (h *JamfProApiHandler) UnmarshalResponse(resp *http.Response, out interface{}) error { - if resp.Request.Method == "DELETE" { - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return nil - } else { - return fmt.Errorf("DELETE request failed with status code: %d", resp.StatusCode) - } - } - - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - h.logger.Error("Failed reading response body for JamfPro API", "error", err) - return err - } - - // Log raw response if in debug mode - if h.debugMode { - h.logger.Debug("Raw HTTP Response for JamfPro API:", string(bodyBytes)) - h.logger.Debug("Unmarshaling response for JamfPro API", "status", resp.Status) - } - - err = json.Unmarshal(bodyBytes, out) - if err != nil { - h.logger.Error("Failed unmarshaling response for JamfPro API", "error", err) - return err - } - return nil -} - -// MarshalRequest returns an error since the API type is unsupported. -func (h *UnknownApiHandler) MarshalRequest(body interface{}, method string) ([]byte, error) { - if h.debugMode { - h.logger.Warn("Attempted to marshal request for an unsupported API type") - } - return nil, fmt.Errorf("unsupported API type") -} - -// UnmarshalResponse returns an error since the API type is unsupported. -func (h *UnknownApiHandler) UnmarshalResponse(resp *http.Response, out interface{}) error { - if h.debugMode { - h.logger.Warn("Attempted to unmarshal response for an unsupported API type", "status", resp.Status) - } - return fmt.Errorf("unsupported API type") -}