Skip to content

Commit 517face

Browse files
shigekitwifkak
authored andcommitted
Add config.ForwardedRequestHeader (#295)
config.ForwardedRequestHeader allows amppkg to copy specified headers from serveHTTP request to a fetch request. Hop-by-hop headers, conditional request headers and Via cannot be included in it.
1 parent d55d2be commit 517face

File tree

9 files changed

+243
-48
lines changed

9 files changed

+243
-48
lines changed

amppkg.example.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ KeyFile = './pems/privkey.pem'
5858
# locking; consider this especially when utilizing network-mounted storage.
5959
OCSPCache = '/tmp/amppkg-ocsp'
6060

61+
# The list of request header names to be forwarded in a fetch request.
62+
# Hop-by-hop headers, conditional request headers and Via cannot be included.
63+
ForwardedRequestHeaders = []
64+
6165
# This is a simple level of validation, to guard against accidental
6266
# misconfiguration of the reverse proxy that sits in front of the packager.
6367
#

cmd/amppkg/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func main() {
127127
}
128128

129129
packager, err := signer.New(certs[0], key, config.URLSet, rtvCache, certCache.IsHealthy,
130-
overrideBaseURL, /*requireHeaders=*/!*flagDevelopment)
130+
overrideBaseURL, /*requireHeaders=*/!*flagDevelopment, config.ForwardedRequestHeaders)
131131
if err != nil {
132132
die(errors.Wrap(err, "building packager"))
133133
}

cmd/gateway_server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func (s *gatewayServer) GenerateSXG(ctx context.Context, request *pb.SXGRequest)
112112
},
113113
}
114114

115-
packager, err := signer.New(certs[0], privateKey, urlSets, s.rtvCache, shouldPackage, signUrl, false)
115+
packager, err := signer.New(certs[0], privateKey, urlSets, s.rtvCache, shouldPackage, signUrl, false, []string{})
116116

