Skip to content

Commit f3e374d

Browse files
mattwompletwifkak
authored andcommitted
accept: Improve tokenization (#296)
- Support parameter values with commas (inside quotes) - Add a few relevant tests for new support Uses a tokenization algorithm similar to that of spring-projects, which is also under the Apache 2.0 license: https://github.com/spring-projects/spring-framework/
1 parent 60d5479 commit f3e374d

File tree

3 files changed

+65
-14
lines changed

3 files changed

+65
-14
lines changed

packager/accept/accept.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package accept
22

33
import (
4+
"log"
45
"mime"
6+
"strings"
57

68
"github.com/WICG/webpackage/go/signedexchange/version"
79
"github.com/ampproject/amppackager/packager/util"
@@ -18,23 +20,61 @@ const SxgContentType = "application/signed-exchange;v=" + AcceptedSxgVersion
1820
// signedexchange library.
1921
var SxgVersion = version.Version1b3
2022

23+
// Tokenize a comma-separated string of accept patterns into a slice
24+
func tokenize(accept string) []string {
25+
var tokens []string
26+
acceptLen := len(accept)
27+
if acceptLen == 0 {
28+
return tokens
29+
}
30+
31+
inQuotes := false
32+
startIndex := 0
33+
for i := 0; i < acceptLen; i++ {
34+
char := accept[i]
35+
switch char {
36+
case '"':
37+
inQuotes = !inQuotes
38+
case ',':
39+
if !inQuotes {
40+
tokens = append(tokens, util.TrimHeaderValue(accept[startIndex:i]))
41+
startIndex = i + 1
42+
}
43+
case '\\':
44+
if !inQuotes {
45+
log.Printf("unable to parse Accept header: %s", accept)
46+
return []string{}
47+
}
48+
i++
49+
}
50+
}
51+
tokens = append(tokens, util.TrimHeaderValue(accept[startIndex:]))
52+
return tokens
53+
}
54+
55+
// Determine whether a version specified by the accept header matches the
56+
// version of signed exchange output by the packager
57+
func hasMatchingSxgVersion(versions []string) bool {
58+
for _, version := range versions {
59+
if version == AcceptedSxgVersion {
60+
return true
61+
}
62+
}
63+
return false
64+
}
65+
2166
// True if the given Accept header is one that the packager can satisfy. It
2267
// must contain application/signed-exchange;v=$V so that the packager knows
2368
// whether or not it can supply the correct version. "" and "*/*" are not
2469
// satisfiable, for this reason.
2570
func CanSatisfy(accept string) bool {
26-
// There is an edge case on which this comma-splitting fails:
27-
// Accept: application/signed-exchange;junk="some,thing";v=b2
28-
// However, in practice, browsers don't send media types with quoted
29-
// commas in them:
30-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values
31-
// So we'll live with this deficiency for the sake of not forking
32-
// mime.ParseMediaType.
33-
types := util.Comma.Split(accept, -1)
71+
types := tokenize(accept)
3472
for _, mediaRange := range types {
3573
mediatype, params, err := mime.ParseMediaType(mediaRange)
36-
if err == nil && mediatype == "application/signed-exchange" && params["v"] == AcceptedSxgVersion {
37-
return true
74+
if err == nil && mediatype == "application/signed-exchange" {
75+
if hasMatchingSxgVersion(strings.Split(params["v"], ",")) {
76+
return true
77+
}
3878
}
3979
}
4080
return false

packager/accept/accept_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ func TestCanSatisfy(t *testing.T) {
1111
assert.False(t, CanSatisfy("*/*"))
1212
assert.False(t, CanSatisfy("image/jpeg;v=b3"))
1313
assert.False(t, CanSatisfy(`application/signed-exchange;v=b2`))
14-
// This is a bug that will be triggered when a UA starts supporting multiple SXG versions:
15-
assert.False(t, CanSatisfy(`application/signed-exchange;x="a,b";v="b3"`))
14+
assert.False(t, CanSatisfy(`application/signed-exchange;v="b1,b2"`))
15+
assert.False(t, CanSatisfy(`application/signed-exchange;x="y,application/signed-exchange;v=b3,z";v=b1`))
1616

1717
assert.True(t, CanSatisfy(`application/signed-exchange;v=b3`))
1818
assert.True(t, CanSatisfy(`application/signed-exchange;v="b3"`))
19+
assert.True(t, CanSatisfy(`application/signed-exchange;v="b2,b3,b4"`))
1920
assert.True(t, CanSatisfy(`application/signed-exchange;v=b3;q=0.8`))
21+
assert.True(t, CanSatisfy(`application/signed-exchange;v=b2;q=0.9,application/signed-exchange;v="b3,b4";q=0.8`))
2022
assert.True(t, CanSatisfy(`application/signed-exchange;v=b1,application/signed-exchange;v=b3`))
2123
assert.True(t, CanSatisfy(`application/signed-exchange;x="v=b1";v="b3"`))
2224
assert.True(t, CanSatisfy("*/*, application/signed-exchange;v=b3"))
2325
assert.True(t, CanSatisfy("*/* \t,\t application/signed-exchange;v=b3"))
24-
// This is the same bug, though one which won't occur in practice:
25-
assert.True(t, CanSatisfy(`application/signed-exchange;x="y,application/signed-exchange;v=b3,z";v=b1`))
26+
assert.True(t, CanSatisfy(`application/signed-exchange;x="a,b";v="b3"`))
2627
}

packager/util/http.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@ import (
44
"fmt"
55
"regexp"
66
"net/http"
7+
"strings"
78
)
89

910
// A comma, as defined in https://tools.ietf.org/html/rfc7230#section-7, with
1011
// OWS defined in https://tools.ietf.org/html/rfc7230#appendix-B. This is
1112
// commonly used as a separator in header field value definitions.
1213
var Comma *regexp.Regexp = regexp.MustCompile(`[ \t]*,[ \t]*`)
1314

15+
// Trim optional whitespace from a header value, adhering to
16+
// https://tools.ietf.org/html/rfc7230#section-7 with OWS defined in
17+
// https://tools.ietf.org/html/rfc7230#appendix-B.
18+
func TrimHeaderValue(s string) string {
19+
return strings.TrimFunc(s, func(r rune) bool {
20+
return r == ' ' || r == '\t'
21+
})
22+
}
23+
1424
// Conditional request headers that ServeHTTP may receive and need to be sent with fetchURL.
1525
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#Conditional_headers
1626
var ConditionalRequestHeaders = map[string]bool{

0 commit comments

Comments
 (0)