Skip to content

Commit 4485a95

Browse files
authored
Merge pull request #108 from deploymenttheory/dev
Added CookieJar Support
2 parents 5a405c9 + 84e7115 commit 4485a95

10 files changed

+137
-14
lines changed

httpclient/httpclient_auth_oauth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func (c *Client) ObtainOAuthToken(credentials AuthConfig) error {
101101
expirationTime := time.Now().Add(expiresIn)
102102

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

107107
c.Token = oauthResp.AccessToken

httpclient/httpclient_client.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package httpclient
99

1010
import (
1111
"net/http"
12+
"net/http/cookiejar"
1213
"sync"
1314
"time"
1415

@@ -41,13 +42,6 @@ type ClientConfig struct {
4142
ClientOptions ClientOptions // Optional configuration options for the HTTP Client
4243
}
4344

44-
// EnvironmentConfig represents the structure to read authentication details from a JSON configuration file.
45-
type EnvironmentConfig struct {
46-
InstanceName string `json:"InstanceName,omitempty"`
47-
OverrideBaseDomain string `json:"OverrideBaseDomain,omitempty"`
48-
APIType string `json:"APIType,omitempty"`
49-
}
50-
5145
// AuthConfig represents the structure to read authentication details from a JSON configuration file.
5246
type AuthConfig struct {
5347
Username string `json:"Username,omitempty"`
@@ -56,8 +50,16 @@ type AuthConfig struct {
5650
ClientSecret string `json:"ClientSecret,omitempty"`
5751
}
5852

53+
// EnvironmentConfig represents the structure to read authentication details from a JSON configuration file.
54+
type EnvironmentConfig struct {
55+
InstanceName string `json:"InstanceName,omitempty"`
56+
OverrideBaseDomain string `json:"OverrideBaseDomain,omitempty"`
57+
APIType string `json:"APIType,omitempty"`
58+
}
59+
5960
// ClientOptions holds optional configuration options for the HTTP Client.
6061
type ClientOptions struct {
62+
EnableCookieJar bool // Field to enable or disable cookie jar
6163
LogLevel string // Field for defining tiered logging level.
6264
LogOutputFormat string // Field for defining the output format of the logs. Use "JSON" for JSON format, "console" for human-readable format
6365
LogConsoleSeparator string // Field for defining the separator in console output format.
@@ -101,6 +103,20 @@ func BuildClient(config ClientConfig) (*Client, error) {
101103

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

106+
// Initialize the internal HTTP client
107+
httpClient := &http.Client{
108+
Timeout: config.ClientOptions.CustomTimeout,
109+
}
110+
111+
// Conditionally create and set a cookie jar if the option is enabled
112+
if config.ClientOptions.EnableCookieJar {
113+
jar, err := cookiejar.New(nil) // nil means no options, which uses default options
114+
if err != nil {
115+
return nil, log.Error("Failed to create cookie jar", zap.Error(err))
116+
}
117+
httpClient.Jar = jar
118+
}
119+
104120
// Determine the authentication method using the helper function
105121
authMethod, err := DetermineAuthMethod(config.Auth)
106122
if err != nil {
@@ -114,7 +130,7 @@ func BuildClient(config ClientConfig) (*Client, error) {
114130
InstanceName: config.Environment.InstanceName,
115131
AuthMethod: authMethod,
116132
OverrideBaseDomain: config.Environment.OverrideBaseDomain,
117-
httpClient: &http.Client{Timeout: config.ClientOptions.CustomTimeout},
133+
httpClient: httpClient,
118134
clientConfig: config,
119135
Logger: log,
120136
ConcurrencyMgr: NewConcurrencyManager(config.ClientOptions.MaxConcurrentRequests, log, true),
@@ -131,6 +147,7 @@ func BuildClient(config ClientConfig) (*Client, error) {
131147
zap.String("Log Encoding Format", config.ClientOptions.LogOutputFormat),
132148
zap.String("Log Separator", config.ClientOptions.LogConsoleSeparator),
133149
zap.Bool("Hide Sensitive Data In Logs", config.ClientOptions.HideSensitiveData),
150+
zap.Bool("Cookie Jar Enabled", config.ClientOptions.EnableCookieJar),
134151
zap.Int("Max Retry Attempts", config.ClientOptions.MaxRetryAttempts),
135152
zap.Int("Max Concurrent Requests", config.ClientOptions.MaxConcurrentRequests),
136153
zap.Bool("Enable Dynamic Rate Limiting", config.ClientOptions.EnableDynamicRateLimiting),

httpclient/httpclient_cookies.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package httpclient
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
)
7+
8+
// RedactSensitiveCookies redacts sensitive information from cookies.
9+
// It takes a slice of *http.Cookie and returns a redacted slice of *http.Cookie.
10+
func RedactSensitiveCookies(cookies []*http.Cookie) []*http.Cookie {
11+
// Define sensitive cookie names that should be redacted.
12+
sensitiveCookieNames := map[string]bool{
13+
"SessionID": true, // Example sensitive cookie name
14+
// Add more sensitive cookie names as needed.
15+
}
16+
17+
// Iterate over the cookies and redact sensitive ones.
18+
for _, cookie := range cookies {
19+
if _, found := sensitiveCookieNames[cookie.Name]; found {
20+
cookie.Value = "REDACTED"
21+
}
22+
}
23+
24+
return cookies
25+
}
26+
27+
// Utility function to convert cookies from http.Header to []*http.Cookie.
28+
// This can be useful if cookies are stored in http.Header (e.g., from a response).
29+
func CookiesFromHeader(header http.Header) []*http.Cookie {
30+
cookies := []*http.Cookie{}
31+
for _, cookieHeader := range header["Set-Cookie"] {
32+
if cookie := ParseCookieHeader(cookieHeader); cookie != nil {
33+
cookies = append(cookies, cookie)
34+
}
35+
}
36+
return cookies
37+
}
38+
39+
// ParseCookieHeader parses a single Set-Cookie header and returns an *http.Cookie.
40+
func ParseCookieHeader(header string) *http.Cookie {
41+
headerParts := strings.Split(header, ";")
42+
if len(headerParts) > 0 {
43+
cookieParts := strings.SplitN(headerParts[0], "=", 2)
44+
if len(cookieParts) == 2 {
45+
return &http.Cookie{Name: cookieParts[0], Value: cookieParts[1]}
46+
}
47+
}
48+
return nil
49+
}

httpclient/httpclient_headers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (h *HeaderManager) SetRequestHeaders(endpoint string) {
109109
}
110110

111111
// LogHeaders prints all the current headers in the http.Request using the zap logger.
112-
// It uses the RedactSensitiveData function to redact sensitive data if required.
112+
// It uses the RedactSensitiveHeaderData function to redact sensitive data if required.
113113
func (h *HeaderManager) LogHeaders(client *Client) {
114114
if h.log.GetLogLevel() <= logger.LogLevelDebug {
115115
// Initialize a new Header to hold the potentially redacted headers
@@ -119,7 +119,7 @@ func (h *HeaderManager) LogHeaders(client *Client) {
119119
// Redact sensitive values
120120
if len(values) > 0 {
121121
// Use the first value for simplicity; adjust if multiple values per header are expected
122-
redactedValue := RedactSensitiveData(client, name, values[0])
122+
redactedValue := RedactSensitiveHeaderData(client, name, values[0])
123123
redactedHeaders.Set(name, redactedValue)
124124
}
125125
}

httpclient/httpclient_helpers.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ func ParseISO8601Date(dateStr string) (time.Time, error) {
1010
return time.Parse(time.RFC3339, dateStr)
1111
}
1212

13-
// RedactSensitiveData redacts sensitive data if the HideSensitiveData flag is set to true.
14-
func RedactSensitiveData(client *Client, key string, value string) string {
13+
// RedactSensitiveHeaderData redacts sensitive data if the HideSensitiveData flag is set to true.
14+
func RedactSensitiveHeaderData(client *Client, key string, value string) string {
1515
if client.clientConfig.ClientOptions.HideSensitiveData {
1616
// Define sensitive data keys that should be redacted.
1717
sensitiveKeys := map[string]bool{

httpclient/httpclient_helpers_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestRedactSensitiveData(t *testing.T) {
7070
},
7171
}
7272

73-
result := RedactSensitiveData(client, tt.key, tt.value)
73+
result := RedactSensitiveHeaderData(client, tt.key, tt.value)
7474
assert.Equal(t, tt.expectedOutcome, result, "Redaction outcome should match expected")
7575
})
7676
}

httpclient/httpclient_mocklogger.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,7 @@ func (m *MockLogger) LogRateLimiting(event string, method string, url string, re
9696
func (m *MockLogger) LogResponse(event string, method string, url string, statusCode int, responseBody string, responseHeaders map[string][]string, duration time.Duration) {
9797
m.Called(event, method, url, statusCode, responseBody, responseHeaders, duration)
9898
}
99+
100+
func (m *MockLogger) LogCookies(direction string, obj interface{}, method, url string) {
101+
m.Called(direction, obj, method, url)
102+
}

httpclient/httpclient_request.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,16 @@ func (c *Client) executeRequestWithRetries(method, endpoint string, body, out in
160160
var retryCount int
161161
for time.Now().Before(totalRetryDeadline) { // Check if the current time is before the total retry deadline
162162
req = req.WithContext(ctx)
163+
164+
// Log outgoing cookies
165+
log.LogCookies("outgoing", req, method, endpoint)
166+
167+
// Execute the HTTP request
163168
resp, err = c.do(req, log, method, endpoint)
169+
170+
// Log outgoing cookies
171+
log.LogCookies("incoming", req, method, endpoint)
172+
164173
// Check for successful status code
165174
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 400 {
166175
if resp.StatusCode >= 300 {
@@ -301,12 +310,18 @@ func (c *Client) executeRequest(method, endpoint string, body, out interface{})
301310

302311
req = req.WithContext(ctx)
303312

313+
// Log outgoing cookies
314+
log.LogCookies("outgoing", req, method, endpoint)
315+
304316
// Execute the HTTP request
305317
resp, err := c.do(req, log, method, endpoint)
306318
if err != nil {
307319
return nil, err
308320
}
309321

322+
// Log incoming cookies
323+
log.LogCookies("incoming", req, method, endpoint)
324+
310325
// Checks for the presence of a deprecation header in the HTTP response and logs if found.
311326
CheckDeprecationHeader(resp, log)
312327

@@ -341,7 +356,9 @@ func (c *Client) executeRequest(method, endpoint string, body, out interface{})
341356
// This function should be used whenever the client needs to send an HTTP request. It abstracts away the common logic of
342357
// request execution and error handling, providing detailed logs for debugging and monitoring.
343358
func (c *Client) do(req *http.Request, log logger.Logger, method, endpoint string) (*http.Response, error) {
359+
344360
resp, err := c.httpClient.Do(req)
361+
345362
if err != nil {
346363
// Log the error with structured logging, including method, endpoint, and the error itself
347364
log.Error("Failed to send request",

logger/zaplogger_logfields.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logger
22

33
import (
4+
"net/http"
45
"time"
56

67
"go.uber.org/zap"
@@ -115,3 +116,37 @@ func (d *defaultLogger) LogResponse(event string, method string, url string, sta
115116
d.logger.Info("HTTP response details", fields...)
116117
}
117118
}
119+
120+
// LogCookies logs the cookies associated with an HTTP request or response.
121+
// `direction` indicates whether the cookies are being sent ("outgoing") or received ("incoming").
122+
// `obj` can be either *http.Request or *http.Response.
123+
func (d *defaultLogger) LogCookies(direction string, obj interface{}, method, url string) {
124+
var cookies []*http.Cookie
125+
var objectType string
126+
127+
// Determine the type and extract cookies
128+
switch v := obj.(type) {
129+
case *http.Request:
130+
cookies = v.Cookies()
131+
objectType = "request"
132+
case *http.Response:
133+
cookies = v.Cookies()
134+
objectType = "response"
135+
default:
136+
// Log a warning if the object is not a request or response
137+
d.logger.Warn("Invalid object type for cookie logging", zap.Any("object", obj))
138+
return
139+
}
140+
141+
// Log the cookies if any are present
142+
if len(cookies) > 0 {
143+
fields := []zap.Field{
144+
zap.String("direction", direction),
145+
zap.String("object_type", objectType),
146+
zap.String("method", method),
147+
zap.String("url", url),
148+
zap.Any("cookies", cookies),
149+
}
150+
d.logger.Debug("Cookies logged", fields...)
151+
}
152+
}

logger/zaplogger_logger.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Logger interface {
3939
LogRetryAttempt(event string, method string, url string, attempt int, reason string, waitDuration time.Duration, err error)
4040
LogRateLimiting(event string, method string, url string, retryAfter string, waitDuration time.Duration)
4141
LogResponse(event string, method string, url string, statusCode int, responseBody string, responseHeaders map[string][]string, duration time.Duration)
42+
LogCookies(direction string, obj interface{}, method, url string)
4243
}
4344

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

0 commit comments

Comments
 (0)