Skip to content
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
61 changes: 32 additions & 29 deletions internal/record/recording_https_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package record

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
Expand All @@ -33,7 +34,7 @@ import (

type RecordingHTTPSProxy struct {
prevRequestSHA string
seenFiles map[string]struct{}
seenFiles map[string]store.RecordFile
config *config.EndpointConfig
recordingDir string
redactor *redact.Redact
Expand All @@ -42,7 +43,7 @@ type RecordingHTTPSProxy struct {
func NewRecordingHTTPSProxy(cfg *config.EndpointConfig, recordingDir string, redactor *redact.Redact) *RecordingHTTPSProxy {
return &RecordingHTTPSProxy{
prevRequestSHA: store.HeadSHA,
seenFiles: make(map[string]struct{}),
seenFiles: make(map[string]store.RecordFile),
config: cfg,
recordingDir: recordingDir,
redactor: redactor,
Expand Down Expand Up @@ -86,7 +87,7 @@ func (r *RecordingHTTPSProxy) handleRequest(w http.ResponseWriter, req *http.Req
}
if _, ok := r.seenFiles[fileName]; !ok {
// Reset to HeadSHA when first time seen a request from the given file.
recReq.PreviousRequest=store.HeadSHA
recReq.PreviousRequest = store.HeadSHA
}

if req.Header.Get("Upgrade") == "websocket" {
Expand All @@ -108,10 +109,9 @@ func (r *RecordingHTTPSProxy) handleRequest(w http.ResponseWriter, req *http.Req
http.Error(w, fmt.Sprintf("Error recording response: %v", err), http.StatusInternalServerError)
return
}
if (fileName != shaSum) {
if fileName != shaSum {
r.prevRequestSHA = shaSum
}
r.seenFiles[fileName] = struct{}{}
}

func (r *RecordingHTTPSProxy) redactRequest(req *http.Request) (*store.RecordedRequest, error) {
Expand All @@ -123,9 +123,13 @@ func (r *RecordingHTTPSProxy) redactRequest(req *http.Request) (*store.RecordedR
// Redact headers by key
recordedRequest.RedactHeaders(r.config.RedactRequestHeaders)
// Redacts secrets from header values
r.redactor.Headers(recordedRequest.Header)
r.redactor.Headers(recordedRequest.Headers)
recordedRequest.Request = r.redactor.String(recordedRequest.Request)
recordedRequest.Body = r.redactor.Bytes(recordedRequest.Body)
var redactedBodySegments []map[string]any
for _, bodySegment := range recordedRequest.BodySegments {
redactedBodySegments = append(redactedBodySegments, r.redactor.Map(bodySegment))
}
recordedRequest.BodySegments = redactedBodySegments
return recordedRequest, nil
}

Expand Down Expand Up @@ -181,42 +185,41 @@ func (r *RecordingHTTPSProxy) recordResponse(recReq *store.RecordedRequest, resp
if err != nil {
return err
}
recordPath := filepath.Join(r.recordingDir, fileName+".http.log")

// Default to overwriting the file, assuming it's a new file to record.
fileMode := os.O_TRUNC
// If we've seen requests with the same file name before, change the mode to append.
if _, ok := r.seenFiles[fileName]; ok {
fileMode = os.O_APPEND
recordFile, ok := r.seenFiles[fileName]
if !ok {
r.seenFiles[fileName] = store.RecordFile{RecordID: fileName, Interactions: []*store.RecordInteraction{}}
recordFile = r.seenFiles[fileName]
}
file, err := os.OpenFile(recordPath, fileMode|os.O_CREATE|os.O_WRONLY , 0644)

var recordInteraction store.RecordInteraction
recordInteraction.Request = recReq
recordInteraction.SHASum = shaSum
recordInteraction.Response = recordedResponse

recordFile.Interactions = append(recordFile.Interactions, &recordInteraction)
r.seenFiles[fileName] = recordFile

recordPath := filepath.Join(r.recordingDir, fileName+".json")

// Default to overwriting the file.
fileMode := os.O_TRUNC
file, err := os.OpenFile(recordPath, fileMode|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()

fmt.Printf("Writing request to: %s\n", recordPath)
serializedReq := recReq.Serialize()
_, err = file.WriteString(fmt.Sprintf("%s.req %d\n", shaSum, len(serializedReq)))
if err != nil {
return err
}
_, err = file.WriteString(serializedReq)
interaction, err := json.MarshalIndent(recordFile, "", " ")
if err != nil {
return err
}

fmt.Printf("Writing response to: %s\n", recordPath)
recordedResponse.Body = r.redactor.Bytes(recordedResponse.Body)
serializedResp := recordedResponse.Serialize()
_, err = file.WriteString(fmt.Sprintf("\n%s.resp %d\n", shaSum, len(serializedResp)))
if err != nil {
return err
}
_, err = file.WriteString(serializedResp)
_, err = file.WriteString(string(interaction))
if err != nil {
return err
}

return nil
}

Expand Down
30 changes: 25 additions & 5 deletions internal/redact/redact.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package redact

import (
"encoding/json"
"regexp"
"strings"
)
Expand Down Expand Up @@ -52,14 +53,12 @@ func NewRedact(secrets []string) (*Redact, error) {
}

// Headers redacts the secrets in the values of the http.Header.
func (r *Redact) Headers(headers map[string][]string) {
func (r *Redact) Headers(headers map[string]string) {
if r == nil || r.regex == nil {
return // No redactor or no secrets configured
}
for name, values := range headers {
for i, value := range values {
headers[name][i] = r.regex.ReplaceAllString(value, REDACTED)
}
for name, value := range headers {
headers[name] = r.regex.ReplaceAllString(value, REDACTED)
}
}

Expand All @@ -81,3 +80,24 @@ func (r *Redact) Bytes(input []byte) []byte {
}
return r.regex.ReplaceAll(input, []byte(REDACTED))
}

func (r *Redact) Map(input map[string]any) map[string]any {
if r == nil || r.regex == nil {
return input // No redactor or no secrets configured
}
if input == nil {
return nil // Return nil if input is nil
}
jsonBytes, err := json.Marshal(input)
if err != nil {
return nil
}
redactedJsonBytes := r.Bytes(jsonBytes)
var redactedMap map[string]any
err = json.Unmarshal(redactedJsonBytes, &redactedMap)
if err != nil {
return nil
}

return redactedMap
}
122 changes: 97 additions & 25 deletions internal/redact/redact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package redact

import (
"net/http"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -129,52 +128,52 @@ func TestRedact_Bytes(t *testing.T) {
func TestRedact_Headers(t *testing.T) {
testCases := []struct {
name string
headers http.Header
headers map[string]string
secrets []string
expectedHeaders http.Header
expectedHeaders map[string]string
}{
{
name: "Redact secret in single header value",
headers: http.Header{
"Authorization": []string{"Bearer secret_token_123"},
"Content-Type": []string{"application/json"},
headers: map[string]string{
"Authorization": "Bearer secret_token_123",
"Content-Type": "application/json",
},
secrets: []string{"secret_token_123"},
expectedHeaders: http.Header{
"Authorization": []string{"Bearer REDACTED"},
"Content-Type": []string{"application/json"},
expectedHeaders: map[string]string{
"Authorization": "Bearer REDACTED",
"Content-Type": "application/json",
},
},
{
name: "Redact secret in multiple header values",
headers: http.Header{
"Set-Cookie": []string{"sessionid=secret_session_id_789", "other=value"},
"X-Api-Key": []string{"key_value_xyz"},
headers: map[string]string{
"Set-Cookie": "sessionid=secret_session_id_789 other=value",
"X-Api-Key": "key_value_xyz",
},
secrets: []string{"secret_session_id_789", "key_value_xyz"},
expectedHeaders: http.Header{
"Set-Cookie": []string{"sessionid=REDACTED", "other=value"},
"X-Api-Key": []string{"REDACTED"},
expectedHeaders: map[string]string{
"Set-Cookie": "sessionid=REDACTED other=value",
"X-Api-Key": "REDACTED",
},
},
{
name: "No secrets to redact",
headers: http.Header{
"Authorization": []string{"Bearer token"},
headers: map[string]string{
"Authorization": "Bearer token",
},
secrets: []string{},
expectedHeaders: http.Header{
"Authorization": []string{"Bearer token"},
expectedHeaders: map[string]string{
"Authorization": "Bearer token",
},
},
{
name: "Empty secret in list",
headers: http.Header{
"Authorization": []string{"Bearer secret_token_123"},
headers: map[string]string{
"Authorization": "Bearer secret_token_123",
},
secrets: []string{"", "secret_token_123"},
expectedHeaders: http.Header{
"Authorization": []string{"Bearer REDACTED"},
expectedHeaders: map[string]string{
"Authorization": "Bearer REDACTED",
},
},
}
Expand All @@ -184,12 +183,85 @@ func TestRedact_Headers(t *testing.T) {
redactor, err := NewRedact(tc.secrets)
require.NoError(t, err)
// Create a copy of the headers to avoid modifying the original test case data
headersCopy := make(http.Header)
headersCopy := make(map[string]string)
for k, v := range tc.headers {
headersCopy[k] = append([]string{}, v...)
headersCopy[k] = v
}
redactor.Headers(headersCopy)
require.Equal(t, tc.expectedHeaders, headersCopy)
})
}
}

func TestRedact_Map(t *testing.T) {
testCases := []struct {
name string
input map[string]any
secrets []string
expectedOutput map[string]any
}{
{
name: "Redact single secret",
input: map[string]any{
"key": "This is a secret: abc",
},
secrets: []string{"abc"},
expectedOutput: map[string]any{
"key": "This is a secret: REDACTED",
},
},
{
name: "Redact multiple secrets",
input: map[string]any{
"Secret1": "123",
"Secret2": "xyz",
},
secrets: []string{"123", "xyz"},
expectedOutput: map[string]any{
"Secret1": "REDACTED",
"Secret2": "REDACTED",
},
},
{
name: "No secrets to redact",
input: map[string]any{
"key": "No secrets here",
},
secrets: []string{},
expectedOutput: map[string]any{
"key": "No secrets here",
},
},
{
name: "Empty input map",
input: map[string]any{},
secrets: []string{"abc"},
expectedOutput: map[string]any{},
},
{
name: "Empty secret in list",
input: map[string]any{
"key": "This is a secret: abc",
},
secrets: []string{"", "abc"},
expectedOutput: map[string]any{
"key": "This is a secret: REDACTED",
},
},
{
name: "Nil input map",
input: nil,
secrets: []string{"abc"},
expectedOutput: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
redactor, err := NewRedact(tc.secrets)
require.NoError(t, err)
actualOutput := redactor.Map(tc.input)
require.Equal(t, tc.expectedOutput, actualOutput)
})
}
}
Loading
Loading