Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added CookieJar Support #108

Merged
merged 3 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion httpclient/httpclient_auth_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (c *Client) ObtainOAuthToken(credentials AuthConfig) error {
expirationTime := time.Now().Add(expiresIn)

// Modified log call using the helper function
redactedAccessToken := RedactSensitiveData(c, "AccessToken", oauthResp.AccessToken)
redactedAccessToken := RedactSensitiveHeaderData(c, "AccessToken", oauthResp.AccessToken)
log.Info("OAuth token obtained successfully", zap.String("AccessToken", redactedAccessToken), zap.Duration("ExpiresIn", expiresIn), zap.Time("ExpirationTime", expirationTime))

c.Token = oauthResp.AccessToken
Expand Down
33 changes: 25 additions & 8 deletions httpclient/httpclient_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package httpclient

import (
"net/http"
"net/http/cookiejar"
"sync"
"time"

Expand Down Expand Up @@ -41,13 +42,6 @@ type ClientConfig struct {
ClientOptions ClientOptions // Optional configuration options for the HTTP Client
}

// EnvironmentConfig represents the structure to read authentication details from a JSON configuration file.
type EnvironmentConfig struct {
InstanceName string `json:"InstanceName,omitempty"`
OverrideBaseDomain string `json:"OverrideBaseDomain,omitempty"`
APIType string `json:"APIType,omitempty"`
}

// AuthConfig represents the structure to read authentication details from a JSON configuration file.
type AuthConfig struct {
Username string `json:"Username,omitempty"`
Expand All @@ -56,8 +50,16 @@ type AuthConfig struct {
ClientSecret string `json:"ClientSecret,omitempty"`
}

// EnvironmentConfig represents the structure to read authentication details from a JSON configuration file.
type EnvironmentConfig struct {
InstanceName string `json:"InstanceName,omitempty"`
OverrideBaseDomain string `json:"OverrideBaseDomain,omitempty"`
APIType string `json:"APIType,omitempty"`
}

// ClientOptions holds optional configuration options for the HTTP Client.
type ClientOptions struct {
EnableCookieJar bool // Field to enable or disable cookie jar
LogLevel string // Field for defining tiered logging level.
LogOutputFormat string // Field for defining the output format of the logs. Use "JSON" for JSON format, "console" for human-readable format
LogConsoleSeparator string // Field for defining the separator in console output format.
Expand Down Expand Up @@ -101,6 +103,20 @@ func BuildClient(config ClientConfig) (*Client, error) {

log.Info("Initializing new HTTP client with the provided configuration")

// Initialize the internal HTTP client
httpClient := &http.Client{
Timeout: config.ClientOptions.CustomTimeout,
}

// Conditionally create and set a cookie jar if the option is enabled
if config.ClientOptions.EnableCookieJar {
jar, err := cookiejar.New(nil) // nil means no options, which uses default options
if err != nil {
return nil, log.Error("Failed to create cookie jar", zap.Error(err))
}
httpClient.Jar = jar
}

// Determine the authentication method using the helper function
authMethod, err := DetermineAuthMethod(config.Auth)
if err != nil {
Expand All @@ -114,7 +130,7 @@ func BuildClient(config ClientConfig) (*Client, error) {
InstanceName: config.Environment.InstanceName,
AuthMethod: authMethod,
OverrideBaseDomain: config.Environment.OverrideBaseDomain,
httpClient: &http.Client{Timeout: config.ClientOptions.CustomTimeout},
httpClient: httpClient,
clientConfig: config,
Logger: log,
ConcurrencyMgr: NewConcurrencyManager(config.ClientOptions.MaxConcurrentRequests, log, true),
Expand All @@ -131,6 +147,7 @@ func BuildClient(config ClientConfig) (*Client, error) {
zap.String("Log Encoding Format", config.ClientOptions.LogOutputFormat),
zap.String("Log Separator", config.ClientOptions.LogConsoleSeparator),
zap.Bool("Hide Sensitive Data In Logs", config.ClientOptions.HideSensitiveData),
zap.Bool("Cookie Jar Enabled", config.ClientOptions.EnableCookieJar),
zap.Int("Max Retry Attempts", config.ClientOptions.MaxRetryAttempts),
zap.Int("Max Concurrent Requests", config.ClientOptions.MaxConcurrentRequests),
zap.Bool("Enable Dynamic Rate Limiting", config.ClientOptions.EnableDynamicRateLimiting),
Expand Down
49 changes: 49 additions & 0 deletions httpclient/httpclient_cookies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package httpclient

import (
"net/http"
"strings"
)

// RedactSensitiveCookies redacts sensitive information from cookies.
// It takes a slice of *http.Cookie and returns a redacted slice of *http.Cookie.
func RedactSensitiveCookies(cookies []*http.Cookie) []*http.Cookie {
// Define sensitive cookie names that should be redacted.
sensitiveCookieNames := map[string]bool{
"SessionID": true, // Example sensitive cookie name
// Add more sensitive cookie names as needed.
}

// Iterate over the cookies and redact sensitive ones.
for _, cookie := range cookies {
if _, found := sensitiveCookieNames[cookie.Name]; found {
cookie.Value = "REDACTED"
}
}

return cookies
}

// Utility function to convert cookies from http.Header to []*http.Cookie.
// This can be useful if cookies are stored in http.Header (e.g., from a response).
func CookiesFromHeader(header http.Header) []*http.Cookie {
cookies := []*http.Cookie{}
for _, cookieHeader := range header["Set-Cookie"] {
if cookie := ParseCookieHeader(cookieHeader); cookie != nil {
cookies = append(cookies, cookie)
}
}
return cookies
}

// ParseCookieHeader parses a single Set-Cookie header and returns an *http.Cookie.
func ParseCookieHeader(header string) *http.Cookie {
headerParts := strings.Split(header, ";")
if len(headerParts) > 0 {
cookieParts := strings.SplitN(headerParts[0], "=", 2)
if len(cookieParts) == 2 {
return &http.Cookie{Name: cookieParts[0], Value: cookieParts[1]}
}
}
return nil
}
4 changes: 2 additions & 2 deletions httpclient/httpclient_headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (h *HeaderManager) SetRequestHeaders(endpoint string) {
}

// LogHeaders prints all the current headers in the http.Request using the zap logger.
// It uses the RedactSensitiveData function to redact sensitive data if required.
// It uses the RedactSensitiveHeaderData function to redact sensitive data if required.
func (h *HeaderManager) LogHeaders(client *Client) {
if h.log.GetLogLevel() <= logger.LogLevelDebug {
// Initialize a new Header to hold the potentially redacted headers
Expand All @@ -119,7 +119,7 @@ func (h *HeaderManager) LogHeaders(client *Client) {
// Redact sensitive values
if len(values) > 0 {
// Use the first value for simplicity; adjust if multiple values per header are expected
redactedValue := RedactSensitiveData(client, name, values[0])
redactedValue := RedactSensitiveHeaderData(client, name, values[0])
redactedHeaders.Set(name, redactedValue)
}
}
Expand Down
4 changes: 2 additions & 2 deletions httpclient/httpclient_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ func ParseISO8601Date(dateStr string) (time.Time, error) {
return time.Parse(time.RFC3339, dateStr)
}

// RedactSensitiveData redacts sensitive data if the HideSensitiveData flag is set to true.
func RedactSensitiveData(client *Client, key string, value string) string {
// RedactSensitiveHeaderData redacts sensitive data if the HideSensitiveData flag is set to true.
func RedactSensitiveHeaderData(client *Client, key string, value string) string {
if client.clientConfig.ClientOptions.HideSensitiveData {
// Define sensitive data keys that should be redacted.
sensitiveKeys := map[string]bool{
Expand Down
2 changes: 1 addition & 1 deletion httpclient/httpclient_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestRedactSensitiveData(t *testing.T) {
},
}

result := RedactSensitiveData(client, tt.key, tt.value)
result := RedactSensitiveHeaderData(client, tt.key, tt.value)
assert.Equal(t, tt.expectedOutcome, result, "Redaction outcome should match expected")
})
}
Expand Down
4 changes: 4 additions & 0 deletions httpclient/httpclient_mocklogger.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@ func (m *MockLogger) LogRateLimiting(event string, method string, url string, re
func (m *MockLogger) LogResponse(event string, method string, url string, statusCode int, responseBody string, responseHeaders map[string][]string, duration time.Duration) {
m.Called(event, method, url, statusCode, responseBody, responseHeaders, duration)
}

func (m *MockLogger) LogCookies(direction string, obj interface{}, method, url string) {
m.Called(direction, obj, method, url)
}
17 changes: 17 additions & 0 deletions httpclient/httpclient_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,16 @@ func (c *Client) executeRequestWithRetries(method, endpoint string, body, out in
var retryCount int
for time.Now().Before(totalRetryDeadline) { // Check if the current time is before the total retry deadline
req = req.WithContext(ctx)

// Log outgoing cookies
log.LogCookies("outgoing", req, method, endpoint)

// Execute the HTTP request
resp, err = c.do(req, log, method, endpoint)

// Log outgoing cookies
log.LogCookies("incoming", req, method, endpoint)

// Check for successful status code
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 400 {
if resp.StatusCode >= 300 {
Expand Down Expand Up @@ -301,12 +310,18 @@ func (c *Client) executeRequest(method, endpoint string, body, out interface{})

req = req.WithContext(ctx)

// Log outgoing cookies
log.LogCookies("outgoing", req, method, endpoint)

// Execute the HTTP request
resp, err := c.do(req, log, method, endpoint)
if err != nil {
return nil, err
}

// Log incoming cookies
log.LogCookies("incoming", req, method, endpoint)

// Checks for the presence of a deprecation header in the HTTP response and logs if found.
CheckDeprecationHeader(resp, log)

Expand Down Expand Up @@ -341,7 +356,9 @@ func (c *Client) executeRequest(method, endpoint string, body, out interface{})
// This function should be used whenever the client needs to send an HTTP request. It abstracts away the common logic of
// request execution and error handling, providing detailed logs for debugging and monitoring.
func (c *Client) do(req *http.Request, log logger.Logger, method, endpoint string) (*http.Response, error) {

resp, err := c.httpClient.Do(req)

if err != nil {
// Log the error with structured logging, including method, endpoint, and the error itself
log.Error("Failed to send request",
Expand Down
35 changes: 35 additions & 0 deletions logger/zaplogger_logfields.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package logger

import (
"net/http"
"time"

"go.uber.org/zap"
Expand Down Expand Up @@ -115,3 +116,37 @@ func (d *defaultLogger) LogResponse(event string, method string, url string, sta
d.logger.Info("HTTP response details", fields...)
}
}

// LogCookies logs the cookies associated with an HTTP request or response.
// `direction` indicates whether the cookies are being sent ("outgoing") or received ("incoming").
// `obj` can be either *http.Request or *http.Response.
func (d *defaultLogger) LogCookies(direction string, obj interface{}, method, url string) {
var cookies []*http.Cookie
var objectType string

// Determine the type and extract cookies
switch v := obj.(type) {
case *http.Request:
cookies = v.Cookies()
objectType = "request"
case *http.Response:
cookies = v.Cookies()
objectType = "response"
default:
// Log a warning if the object is not a request or response
d.logger.Warn("Invalid object type for cookie logging", zap.Any("object", obj))
return
}

// Log the cookies if any are present
if len(cookies) > 0 {
fields := []zap.Field{
zap.String("direction", direction),
zap.String("object_type", objectType),
zap.String("method", method),
zap.String("url", url),
zap.Any("cookies", cookies),
}
d.logger.Debug("Cookies logged", fields...)
}
}
1 change: 1 addition & 0 deletions logger/zaplogger_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Logger interface {
LogRetryAttempt(event string, method string, url string, attempt int, reason string, waitDuration time.Duration, err error)
LogRateLimiting(event string, method string, url string, retryAfter string, waitDuration time.Duration)
LogResponse(event string, method string, url string, statusCode int, responseBody string, responseHeaders map[string][]string, duration time.Duration)
LogCookies(direction string, obj interface{}, method, url string)
}

// GetLogLevel returns the current logging level of the logger. This allows for checking the logger's
Expand Down
Loading