Skip to content

Commit 8bf378b

Browse files
authored
Allow the agent to edit application response headers (#41)
1 parent 372a7ab commit 8bf378b

File tree

11 files changed

+462
-69
lines changed

11 files changed

+462
-69
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Golang Module Release Notes
22

3+
## 1.14.0 2024-11-18
4+
5+
* Allow the agent to edit application response headers
6+
37
## 1.13.0 2023-07-06
48

59
* Added new module configuration option for more granular inspection

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.13.0
1+
1.14.0

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module github.com/signalsciences/sigsci-module-golang
22

3-
go 1.13
3+
go 1.21
44

55
require (
66
github.com/signalsciences/tlstext v1.2.0
7-
github.com/tinylib/msgp v1.1.0
7+
github.com/tinylib/msgp v1.2.4
88
)
99

10-
require github.com/philhofer/fwd v1.0.0 // indirect
10+
require github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
2-
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
1+
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
2+
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
33
github.com/signalsciences/tlstext v1.2.0 h1:ps1ZCoDz93oMK0ySe7G/2J0dpTT32cN20U+/xy0S7uk=
44
github.com/signalsciences/tlstext v1.2.0/go.mod h1:DKD8bjL8ZiedHAgWtcpgZm6TBAnmAlImuyJX2whrm3k=
5-
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
6-
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
5+
github.com/tinylib/msgp v1.2.4 h1:yLFeUGostXXSGW5vxfT5dXG/qzkn4schv2I7at5+hVU=
6+
github.com/tinylib/msgp v1.2.4/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=

module.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import (
1212
"sync"
1313
"time"
1414

15+
"github.com/signalsciences/sigsci-module-golang/schema"
1516
"github.com/signalsciences/tlstext"
1617
)
1718

19+
type RPCMsgIn = schema.RPCMsgIn
20+
type RPCMsgIn2 = schema.RPCMsgIn2
21+
type RPCMsgOut = schema.RPCMsgOut
22+
1823
// Module is an http.Handler that wraps an existing handler with
1924
// data collection and sends it to the Signal Sciences Agent for
2025
// inspection.
@@ -115,7 +120,7 @@ func (m *Module) ServeHTTP(w http.ResponseWriter, req *http.Request) {
115120
return
116121
}
117122

118-
rw := NewResponseWriter(w)
123+
rw := newResponseWriter(w, out.RespActions)
119124

120125
wafresponse := out.WAFResponse
121126
switch {

responsewriter.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"io"
77
"net"
88
"net/http"
9+
10+
"github.com/signalsciences/sigsci-module-golang/schema"
911
)
1012

1113
// ResponseWriter is a http.ResponseWriter allowing extraction of data needed for inspection
@@ -24,12 +26,17 @@ type ResponseWriterFlusher interface {
2426

2527
// NewResponseWriter returns a ResponseWriter or ResponseWriterFlusher depending on the base http.ResponseWriter.
2628
func NewResponseWriter(base http.ResponseWriter) ResponseWriter {
29+
return newResponseWriter(base, nil)
30+
}
31+
32+
func newResponseWriter(base http.ResponseWriter, actions []schema.Action) ResponseWriter {
2733
// NOTE: according to net/http docs, if WriteHeader is not called explicitly,
2834
// the first call to Write will trigger an implicit WriteHeader(http.StatusOK).
2935
// this is why the default code is 200 and it only changes if WriteHeader is called.
3036
w := &responseRecorder{
31-
base: base,
32-
code: 200,
37+
base: base,
38+
code: 200,
39+
actions: actions,
3340
}
3441
if _, ok := w.base.(http.Flusher); ok {
3542
return &responseRecorderFlusher{w}
@@ -39,9 +46,10 @@ func NewResponseWriter(base http.ResponseWriter) ResponseWriter {
3946

4047
// responseRecorder wraps a base http.ResponseWriter allowing extraction of additional inspection data
4148
type responseRecorder struct {
42-
base http.ResponseWriter
43-
code int
44-
size int64
49+
base http.ResponseWriter
50+
code int
51+
size int64
52+
actions []schema.Action
4553
}
4654

4755
// BaseResponseWriter returns the base http.ResponseWriter allowing access if needed
@@ -66,12 +74,37 @@ func (w *responseRecorder) Header() http.Header {
6674

6775
// WriteHeader writes the header, recording the status code for inspection
6876
func (w *responseRecorder) WriteHeader(status int) {
77+
if w.actions != nil {
78+
w.mergeHeader()
79+
}
6980
w.code = status
7081
w.base.WriteHeader(status)
7182
}
7283

84+
func (w *responseRecorder) mergeHeader() {
85+
hdr := w.base.Header()
86+
for _, a := range w.actions {
87+
switch a.Code {
88+
case schema.AddHdr:
89+
hdr.Add(a.Args[0], a.Args[1])
90+
case schema.SetHdr:
91+
hdr.Set(a.Args[0], a.Args[1])
92+
case schema.SetNEHdr:
93+
if len(hdr.Get(a.Args[0])) == 0 {
94+
hdr.Set(a.Args[0], a.Args[1])
95+
}
96+
case schema.DelHdr:
97+
hdr.Del(a.Args[0])
98+
}
99+
}
100+
w.actions = nil
101+
}
102+
73103
// Write writes data, tracking the length written for inspection
74104
func (w *responseRecorder) Write(b []byte) (int, error) {
105+
if w.actions != nil {
106+
w.mergeHeader()
107+
}
75108
w.size += int64(len(b))
76109
return w.base.Write(b)
77110
}

responsewriter_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"io/ioutil"
77
"net/http"
88
"net/http/httptest"
9+
"reflect"
910
"testing"
11+
12+
"github.com/signalsciences/sigsci-module-golang/schema"
1013
)
1114

1215
// testResponseRecorder is a httptest.ResponseRecorder without the Flusher interface
@@ -123,3 +126,31 @@ func TestResponseWriter(t *testing.T) {
123126
func TestResponseWriterFlusher(t *testing.T) {
124127
testResponseWriter(t, NewResponseWriter(&testResponseRecorderFlusher{httptest.NewRecorder()}), true)
125128
}
129+
130+
func TestResponseHeaders(t *testing.T) {
131+
132+
resp := &httptest.ResponseRecorder{
133+
HeaderMap: http.Header{
134+
"X-Powered-By": []string{"aa"},
135+
"Content-Type": []string{"text/plain"},
136+
"X-Report": []string{"bb"},
137+
},
138+
}
139+
actions := []schema.Action{
140+
{schema.AddHdr, []string{"csp", "src=abc"}},
141+
{schema.SetHdr, []string{"content-type", "text/json"}},
142+
{schema.DelHdr, []string{"x-powered-by"}},
143+
{schema.SetNEHdr, []string{"x-report", "cc"}},
144+
}
145+
newResponseWriter(resp, actions).Write([]byte("foo"))
146+
147+
got := resp.Header()
148+
expected := http.Header{
149+
"Csp": []string{"src=abc"},
150+
"Content-Type": []string{"text/json"},
151+
"X-Report": []string{"bb"},
152+
}
153+
if !reflect.DeepEqual(got, expected) {
154+
t.Fatalf("expected %v, got %v", expected, got)
155+
}
156+
}

rpc.go renamed to schema/rpc.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package sigsci
1+
package schema
22

3-
//go:generate msgp -unexported -tests=false
3+
//go:generate go run github.com/tinylib/msgp@v1.2.4 -unexported -tests=false
44

55
//
66
// This is for messages to and from the agent
@@ -34,8 +34,22 @@ type RPCMsgIn struct {
3434
// RPCMsgOut is sent back to the webserver
3535
type RPCMsgOut struct {
3636
WAFResponse int32
37-
RequestID string `json:",omitempty"` // Set if the server expects an UpdateRequest with this ID (UUID)
38-
RequestHeaders [][2]string `json:",omitempty"` // Any additional information in the form of additional request headers
37+
RequestID string `json:",omitempty"` // Set if the server expects an UpdateRequest with this ID (UUID)
38+
RequestHeaders [][2]string `json:",omitempty"` // Any additional information in the form of additional request headers
39+
RespActions []Action `json:",omitempty" msg:",omitempty"` // Add or Delete application response headers
40+
}
41+
42+
const (
43+
AddHdr int8 = iota + 1
44+
SetHdr
45+
SetNEHdr
46+
DelHdr
47+
)
48+
49+
//msgp:tuple Action
50+
type Action struct {
51+
Code int8
52+
Args []string
3953
}
4054

4155
// RPCMsgIn2 is a follow-up message from the webserver to the Agent

0 commit comments

Comments
 (0)