Skip to content

Commit 547d59e

Browse files
authored
strip transport headers from function response (#1489)
we weren't stripping transport headers from function response before copying to the client response. this means e.g. a function could send a "Connection: close" when fn wants to use a keep alive. this also moved this to common since we're doing it in 3 places. I know that it's not the ideal ideal big O performance and we can pedant about it, but we're talking about 7 things here. I'm open to ideas about what this should look like, I ripped it out of the stdlib and it seems okay. changed the trigger stuff to use this too, as it was copied. added tests to cover all 3 of these spots. the only behavior change is no longer stripping 'authorization' header when passing headers from the client request into the function. authorization seems like a useful header for users that want to use auth in their functions, I think if we need to use this one in the backend we should more carefully strip out the parts we use before passing them on instead of relying on the agent to strip the header to not leak anything into a function. closes #1488
1 parent e272c70 commit 547d59e

File tree

5 files changed

+94
-54
lines changed

5 files changed

+94
-54
lines changed

api/agent/agent.go

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -605,16 +605,6 @@ func (s *hotSlot) exec(ctx context.Context, call *call) error {
605605
return err
606606
}
607607

608-
var removeHeaders = map[string]bool{
609-
"connection": true,
610-
"keep-alive": true,
611-
"trailer": true,
612-
"transfer-encoding": true,
613-
"te": true,
614-
"upgrade": true,
615-
"authorization": true,
616-
}
617-
618608
func createUDSRequest(ctx context.Context, call *call) *http.Request {
619609
req, err := http.NewRequest("POST", "http://localhost/call", call.req.Body)
620610
if err != nil {
@@ -625,12 +615,13 @@ func createUDSRequest(ctx context.Context, call *call) *http.Request {
625615
// it properly and close connections at the end, e.g. when using UDS.
626616
req = req.WithContext(ctx)
627617

618+
// remove transport headers before passing to function
619+
common.StripHopHeaders(call.req.Header)
620+
628621
req.Header = make(http.Header)
629622
for k, vs := range call.req.Header {
630-
if !removeHeaders[strings.ToLower(k)] {
631-
for _, v := range vs {
632-
req.Header.Add(k, v)
633-
}
623+
for _, v := range vs {
624+
req.Header.Add(k, v)
634625
}
635626
}
636627

@@ -723,6 +714,9 @@ func (s *hotSlot) writeResp(ctx context.Context, max uint64, resp *http.Response
723714

724715
rw = newSizerRespWriter(max, rw)
725716

717+
// remove transport headers before copying to client response
718+
common.StripHopHeaders(resp.Header)
719+
726720
// WARNING: is the following header copy safe?
727721
// if we're writing directly to the response writer, we need to set headers
728722
// and only copy the body. resp.Write would copy a full

api/common/io_utils.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,51 @@ package common
22

33
import (
44
"io"
5+
"net/http"
56
"sync"
67
)
78

9+
// StripHopHeaders removes transport related headers.
10+
func StripHopHeaders(hdr http.Header) {
11+
// Remove hop-by-hop headers to the backend. Especially
12+
// important is "Connection" because we want a persistent
13+
// connection, regardless of what the client sent to us.
14+
for _, h := range hopHeaders {
15+
hv := hdr.Get(h)
16+
if hv == "" {
17+
continue
18+
}
19+
if h == "Te" && hv == "trailers" {
20+
// Issue 21096: tell backend applications that
21+
// care about trailer support that we support
22+
// trailers. (We do, but we don't go out of
23+
// our way to advertise that unless the
24+
// incoming client request thought it was
25+
// worth mentioning)
26+
continue
27+
}
28+
hdr.Del(h)
29+
}
30+
}
31+
32+
// Hop-by-hop headers. These are removed when sent to the backend.
33+
// As of RFC 7230, hop-by-hop headers are required to appear in the
34+
// Connection header field. These are the headers defined by the
35+
// obsoleted RFC 2616 (section 13.5.1) and are used for backward
36+
// compatibility.
37+
// stolen: net/http/httputil/reverseproxy.go
38+
var hopHeaders = []string{
39+
"Connection",
40+
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
41+
"Keep-Alive",
42+
"Proxy-Authenticate",
43+
"Proxy-Authorization",
44+
"Te", // canonicalized version of "TE"
45+
"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
46+
"Transfer-Encoding",
47+
"Upgrade",
48+
}
49+
850
// NoopReadWriteCloser implements io.ReadWriteCloser, discarding all bytes, Read always returns EOF
951
type NoopReadWriteCloser struct{}
1052

api/server/runner_fninvoke_test.go

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,11 @@ func TestFnInvokeRunnerExecution(t *testing.T) {
164164

165165
srv := testServer(ds, rnr, ServerTypeFull, LimitRequestBody(32256))
166166

167+
inStripHeaders := map[string][]string{"Keep-Alive": {"true"}}
168+
167169
expHeaders := map[string][]string{"Content-Type": {"application/json; charset=utf-8"}}
168170
expCTHeaders := map[string][]string{"Content-Type": {"foo/bar"}}
171+
expStripHeaders := map[string][]string{"Keep-Alive": {""}}
169172

170173
// Checking for EndOfLogs currently depends on scheduling of go-routines (in docker/containerd) that process stderr & stdout.
171174
// Therefore, not testing for EndOfLogs for hot containers (which has complex I/O processing) anymore.
@@ -186,41 +189,46 @@ func TestFnInvokeRunnerExecution(t *testing.T) {
186189
bigoutput := `{"echoContent": "_TRX_ID_", "isDebug": true, "trailerRepeat": 1000}` // 1000 trailers to exceed 2K
187190

188191
bighdroutput := `{"echoContent": "_TRX_ID_", "isDebug": true, "returnHeaders": {"zoo": ["` + strings.Repeat("a", 1024) + `"]}}` // big header to exceed
192+
striphdr := `{"echoContent": "_TRX_ID_", "isDebug": true, "returnHeaders": {"Keep-Alive": ["true"]}}` // this should get stripped
193+
striphdrin := `{"echoContent": "_TRX_ID_", "isDebug": true, "expectHeaders": {"Keep-Alive": [""]}}` // this should get stripped
189194

190195
smalloutput := `{"echoContent": "_TRX_ID_", "isDebug": true, "responseContentType":"application/json; charset=utf-8", "trailerRepeat": 1}` // 1 trailer < 2K
191196

192197
testCases := []struct {
193198
path string
194199
body string
200+
headers map[string][]string
195201
method string
196202
expectedCode int
197203
expectedHeaders map[string][]string
198204
expectedErrSubStr string
199205
expectedLogsSubStr []string
200206
}{
201-
{"/invoke/http_stream_fn_id", ok, http.MethodPost, http.StatusOK, expHeaders, "", nil},
202-
// NOTE: we can't test bad response framing anymore easily (eg invalid http response), should we even worry about it?
203-
{"/invoke/http_stream_fn_id", respTypeLie, http.MethodPost, http.StatusOK, expCTHeaders, "", nil},
204-
{"/invoke/http_stream_fn_id", crasher, http.MethodPost, http.StatusBadGateway, expHeaders, "error receiving function response", nil},
205-
// XXX(reed): we could stop buffering function responses so that we can stream things?
206-
{"/invoke/http_stream_fn_id", bighdroutput, http.MethodPost, http.StatusBadGateway, nil, "function response header too large", nil},
207-
{"/invoke/http_stream_fn_id", bigoutput, http.MethodPost, http.StatusBadGateway, nil, "function response too large", nil},
208-
{"/invoke/http_stream_fn_id", smalloutput, http.MethodPost, http.StatusOK, expHeaders, "", nil},
209-
// XXX(reed): meh we really should try to get oom out, but maybe it's better left to the logs?
210-
{"/invoke/http_stream_fn_id", oomer, http.MethodPost, http.StatusBadGateway, nil, "error receiving function response", nil},
211-
{"/invoke/http_stream_fn_id", bigbuf, http.MethodPost, http.StatusRequestEntityTooLarge, nil, "", nil},
212-
213-
{"/invoke/dne_fn_id", ``, http.MethodPost, http.StatusNotFound, nil, "pull access denied", nil},
214-
{"/invoke/dnereg_fn_id", ``, http.MethodPost, http.StatusBadGateway, nil, "connection refused", nil},
215-
216-
// XXX(reed): what are these?
217-
{"/invoke/http_stream_fn_id", multiLog, http.MethodPost, http.StatusOK, nil, "", multiLogExpectHot},
218-
219-
{"/invoke/fail_fn_quick", ok, http.MethodPost, http.StatusBadGateway, nil, "container failed to initialize", nil},
220-
{"/invoke/fail_fn_timeout", ok, http.MethodPost, http.StatusGatewayTimeout, nil, "Container initialization timed out", nil},
221-
{"/invoke/fn_id", ok, http.MethodPut, http.StatusMethodNotAllowed, nil, "Method not allowed", nil},
222-
223-
{"/invoke/bigmem", ok, http.MethodPost, http.StatusBadRequest, nil, "cannot be allocated", nil},
207+
{"/invoke/http_stream_fn_id", ok, nil, http.MethodPost, http.StatusOK, expHeaders, "", nil},
208+
// NOTE: nil, we can't test bad response framing anymore easily (eg invalid http response), should we even worry about it?
209+
{"/invoke/http_stream_fn_id", respTypeLie, nil, http.MethodPost, http.StatusOK, expCTHeaders, "", nil},
210+
{"/invoke/http_stream_fn_id", crasher, nil, http.MethodPost, http.StatusBadGateway, expHeaders, "error receiving function response", nil},
211+
// XXX(reed): nil, we could stop buffering function responses so that we can stream things?
212+
{"/invoke/http_stream_fn_id", bighdroutput, nil, http.MethodPost, http.StatusBadGateway, nil, "function response header too large", nil},
213+
{"/invoke/http_stream_fn_id", striphdr, nil, http.MethodPost, http.StatusOK, expStripHeaders, "", nil},
214+
{"/invoke/http_stream_fn_id", striphdrin, inStripHeaders, http.MethodPost, http.StatusOK, nil, "", nil},
215+
{"/invoke/http_stream_fn_id", bigoutput, nil, http.MethodPost, http.StatusBadGateway, nil, "function response too large", nil},
216+
{"/invoke/http_stream_fn_id", smalloutput, nil, http.MethodPost, http.StatusOK, expHeaders, "", nil},
217+
// XXX(reed): nil, meh we really should try to get oom out, but maybe it's better left to the logs?
218+
{"/invoke/http_stream_fn_id", oomer, nil, http.MethodPost, http.StatusBadGateway, nil, "error receiving function response", nil},
219+
{"/invoke/http_stream_fn_id", bigbuf, nil, http.MethodPost, http.StatusRequestEntityTooLarge, nil, "", nil},
220+
221+
{"/invoke/dne_fn_id", ``, nil, http.MethodPost, http.StatusNotFound, nil, "pull access denied", nil},
222+
{"/invoke/dnereg_fn_id", ``, nil, http.MethodPost, http.StatusBadGateway, nil, "connection refused", nil},
223+
224+
// XXX(reed): nil, nil, what are these?
225+
{"/invoke/http_stream_fn_id", multiLog, nil, http.MethodPost, http.StatusOK, nil, "", multiLogExpectHot},
226+
227+
{"/invoke/fail_fn_quick", ok, nil, http.MethodPost, http.StatusBadGateway, nil, "container failed to initialize", nil},
228+
{"/invoke/fail_fn_timeout", ok, nil, http.MethodPost, http.StatusGatewayTimeout, nil, "Container initialization timed out", nil},
229+
{"/invoke/fn_id", ok, nil, http.MethodPut, http.StatusMethodNotAllowed, nil, "Method not allowed", nil},
230+
231+
{"/invoke/bigmem", ok, nil, http.MethodPost, http.StatusBadRequest, nil, "cannot be allocated", nil},
224232
}
225233

226234
callIds := make([]string, len(testCases))
@@ -229,7 +237,11 @@ func TestFnInvokeRunnerExecution(t *testing.T) {
229237
t.Run(fmt.Sprintf("Test_%d_%s", i, strings.Replace(test.path, "/", "_", -1)), func(t *testing.T) {
230238
trx := fmt.Sprintf("_trx_%d_", i)
231239
body := strings.NewReader(strings.Replace(test.body, "_TRX_ID_", trx, 1))
232-
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
240+
req := createRequest(t, test.method, test.path, body)
241+
if test.headers != nil {
242+
req.Header = test.headers
243+
}
244+
_, rec := routerRequest2(t, srv.Router, req)
233245
respBytes, _ := ioutil.ReadAll(rec.Body)
234246
respBody := string(respBytes)
235247
maxBody := len(respBody)

api/server/runner_httptrigger.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package server
33
import (
44
"fmt"
55
"net/http"
6-
"net/textproto"
76
"strconv"
87
"strings"
98

109
"github.com/fnproject/fn/api"
10+
"github.com/fnproject/fn/api/common"
1111
"github.com/fnproject/fn/api/models"
1212
"github.com/gin-gonic/gin"
1313
"go.opencensus.io/tag"
@@ -135,15 +135,6 @@ func (trw *triggerResponseWriter) WriteHeader(serviceStatus int) {
135135
trw.inner.WriteHeader(finalStatus)
136136
}
137137

138-
var skipTriggerHeaders = map[string]bool{
139-
"Connection": true,
140-
"Keep-Alive": true,
141-
"Trailer": true,
142-
"Transfer-Encoding": true,
143-
"TE": true,
144-
"Upgrade": true,
145-
}
146-
147138
func reqURL(req *http.Request) string {
148139
if req.URL.Scheme == "" {
149140
if req.TLS == nil {
@@ -164,12 +155,11 @@ func (s *Server) ServeHTTPTrigger(c *gin.Context, app *models.App, fn *models.Fn
164155
// transpose trigger headers into the request
165156
req := c.Request
166157
headers := make(http.Header, len(req.Header))
158+
159+
// remove transport headers before decorating headers
160+
common.StripHopHeaders(req.Header)
161+
167162
for k, vs := range req.Header {
168-
// should be generally unnecessary but to be doubly sure.
169-
k = textproto.CanonicalMIMEHeaderKey(k)
170-
if skipTriggerHeaders[k] {
171-
continue
172-
}
173163
switch k {
174164
case "Content-Type":
175165
default:

api/server/runner_httptrigger_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,13 @@ func TestTriggerRunnerExecution(t *testing.T) {
262262
// these tests are such a pita it's easier to comment most of them out. instead of fixing it i'm doing this fuck me yea
263263
_, _, _, _, _, _, _, _, _, _, _ = expHeaders, expCTHeaders, multiLogExpectHot, crasher, oomer, ok, respTypeLie, multiLog, bigoutput, smalloutput, statusChecker
264264

265-
fooHeader := map[string][]string{"Content-Type": {"application/hateson"}, "Test-Header": {"foo"}}
265+
// Keep-Alive should get stripped, Content-Type should not get framed, Test-Header should get framed
266+
fooHeader := map[string][]string{"Content-Type": {"application/hateson"}, "Test-Header": {"foo"}, "Keep-Alive": {"true"}}
266267
expFooHeaders := map[string][]string{"Content-Type": {"application/hateson"}, "Return-Header": {"foo", "bar"}}
267268
expFooHeadersBody := `{"echoContent": "_TRX_ID_",
268269
"expectHeaders": {
269270
"Content-Type":["application/hateson"],
271+
"Keep-Alive":[""],
270272
"Test-Header":["foo"]
271273
},
272274
"returnHeaders": {

0 commit comments

Comments
 (0)