117117
if err != nil {
118118
return errorToSXGResponse(err), nil

packager/signer/signer.go

Lines changed: 13 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,6 @@ const userAgent = "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) " +
4545
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile " +
4646
"Safari/537.36 (compatible; amppackager/0.0.0; +https://github.com/ampproject/amppackager)"
4747

48-
// Conditional request headers that ServeHTTP may receive and need to be sent with fetchURL.
49-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#Conditional_headers
50-
var conditionalRequestHeaders = map[string]bool{
51-
"If-Match": true,
52-
"If-None-Match": true,
53-
"If-Modified-Since": true,
54-
"If-Unmodified-Since": true,
55-
"If-Range": true,
56-
}
57-
5848
// Advised against, per
5949
// https://tools.ietf.org/html/draft-yasskin-httpbis-origin-signed-exchanges-impl-00#section-4.1
6050
// and blocked in http://crrev.com/c/958945.
@@ -113,34 +103,6 @@ var getTransformerRequest = func(r *rtv.RTVCache, s, u string) *rpb.Request {
113103
// in that it allows multiple slashes, as well as initial and terminal slashes.
114104
var protocol = regexp.MustCompile("^[!#$%&'*+\\-.^_`|~0-9a-zA-Z/]+$")
115105

116-
// The following hop-by-hop headers should be removed even when not specified
117-
// in Connection, for backwards compatibility with downstream servers that were
118-
// written against RFC 2616, and expect gateways to behave according to
119-
// https://tools.ietf.org/html/rfc2616#section-13.5.1. (Note: "Trailers" is a
120-
// typo there; should be "Trailer".)
121-
//
122-
// Connection header should also be removed per
123-
// https://tools.ietf.org/html/rfc7230#section-6.1.
124-
//
125-
// Proxy-Connection should also be deleted, per
126-
// https://github.com/WICG/webpackage/pull/339.
127-
var legacyHeaders = []string{"Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Proxy-Connection", "TE", "Trailer", "Transfer-Encoding", "Upgrade"}
128-
129-
// Remove hop-by-hop headers, per https://tools.ietf.org/html/rfc7230#section-6.1.
130-
func removeHopByHopHeaders(resp *http.Response) {
131-
if connections, ok := resp.Header[http.CanonicalHeaderKey("Connection")]; ok {
132-
for _, connection := range connections {
133-
headerNames := util.Comma.Split(connection, -1)
134-
for _, headerName := range headerNames {
135-
resp.Header.Del(headerName)
136-
}
137-
}
138-
}
139-
for _, headerName := range legacyHeaders {
140-
resp.Header.Del(headerName)
141-
}
142-
}
143-
144106
// Gets all values of the named header, joined on comma.
145107
func GetJoined(h http.Header, name string) string {
146108
if values, ok := h[http.CanonicalHeaderKey(name)]; ok {
@@ -168,6 +130,7 @@ type Signer struct {
168130
shouldPackage func() bool
169131
overrideBaseURL *url.URL
170132
requireHeaders bool
133+
forwardedRequestHeaders []string
171134
}
172135

173136
func noRedirects(req *http.Request, via []*http.Request) error {
@@ -176,14 +139,14 @@ func noRedirects(req *http.Request, via []*http.Request) error {
176139

177140
func New(cert *x509.Certificate, key crypto.PrivateKey, urlSets []util.URLSet,
178141
rtvCache *rtv.RTVCache, shouldPackage func() bool, overrideBaseURL *url.URL,
179-
requireHeaders bool) (*Signer, error) {
142+
requireHeaders bool, forwardedRequestHeaders []string) (*Signer, error) {
180143
client := http.Client{
181144
CheckRedirect: noRedirects,
182145
// TODO(twifkak): Load-test and see if default transport settings are okay.
183146
Timeout: 60 * time.Second,
184147
}
185148

186-
return &Signer{cert, key, &client, urlSets, rtvCache, shouldPackage, overrideBaseURL, requireHeaders}, nil
149+
return &Signer{cert, key, &client, urlSets, rtvCache, shouldPackage, overrideBaseURL, requireHeaders, forwardedRequestHeaders}, nil
187150
}
188151

189152
func (this *Signer) fetchURL(fetch *url.URL, serveHTTPReq *http.Request) (*http.Request, *http.Response, *util.HTTPError) {
@@ -195,6 +158,14 @@ func (this *Signer) fetchURL(fetch *url.URL, serveHTTPReq *http.Request) (*http.
195158
return nil, nil, util.NewHTTPError(http.StatusInternalServerError, "Error building request: ", err)
196159
}
197160
req.Header.Set("User-Agent", userAgent)
161+
// copy forwardedRequestHeaders
162+
for _, header := range this.forwardedRequestHeaders {
163+
if http.CanonicalHeaderKey(header) == "Host" {
164+
req.Host = serveHTTPReq.Host
165+
} else if value := GetJoined(serveHTTPReq.Header, header); value != "" {
166+
req.Header.Set(header, value)
167+
}
168+
}
198169
// Golang's HTTP parser appears not to validate the protocol it parses
199170
// from the request line, so we do so here.
200171
if protocol.MatchString(serveHTTPReq.Proto) {
@@ -206,7 +177,7 @@ func (this *Signer) fetchURL(fetch *url.URL, serveHTTPReq *http.Request) (*http.
206177
req.Header.Set("Via", via)
207178
}
208179
// Set conditional headers that were included in ServeHTTP's Request.
209-
for header := range conditionalRequestHeaders {
180+
for header := range util.ConditionalRequestHeaders {
210181
if value := GetJoined(serveHTTPReq.Header, header); value != "" {
211182
req.Header.Set(header, value)
212183
}
@@ -215,7 +186,7 @@ func (this *Signer) fetchURL(fetch *url.URL, serveHTTPReq *http.Request) (*http.
215186
if err != nil {
216187
return nil, nil, util.NewHTTPError(http.StatusBadGateway, "Error fetching: ", err)
217188
}
218-
removeHopByHopHeaders(resp)
189+
util.RemoveHopByHopHeaders(resp.Header)
219190
return req, resp, nil
220191
}
221192

packager/signer/signer_test.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ type SignerSuite struct {
6262
}
6363

6464
func (this *SignerSuite) new(urlSets []util.URLSet) *Signer {
65-
handler, err := New(pkgt.Certs[0], pkgt.Key, urlSets, &rtv.RTVCache{}, func() bool { return this.shouldPackage }, nil, true)
65+
forwardedRequestHeaders := []string{"Host", "X-Foo"}
66+
handler, err := New(pkgt.Certs[0], pkgt.Key, urlSets, &rtv.RTVCache{}, func() bool { return this.shouldPackage }, nil, true, forwardedRequestHeaders)
6667
this.Require().NoError(err)
6768
// Accept the self-signed certificate generated by the test server.
6869
handler.client = this.httpsClient
@@ -78,6 +79,10 @@ func (this *SignerSuite) getP(t *testing.T, handler pkgt.AlmostHandler, target s
7879
"AMP-Cache-Transform": {"google"}, "Accept": {"application/signed-exchange;v=" + accept.AcceptedSxgVersion}}, params)
7980
}
8081

82+
func (this *SignerSuite) getFRH(t *testing.T, handler pkgt.AlmostHandler, target string, host string, header http.Header) *http.Response {
83+
return pkgt.GetBHHP(t, handler, target, host, header, httprouter.Params{})
84+
}
85+
8186
func (this *SignerSuite) getB(t *testing.T, handler pkgt.AlmostHandler, target string, body string) *http.Response {
8287
return pkgt.GetBH(t, handler, target, strings.NewReader(body), http.Header{
8388
"AMP-Cache-Transform": {"google"}, "Accept": {"application/signed-exchange;v=" + accept.AcceptedSxgVersion}})
@@ -101,6 +106,17 @@ func (this *SignerSuite) httpSignURL() string {
101106
return u.String()
102107
}
103108

109+
func (this *SignerSuite) certSubjectCN() string {
110+
return pkgt.Certs[0].Subject.CommonName
111+
}
112+
113+
func (this *SignerSuite) httpSignURL_CertSubjectCN() string {
114+
u, err := url.Parse(this.certSubjectCN())
115+
this.Require().NoError(err)
116+
u.Scheme = "https"
117+
return u.String()
118+
}
119+
104120
func (this *SignerSuite) httpsURL() string {
105121
return this.tlsServer.URL
106122
}
@@ -185,6 +201,54 @@ func (this *SignerSuite) TestSimple() {
185201
this.Assert().Equal(append(payloadPrefix.Bytes(), transformedBody...), exchange.Payload)
186202
}
187203

204+
func (this *SignerSuite) TestFetchSignWithForwardedRequestHeaders() {
205+
urlSets := []util.URLSet{{
206+
Sign: &util.URLPattern{[]string{"https"}, "", this.certSubjectCN(), stringPtr("/amp/.*"), []string{}, stringPtr(""), false, 2000, nil},
207+
Fetch: &util.URLPattern{[]string{"http"}, "", this.httpHost(), stringPtr("/amp/.*"), []string{}, stringPtr(""), false, 2000, boolPtr(true)},
208+
}}
209+
this.fakeHandler = func(resp http.ResponseWriter, req *http.Request) {
210+
// Host and X-Foo headers are forwarded with forwardedRequestHeaders
211+
this.Assert().Equal("www.example.com", req.Host)
212+
this.Assert().Equal("foo", req.Header.Get("X-Foo"))
213+
this.Assert().Equal("", req.Header.Get("X-Bar"))
214+
this.lastRequest = req
215+
resp.Header().Set("Content-Type", "text/html")
216+
resp.Write(fakeBody)
217+
}
218+
// Request with ForwardedRequestHeaders
219+
header := http.Header{"AMP-Cache-Transform": {"google"}, "Accept": {"application/signed-exchange;v=" + accept.AcceptedSxgVersion},
220+
"X-Foo": {"foo"}, "X-Bar": {"bar"}}
221+
resp := this.getFRH(this.T(), this.new(urlSets),
222+
"/priv/doc?fetch="+url.QueryEscape(this.httpURL()+fakePath)+"&sign="+url.QueryEscape(this.httpSignURL_CertSubjectCN()+fakePath),
223+
"www.example.com", header)
224+
this.Assert().Equal(fakePath, this.lastRequest.URL.String())
225+
this.Assert().Equal(userAgent, this.lastRequest.Header.Get("User-Agent"))
226+
this.Assert().Equal("1.1 amppkg", this.lastRequest.Header.Get("Via"))
227+
this.Assert().Equal(http.StatusOK, resp.StatusCode, "incorrect status: %#v", resp)
228+
this.Assert().Equal(fmt.Sprintf(`google;v="%d"`, transformer.SupportedVersions[0].Max), resp.Header.Get("AMP-Cache-Transform"))
229+
this.Assert().Equal("nosniff", resp.Header.Get("X-Content-Type-Options"))
230+
this.Assert().Equal("Accept, AMP-Cache-Transform", resp.Header.Get("Vary"))
231+
exchange, err := signedexchange.ReadExchange(resp.Body)
232+
this.Require().NoError(err)
233+
this.Assert().Equal(this.httpSignURL_CertSubjectCN()+fakePath, exchange.RequestURI)
234+
this.Assert().Equal("GET", exchange.RequestMethod)
235+
this.Assert().Equal(http.Header{}, exchange.RequestHeaders)
236+
this.Assert().Equal(200, exchange.ResponseStatus)
237+
this.Assert().Equal(
238+
[]string{"content-encoding", "content-length", "content-security-policy", "content-type", "date", "digest", "x-content-type-options"},
239+
headerNames(exchange.ResponseHeaders))
240+
this.Assert().Equal("text/html", exchange.ResponseHeaders.Get("Content-Type"))
241+
this.Assert().Equal("text/html", exchange.ResponseHeaders.Get("Content-Type"))
242+
this.Assert().Equal("nosniff", exchange.ResponseHeaders.Get("X-Content-Type-Options"))
243+
this.Assert().Contains(exchange.SignatureHeaderValue, "validity-url=\""+this.httpSignURL_CertSubjectCN()+"/amppkg/validity\"")
244+
this.Assert().Contains(exchange.SignatureHeaderValue, "integrity=\"digest/mi-sha256-03\"")
245+
this.Assert().Contains(exchange.SignatureHeaderValue, "cert-url=\""+this.httpSignURL_CertSubjectCN()+"/amppkg/cert/"+pkgt.CertName+"\"")
246+
this.Assert().Contains(exchange.SignatureHeaderValue, "cert-sha256=*"+pkgt.CertName+"=*")
247+
var payloadPrefix bytes.Buffer
248+
binary.Write(&payloadPrefix, binary.BigEndian, uint64(miRecordSize))
249+
this.Assert().Equal(append(payloadPrefix.Bytes(), transformedBody...), exchange.Payload)
250+
}
251+
188252
func (this *SignerSuite) TestParamsInPostBody() {
189253
urlSets := []util.URLSet{{
190254
Sign: &util.URLPattern{[]string{"https"}, "", this.httpHost(), stringPtr("/amp/.*"), []string{}, stringPtr(""), false, 2000, nil},

packager/testing/testing.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,18 @@ func GetP(t *testing.T, handler AlmostHandler, target string, params httprouter.
6666
}
6767

6868
func GetBH(t *testing.T, handler AlmostHandler, target string, body io.Reader, headers http.Header) *http.Response {
69-
return GetBHP(t, handler, target, body, headers, httprouter.Params{})
69+
return GetBHP(t, handler, target, "", body, headers, httprouter.Params{})
7070
}
7171

7272
func GetHP(t *testing.T, handler AlmostHandler, target string, headers http.Header, params httprouter.Params) *http.Response {
73-
return GetBHP(t, handler, target, nil, headers, params)
73+
return GetBHP(t, handler, target, "", nil, headers, params)
7474
}
7575

76-
func GetBHP(t *testing.T, handler AlmostHandler, target string, body io.Reader, headers http.Header, params httprouter.Params) *http.Response {
76+
func GetBHHP(t *testing.T, handler AlmostHandler, target string, host string, headers http.Header, params httprouter.Params) *http.Response {
77+
return GetBHP(t, handler, target, host, nil, headers, params)
78+
}
79+
80+
func GetBHP(t *testing.T, handler AlmostHandler, target string, host string, body io.Reader, headers http.Header, params httprouter.Params) *http.Response {
7781
rec := httptest.NewRecorder()
7882
method := ""
7983
if body != nil {
@@ -86,6 +90,9 @@ func GetBHP(t *testing.T, handler AlmostHandler, target string, body io.Reader,
8690
req.Header.Add(name, value)
8791
}
8892
}
93+
if host != "" {
94+
req.Host = host
95+
}
8996
handler.ServeHTTP(rec, req, params)
9097
return rec.Result()
9198
}

packager/util/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Config struct {
2929
CertFile string // This must be the full certificate chain.
3030
KeyFile string // Just for the first cert, obviously.
3131
OCSPCache string
32+
ForwardedRequestHeaders []string
3233
URLSet []URLSet
3334
}
3435

@@ -142,6 +143,15 @@ func ValidateFetchURLPattern(pattern *URLPattern) error {
142143
return nil
143144
}
144145

146+
func ValidateForwardedRequestHeaders(hs []string) error {
147+
for _, h := range hs {
148+
if msg := haveInvalidForwardedRequestHeader(h); msg != "" {
149+
return errors.Errorf("ForwardedRequestHeaders must not %s", msg)
150+
}
151+
}
152+
return nil
153+
}
154+
145155
// ReadConfig reads the config file specified at --config and validates it.
146156
func ReadConfig(configBytes []byte) (*Config, error) {
147157
tree, err := toml.LoadBytes(configBytes)
@@ -166,6 +176,11 @@ func ReadConfig(configBytes []byte) (*Config, error) {
166176
if config.OCSPCache == "" {
167177
return nil, errors.New("must specify OCSPCache")
168178
}
179+
if len(config.ForwardedRequestHeaders) > 0 {
180+
if err := ValidateForwardedRequestHeaders(config.ForwardedRequestHeaders); err != nil {
181+
return nil, err
182+
}
183+
}
169184
ocspDir := filepath.Dir(config.OCSPCache)
170185
if stat, err := os.Stat(ocspDir); os.IsNotExist(err) || !stat.Mode().IsDir() {
171186
return nil, errors.Errorf("OCSPCache parent directory must exist: %s", ocspDir)

packager/util/config_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,70 @@ func TestMinimalValidConfig(t *testing.T) {
5151
}, *config)
5252
}
5353

54+
func TestForwardedRequestHeader(t *testing.T) {
55+
config, err := ReadConfig([]byte(`
56+
CertFile = "cert.pem"
57+
KeyFile = "key.pem"
58+
OCSPCache = "/tmp/ocsp"
59+
ForwardedRequestHeaders = ["X-Foo", "X-Bar"]
60+
[[URLSet]]
61+
[URLSet.Sign]
62+
Domain = "example.com"
63+
`))
64+
require.NoError(t, err)
65+
assert.Equal(t, Config{
66+
Port: 8080,
67+
CertFile: "cert.pem",
68+
KeyFile: "key.pem",
69+
OCSPCache: "/tmp/ocsp",
70+
ForwardedRequestHeaders: []string{"X-Foo", "X-Bar"},
71+
URLSet: []URLSet{{
72+
Sign: &URLPattern{
73+
Domain: "example.com",
74+
PathRE: stringPtr(".*"),
75+
QueryRE: stringPtr(""),
76+
MaxLength: 2000,
77+
},
78+
}},
79+
}, *config)
80+
}
81+
82+
func TestForwardedRequestHeadersHaveHopByHopHeader(t *testing.T) {
83+
assert.Contains(t, errorFrom(ReadConfig([]byte(`
84+
CertFile = "cert.pem"
85+
KeyFile = "key.pem"
86+
OCSPCache = "/tmp/ocsp"
87+
ForwardedRequestHeaders = ["X-Foo", "X-Bar", "connection"]
88+
[[URLSet]]
89+
[URLSet.Sign]
90+
Domain = "example.com"
91+
`))), "ForwardedRequestHeaders must not have hop-by-hop header of connection")
92+
}
93+
94+
func TestForwardedRequestHeadersHaveConditionalRequestHeader(t *testing.T) {
95+
assert.Contains(t, errorFrom(ReadConfig([]byte(`
96+
CertFile = "cert.pem"
97+
KeyFile = "key.pem"
98+
OCSPCache = "/tmp/ocsp"
99+
ForwardedRequestHeaders = ["X-Foo", "X-Bar", "if-match"]
100+
[[URLSet]]
101+
[URLSet.Sign]
102+
Domain = "example.com"
103+
`))), "ForwardedRequestHeaders must not have conditional request header of if-match")
104+
}
105+
106+
func TestForwardedRequestHeadersHaveDisallowedHeader(t *testing.T) {
107+
assert.Contains(t, errorFrom(ReadConfig([]byte(`
108+
CertFile = "cert.pem"
109+
KeyFile = "key.pem"
110+
OCSPCache = "/tmp/ocsp"
111+
ForwardedRequestHeaders = ["X-Foo", "X-Bar", "via"]
112+
[[URLSet]]
113+
[URLSet.Sign]
114+
Domain = "example.com"
115+
`))), "ForwardedRequestHeaders must not include request header of via")
116+
}
117+
54118
func TestOCSPDirDoesntExist(t *testing.T) {
55119
assert.Contains(t, errorFrom(ReadConfig([]byte(`
56120
CertFile = "cert.pem"

0 commit comments

Comments
 (0)