aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2023-10-08 12:35:38 -0700
committerMike Crute <mike@crute.us>2023-10-08 12:35:38 -0700
commitd51849bf18a32de34c35d75c118caef5294223e8 (patch)
tree5dc9eb1437eb7ffd0c635947cca275032fe390ea
parent2d903b840e18144f29a4f4a9a0305f8ba16f19a3 (diff)
downloadgolib-d51849bf18a32de34c35d75c118caef5294223e8.tar.bz2
golib-d51849bf18a32de34c35d75c118caef5294223e8.tar.xz
golib-d51849bf18a32de34c35d75c118caef5294223e8.zip
httputil: add header parsing from godoc
-rw-r--r--httputil/buster.go95
-rw-r--r--httputil/buster_test.go29
-rw-r--r--httputil/header/header.go298
-rw-r--r--httputil/header/header_test.go139
-rw-r--r--httputil/httputil.go25
-rw-r--r--httputil/negotiate.go79
-rw-r--r--httputil/negotiate_test.go71
-rw-r--r--httputil/respbuf.go58
-rw-r--r--httputil/static.go265
-rw-r--r--httputil/static_test.go175
-rw-r--r--httputil/transport.go87
-rw-r--r--httputil/transport_test.go126
12 files changed, 1447 insertions, 0 deletions
diff --git a/httputil/buster.go b/httputil/buster.go
new file mode 100644
index 0000000..beab151
--- /dev/null
+++ b/httputil/buster.go
@@ -0,0 +1,95 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil
8
9import (
10 "io"
11 "io/ioutil"
12 "net/http"
13 "net/url"
14 "strings"
15 "sync"
16)
17
18type busterWriter struct {
19 headerMap http.Header
20 status int
21 io.Writer
22}
23
24func (bw *busterWriter) Header() http.Header {
25 return bw.headerMap
26}
27
28func (bw *busterWriter) WriteHeader(status int) {
29 bw.status = status
30}
31
32// CacheBusters maintains a cache of cache busting tokens for static resources served by Handler.
33type CacheBusters struct {
34 Handler http.Handler
35
36 mu sync.Mutex
37 tokens map[string]string
38}
39
40func sanitizeTokenRune(r rune) rune {
41 if r <= ' ' || r >= 127 {
42 return -1
43 }
44 // Convert percent encoding reserved characters to '-'.
45 if strings.ContainsRune("!#$&'()*+,/:;=?@[]", r) {
46 return '-'
47 }
48 return r
49}
50
51// Get returns the cache busting token for path. If the token is not already
52// cached, Get issues a HEAD request on handler and uses the response ETag and
53// Last-Modified headers to compute a token.
54func (cb *CacheBusters) Get(path string) string {
55 cb.mu.Lock()
56 if cb.tokens == nil {
57 cb.tokens = make(map[string]string)
58 }
59 token, ok := cb.tokens[path]
60 cb.mu.Unlock()
61 if ok {
62 return token
63 }
64
65 w := busterWriter{
66 Writer: ioutil.Discard,
67 headerMap: make(http.Header),
68 }
69 r := &http.Request{URL: &url.URL{Path: path}, Method: "HEAD"}
70 cb.Handler.ServeHTTP(&w, r)
71
72 if w.status == 200 {
73 token = w.headerMap.Get("Etag")
74 if token == "" {
75 token = w.headerMap.Get("Last-Modified")
76 }
77 token = strings.Trim(token, `" `)
78 token = strings.Map(sanitizeTokenRune, token)
79 }
80
81 cb.mu.Lock()
82 cb.tokens[path] = token
83 cb.mu.Unlock()
84
85 return token
86}
87
88// AppendQueryParam appends the token as a query parameter to path.
89func (cb *CacheBusters) AppendQueryParam(path string, name string) string {
90 token := cb.Get(path)
91 if token == "" {
92 return path
93 }
94 return path + "?" + name + "=" + token
95}
diff --git a/httputil/buster_test.go b/httputil/buster_test.go
new file mode 100644
index 0000000..eb44cf4
--- /dev/null
+++ b/httputil/buster_test.go
@@ -0,0 +1,29 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil
8
9import (
10 "net/http"
11 "testing"
12)
13
14func TestCacheBusters(t *testing.T) {
15 cbs := &CacheBusters{Handler: http.FileServer(http.Dir("."))}
16
17 token := cbs.Get("/buster_test.go")
18 if token == "" {
19 t.Errorf("could not extract token from http.FileServer")
20 }
21
22 var ss StaticServer
23 cbs = &CacheBusters{Handler: ss.FileHandler("buster_test.go")}
24
25 token = cbs.Get("/xxx")
26 if token == "" {
27 t.Errorf("could not extract token from StaticServer FileHandler")
28 }
29}
diff --git a/httputil/header/header.go b/httputil/header/header.go
new file mode 100644
index 0000000..0f1572e
--- /dev/null
+++ b/httputil/header/header.go
@@ -0,0 +1,298 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7// Package header provides functions for parsing HTTP headers.
8package header
9
10import (
11 "net/http"
12 "strings"
13 "time"
14)
15
16// Octet types from RFC 2616.
17var octetTypes [256]octetType
18
19type octetType byte
20
21const (
22 isToken octetType = 1 << iota
23 isSpace
24)
25
26func init() {
27 // OCTET = <any 8-bit sequence of data>
28 // CHAR = <any US-ASCII character (octets 0 - 127)>
29 // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
30 // CR = <US-ASCII CR, carriage return (13)>
31 // LF = <US-ASCII LF, linefeed (10)>
32 // SP = <US-ASCII SP, space (32)>
33 // HT = <US-ASCII HT, horizontal-tab (9)>
34 // <"> = <US-ASCII double-quote mark (34)>
35 // CRLF = CR LF
36 // LWS = [CRLF] 1*( SP | HT )
37 // TEXT = <any OCTET except CTLs, but including LWS>
38 // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
39 // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
40 // token = 1*<any CHAR except CTLs or separators>
41 // qdtext = <any TEXT except <">>
42
43 for c := 0; c < 256; c++ {
44 var t octetType
45 isCtl := c <= 31 || c == 127
46 isChar := 0 <= c && c <= 127
47 isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
48 if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
49 t |= isSpace
50 }
51 if isChar && !isCtl && !isSeparator {
52 t |= isToken
53 }
54 octetTypes[c] = t
55 }
56}
57
58// Copy returns a shallow copy of the header.
59func Copy(header http.Header) http.Header {
60 h := make(http.Header)
61 for k, vs := range header {
62 h[k] = vs
63 }
64 return h
65}
66
67var timeLayouts = []string{"Mon, 02 Jan 2006 15:04:05 GMT", time.RFC850, time.ANSIC}
68
69// ParseTime parses the header as time. The zero value is returned if the
70// header is not present or there is an error parsing the
71// header.
72func ParseTime(header http.Header, key string) time.Time {
73 if s := header.Get(key); s != "" {
74 for _, layout := range timeLayouts {
75 if t, err := time.Parse(layout, s); err == nil {
76 return t.UTC()
77 }
78 }
79 }
80 return time.Time{}
81}
82
83// ParseList parses a comma separated list of values. Commas are ignored in
84// quoted strings. Quoted values are not unescaped or unquoted. Whitespace is
85// trimmed.
86func ParseList(header http.Header, key string) []string {
87 var result []string
88 for _, s := range header[http.CanonicalHeaderKey(key)] {
89 begin := 0
90 end := 0
91 escape := false
92 quote := false
93 for i := 0; i < len(s); i++ {
94 b := s[i]
95 switch {
96 case escape:
97 escape = false
98 end = i + 1
99 case quote:
100 switch b {
101 case '\\':
102 escape = true
103 case '"':
104 quote = false
105 }
106 end = i + 1
107 case b == '"':
108 quote = true
109 end = i + 1
110 case octetTypes[b]&isSpace != 0:
111 if begin == end {
112 begin = i + 1
113 end = begin
114 }
115 case b == ',':
116 if begin < end {
117 result = append(result, s[begin:end])
118 }
119 begin = i + 1
120 end = begin
121 default:
122 end = i + 1
123 }
124 }
125 if begin < end {
126 result = append(result, s[begin:end])
127 }
128 }
129 return result
130}
131
132// ParseValueAndParams parses a comma separated list of values with optional
133// semicolon separated name-value pairs. Content-Type and Content-Disposition
134// headers are in this format.
135func ParseValueAndParams(header http.Header, key string) (value string, params map[string]string) {
136 params = make(map[string]string)
137 s := header.Get(key)
138 value, s = expectTokenSlash(s)
139 if value == "" {
140 return
141 }
142 value = strings.ToLower(value)
143 s = skipSpace(s)
144 for strings.HasPrefix(s, ";") {
145 var pkey string
146 pkey, s = expectToken(skipSpace(s[1:]))
147 if pkey == "" {
148 return
149 }
150 if !strings.HasPrefix(s, "=") {
151 return
152 }
153 var pvalue string
154 pvalue, s = expectTokenOrQuoted(s[1:])
155 if pvalue == "" {
156 return
157 }
158 pkey = strings.ToLower(pkey)
159 params[pkey] = pvalue
160 s = skipSpace(s)
161 }
162 return
163}
164
165// AcceptSpec describes an Accept* header.
166type AcceptSpec struct {
167 Value string
168 Q float64
169}
170
171// ParseAccept parses Accept* headers.
172func ParseAccept(header http.Header, key string) (specs []AcceptSpec) {
173loop:
174 for _, s := range header[key] {
175 for {
176 var spec AcceptSpec
177 spec.Value, s = expectTokenSlash(s)
178 if spec.Value == "" {
179 continue loop
180 }
181 spec.Q = 1.0
182 s = skipSpace(s)
183 if strings.HasPrefix(s, ";") {
184 s = skipSpace(s[1:])
185 if !strings.HasPrefix(s, "q=") {
186 continue loop
187 }
188 spec.Q, s = expectQuality(s[2:])
189 if spec.Q < 0.0 {
190 continue loop
191 }
192 }
193 specs = append(specs, spec)
194 s = skipSpace(s)
195 if !strings.HasPrefix(s, ",") {
196 continue loop
197 }
198 s = skipSpace(s[1:])
199 }
200 }
201 return
202}
203
204func skipSpace(s string) (rest string) {
205 i := 0
206 for ; i < len(s); i++ {
207 if octetTypes[s[i]]&isSpace == 0 {
208 break
209 }
210 }
211 return s[i:]
212}
213
214func expectToken(s string) (token, rest string) {
215 i := 0
216 for ; i < len(s); i++ {
217 if octetTypes[s[i]]&isToken == 0 {
218 break
219 }
220 }
221 return s[:i], s[i:]
222}
223
224func expectTokenSlash(s string) (token, rest string) {
225 i := 0
226 for ; i < len(s); i++ {
227 b := s[i]
228 if (octetTypes[b]&isToken == 0) && b != '/' {
229 break
230 }
231 }
232 return s[:i], s[i:]
233}
234
235func expectQuality(s string) (q float64, rest string) {
236 switch {
237 case len(s) == 0:
238 return -1, ""
239 case s[0] == '0':
240 q = 0
241 case s[0] == '1':
242 q = 1
243 default:
244 return -1, ""
245 }
246 s = s[1:]
247 if !strings.HasPrefix(s, ".") {
248 return q, s
249 }
250 s = s[1:]
251 i := 0
252 n := 0
253 d := 1
254 for ; i < len(s); i++ {
255 b := s[i]
256 if b < '0' || b > '9' {
257 break
258 }
259 n = n*10 + int(b) - '0'
260 d *= 10
261 }
262 return q + float64(n)/float64(d), s[i:]
263}
264
265func expectTokenOrQuoted(s string) (value string, rest string) {
266 if !strings.HasPrefix(s, "\"") {
267 return expectToken(s)
268 }
269 s = s[1:]
270 for i := 0; i < len(s); i++ {
271 switch s[i] {
272 case '"':
273 return s[:i], s[i+1:]
274 case '\\':
275 p := make([]byte, len(s)-1)
276 j := copy(p, s[:i])
277 escape := true
278 for i = i + 1; i < len(s); i++ {
279 b := s[i]
280 switch {
281 case escape:
282 escape = false
283 p[j] = b
284 j++
285 case b == '\\':
286 escape = true
287 case b == '"':
288 return string(p[:j]), s[i+1:]
289 default:
290 p[j] = b
291 j++
292 }
293 }
294 return "", ""
295 }
296 }
297 return "", ""
298}
diff --git a/httputil/header/header_test.go b/httputil/header/header_test.go
new file mode 100644
index 0000000..97c8685
--- /dev/null
+++ b/httputil/header/header_test.go
@@ -0,0 +1,139 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package header
8
9import (
10 "net/http"
11 "testing"
12 "time"
13
14 "github.com/google/go-cmp/cmp"
15)
16
17var getHeaderListTests = []struct {
18 s string
19 l []string
20}{
21 {s: `a`, l: []string{`a`}},
22 {s: `a, b , c `, l: []string{`a`, `b`, `c`}},
23 {s: `a,, b , , c `, l: []string{`a`, `b`, `c`}},
24 {s: `a,b,c`, l: []string{`a`, `b`, `c`}},
25 {s: ` a b, c d `, l: []string{`a b`, `c d`}},
26 {s: `"a, b, c", d `, l: []string{`"a, b, c"`, "d"}},
27 {s: `","`, l: []string{`","`}},
28 {s: `"\""`, l: []string{`"\""`}},
29 {s: `" "`, l: []string{`" "`}},
30}
31
32func TestGetHeaderList(t *testing.T) {
33 for _, tt := range getHeaderListTests {
34 header := http.Header{"Foo": {tt.s}}
35 if l := ParseList(header, "foo"); !cmp.Equal(tt.l, l) {
36 t.Errorf("ParseList for %q = %q, want %q", tt.s, l, tt.l)
37 }
38 }
39}
40
41var parseValueAndParamsTests = []struct {
42 s string
43 value string
44 params map[string]string
45}{
46 {`text/html`, "text/html", map[string]string{}},
47 {`text/html `, "text/html", map[string]string{}},
48 {`text/html ; `, "text/html", map[string]string{}},
49 {`tExt/htMl`, "text/html", map[string]string{}},
50 {`tExt/htMl; fOO=";"; hellO=world`, "text/html", map[string]string{
51 "hello": "world",
52 "foo": `;`,
53 }},
54 {`text/html; foo=bar, hello=world`, "text/html", map[string]string{"foo": "bar"}},
55 {`text/html ; foo=bar `, "text/html", map[string]string{"foo": "bar"}},
56 {`text/html ;foo=bar `, "text/html", map[string]string{"foo": "bar"}},
57 {`text/html; foo="b\ar"`, "text/html", map[string]string{"foo": "bar"}},
58 {`text/html; foo="bar\"baz\"qux"`, "text/html", map[string]string{"foo": `bar"baz"qux`}},
59 {`text/html; foo="b,ar"`, "text/html", map[string]string{"foo": "b,ar"}},
60 {`text/html; foo="b;ar"`, "text/html", map[string]string{"foo": "b;ar"}},
61 {`text/html; FOO="bar"`, "text/html", map[string]string{"foo": "bar"}},
62 {`form-data; filename="file.txt"; name=file`, "form-data", map[string]string{"filename": "file.txt", "name": "file"}},
63}
64
65func TestParseValueAndParams(t *testing.T) {
66 for _, tt := range parseValueAndParamsTests {
67 header := http.Header{"Content-Type": {tt.s}}
68 value, params := ParseValueAndParams(header, "Content-Type")
69 if value != tt.value {
70 t.Errorf("%q, value=%q, want %q", tt.s, value, tt.value)
71 }
72 if !cmp.Equal(params, tt.params) {
73 t.Errorf("%q, param=%#v, want %#v", tt.s, params, tt.params)
74 }
75 }
76}
77
78var parseTimeValidTests = []string{
79 "Sun, 06 Nov 1994 08:49:37 GMT",
80 "Sunday, 06-Nov-94 08:49:37 GMT",
81 "Sun Nov 6 08:49:37 1994",
82}
83
84var parseTimeInvalidTests = []string{
85 "junk",
86}
87
88func TestParseTime(t *testing.T) {
89 expected := time.Date(1994, 11, 6, 8, 49, 37, 0, time.UTC)
90 for _, s := range parseTimeValidTests {
91 header := http.Header{"Date": {s}}
92 actual := ParseTime(header, "Date")
93 if actual != expected {
94 t.Errorf("GetTime(%q)=%v, want %v", s, actual, expected)
95 }
96 }
97 for _, s := range parseTimeInvalidTests {
98 header := http.Header{"Date": {s}}
99 actual := ParseTime(header, "Date")
100 if !actual.IsZero() {
101 t.Errorf("GetTime(%q) did not return zero", s)
102 }
103 }
104}
105
106var parseAcceptTests = []struct {
107 s string
108 expected []AcceptSpec
109}{
110 {"text/html", []AcceptSpec{{"text/html", 1}}},
111 {"text/html; q=0", []AcceptSpec{{"text/html", 0}}},
112 {"text/html; q=0.0", []AcceptSpec{{"text/html", 0}}},
113 {"text/html; q=1", []AcceptSpec{{"text/html", 1}}},
114 {"text/html; q=1.0", []AcceptSpec{{"text/html", 1}}},
115 {"text/html; q=0.1", []AcceptSpec{{"text/html", 0.1}}},
116 {"text/html;q=0.1", []AcceptSpec{{"text/html", 0.1}}},
117 {"text/html, text/plain", []AcceptSpec{{"text/html", 1}, {"text/plain", 1}}},
118 {"text/html; q=0.1, text/plain", []AcceptSpec{{"text/html", 0.1}, {"text/plain", 1}}},
119 {"iso-8859-5, unicode-1-1;q=0.8,iso-8859-1", []AcceptSpec{{"iso-8859-5", 1}, {"unicode-1-1", 0.8}, {"iso-8859-1", 1}}},
120 {"iso-8859-1", []AcceptSpec{{"iso-8859-1", 1}}},
121 {"*", []AcceptSpec{{"*", 1}}},
122 {"da, en-gb;q=0.8, en;q=0.7", []AcceptSpec{{"da", 1}, {"en-gb", 0.8}, {"en", 0.7}}},
123 {"da, q, en-gb;q=0.8", []AcceptSpec{{"da", 1}, {"q", 1}, {"en-gb", 0.8}}},
124 {"image/png, image/*;q=0.5", []AcceptSpec{{"image/png", 1}, {"image/*", 0.5}}},
125
126 // bad cases
127 {"value1; q=0.1.2", []AcceptSpec{{"value1", 0.1}}},
128 {"da, en-gb;q=foo", []AcceptSpec{{"da", 1}}},
129}
130
131func TestParseAccept(t *testing.T) {
132 for _, tt := range parseAcceptTests {
133 header := http.Header{"Accept": {tt.s}}
134 actual := ParseAccept(header, "Accept")
135 if !cmp.Equal(actual, tt.expected) {
136 t.Errorf("ParseAccept(h, %q)=%v, want %v", tt.s, actual, tt.expected)
137 }
138 }
139}
diff --git a/httputil/httputil.go b/httputil/httputil.go
new file mode 100644
index 0000000..a03717c
--- /dev/null
+++ b/httputil/httputil.go
@@ -0,0 +1,25 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7// Package httputil is a toolkit for the Go net/http package.
8package httputil
9
10import (
11 "net"
12 "net/http"
13)
14
15// StripPort removes the port specification from an address.
16func StripPort(s string) string {
17 if h, _, err := net.SplitHostPort(s); err == nil {
18 s = h
19 }
20 return s
21}
22
23// Error defines a type for a function that accepts a ResponseWriter for
24// a Request with the HTTP status code and error.
25type Error func(w http.ResponseWriter, r *http.Request, status int, err error)
diff --git a/httputil/negotiate.go b/httputil/negotiate.go
new file mode 100644
index 0000000..6af3e4c
--- /dev/null
+++ b/httputil/negotiate.go
@@ -0,0 +1,79 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil
8
9import (
10 "github.com/golang/gddo/httputil/header"
11 "net/http"
12 "strings"
13)
14
15// NegotiateContentEncoding returns the best offered content encoding for the
16// request's Accept-Encoding header. If two offers match with equal weight and
17// then the offer earlier in the list is preferred. If no offers are
18// acceptable, then "" is returned.
19func NegotiateContentEncoding(r *http.Request, offers []string) string {
20 bestOffer := "identity"
21 bestQ := -1.0
22 specs := header.ParseAccept(r.Header, "Accept-Encoding")
23 for _, offer := range offers {
24 for _, spec := range specs {
25 if spec.Q > bestQ &&
26 (spec.Value == "*" || spec.Value == offer) {
27 bestQ = spec.Q
28 bestOffer = offer
29 }
30 }
31 }
32 if bestQ == 0 {
33 bestOffer = ""
34 }
35 return bestOffer
36}
37
38// NegotiateContentType returns the best offered content type for the request's
39// Accept header. If two offers match with equal weight, then the more specific
40// offer is preferred. For example, text/* trumps */*. If two offers match
41// with equal weight and specificity, then the offer earlier in the list is
42// preferred. If no offers match, then defaultOffer is returned.
43func NegotiateContentType(r *http.Request, offers []string, defaultOffer string) string {
44 bestOffer := defaultOffer
45 bestQ := -1.0
46 bestWild := 3
47 specs := header.ParseAccept(r.Header, "Accept")
48 for _, offer := range offers {
49 for _, spec := range specs {
50 switch {
51 case spec.Q == 0.0:
52 // ignore
53 case spec.Q < bestQ:
54 // better match found
55 case spec.Value == "*/*":
56 if spec.Q > bestQ || bestWild > 2 {
57 bestQ = spec.Q
58 bestWild = 2
59 bestOffer = offer
60 }
61 case strings.HasSuffix(spec.Value, "/*"):
62 if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) &&
63 (spec.Q > bestQ || bestWild > 1) {
64 bestQ = spec.Q
65 bestWild = 1
66 bestOffer = offer
67 }
68 default:
69 if spec.Value == offer &&
70 (spec.Q > bestQ || bestWild > 0) {
71 bestQ = spec.Q
72 bestWild = 0
73 bestOffer = offer
74 }
75 }
76 }
77 }
78 return bestOffer
79}
diff --git a/httputil/negotiate_test.go b/httputil/negotiate_test.go
new file mode 100644
index 0000000..24bf4be
--- /dev/null
+++ b/httputil/negotiate_test.go
@@ -0,0 +1,71 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil_test
8
9import (
10 "github.com/golang/gddo/httputil"
11 "net/http"
12 "testing"
13)
14
15var negotiateContentEncodingTests = []struct {
16 s string
17 offers []string
18 expect string
19}{
20 {"", []string{"identity", "gzip"}, "identity"},
21 {"*;q=0", []string{"identity", "gzip"}, ""},
22 {"gzip", []string{"identity", "gzip"}, "gzip"},
23}
24
25func TestNegotiateContentEnoding(t *testing.T) {
26 for _, tt := range negotiateContentEncodingTests {
27 r := &http.Request{Header: http.Header{"Accept-Encoding": {tt.s}}}
28 actual := httputil.NegotiateContentEncoding(r, tt.offers)
29 if actual != tt.expect {
30 t.Errorf("NegotiateContentEncoding(%q, %#v)=%q, want %q", tt.s, tt.offers, actual, tt.expect)
31 }
32 }
33}
34
35var negotiateContentTypeTests = []struct {
36 s string
37 offers []string
38 defaultOffer string
39 expect string
40}{
41 {"text/html, */*;q=0", []string{"x/y"}, "", ""},
42 {"text/html, */*", []string{"x/y"}, "", "x/y"},
43 {"text/html, image/png", []string{"text/html", "image/png"}, "", "text/html"},
44 {"text/html, image/png", []string{"image/png", "text/html"}, "", "image/png"},
45 {"text/html, image/png; q=0.5", []string{"image/png"}, "", "image/png"},
46 {"text/html, image/png; q=0.5", []string{"text/html"}, "", "text/html"},
47 {"text/html, image/png; q=0.5", []string{"foo/bar"}, "", ""},
48 {"text/html, image/png; q=0.5", []string{"image/png", "text/html"}, "", "text/html"},
49 {"text/html, image/png; q=0.5", []string{"text/html", "image/png"}, "", "text/html"},
50 {"text/html;q=0.5, image/png", []string{"image/png"}, "", "image/png"},
51 {"text/html;q=0.5, image/png", []string{"text/html"}, "", "text/html"},
52 {"text/html;q=0.5, image/png", []string{"image/png", "text/html"}, "", "image/png"},
53 {"text/html;q=0.5, image/png", []string{"text/html", "image/png"}, "", "image/png"},
54 {"image/png, image/*;q=0.5", []string{"image/jpg", "image/png"}, "", "image/png"},
55 {"image/png, image/*;q=0.5", []string{"image/jpg"}, "", "image/jpg"},
56 {"image/png, image/*;q=0.5", []string{"image/jpg", "image/gif"}, "", "image/jpg"},
57 {"image/png, image/*", []string{"image/jpg", "image/gif"}, "", "image/jpg"},
58 {"image/png, image/*", []string{"image/gif", "image/jpg"}, "", "image/gif"},
59 {"image/png, image/*", []string{"image/gif", "image/png"}, "", "image/png"},
60 {"image/png, image/*", []string{"image/png", "image/gif"}, "", "image/png"},
61}
62
63func TestNegotiateContentType(t *testing.T) {
64 for _, tt := range negotiateContentTypeTests {
65 r := &http.Request{Header: http.Header{"Accept": {tt.s}}}
66 actual := httputil.NegotiateContentType(r, tt.offers, tt.defaultOffer)
67 if actual != tt.expect {
68 t.Errorf("NegotiateContentType(%q, %#v, %q)=%q, want %q", tt.s, tt.offers, tt.defaultOffer, actual, tt.expect)
69 }
70 }
71}
diff --git a/httputil/respbuf.go b/httputil/respbuf.go
new file mode 100644
index 0000000..051af21
--- /dev/null
+++ b/httputil/respbuf.go
@@ -0,0 +1,58 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil
8
9import (
10 "bytes"
11 "net/http"
12 "strconv"
13)
14
15// ResponseBuffer is the current response being composed by its owner.
16// It implements http.ResponseWriter and io.WriterTo.
17type ResponseBuffer struct {
18 buf bytes.Buffer
19 status int
20 header http.Header
21}
22
23// Write implements the http.ResponseWriter interface.
24func (rb *ResponseBuffer) Write(p []byte) (int, error) {
25 return rb.buf.Write(p)
26}
27
28// WriteHeader implements the http.ResponseWriter interface.
29func (rb *ResponseBuffer) WriteHeader(status int) {
30 rb.status = status
31}
32
33// Header implements the http.ResponseWriter interface.
34func (rb *ResponseBuffer) Header() http.Header {
35 if rb.header == nil {
36 rb.header = make(http.Header)
37 }
38 return rb.header
39}
40
41// WriteTo implements the io.WriterTo interface.
42func (rb *ResponseBuffer) WriteTo(w http.ResponseWriter) error {
43 for k, v := range rb.header {
44 w.Header()[k] = v
45 }
46 if rb.buf.Len() > 0 {
47 w.Header().Set("Content-Length", strconv.Itoa(rb.buf.Len()))
48 }
49 if rb.status != 0 {
50 w.WriteHeader(rb.status)
51 }
52 if rb.buf.Len() > 0 {
53 if _, err := w.Write(rb.buf.Bytes()); err != nil {
54 return err
55 }
56 }
57 return nil
58}
diff --git a/httputil/static.go b/httputil/static.go
new file mode 100644
index 0000000..6610dde
--- /dev/null
+++ b/httputil/static.go
@@ -0,0 +1,265 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil
8
9import (
10 "bytes"
11 "crypto/sha1"
12 "errors"
13 "fmt"
14 "github.com/golang/gddo/httputil/header"
15 "io"
16 "io/ioutil"
17 "mime"
18 "net/http"
19 "os"
20 "path"
21 "path/filepath"
22 "strconv"
23 "strings"
24 "sync"
25 "time"
26)
27
28// StaticServer serves static files.
29type StaticServer struct {
30 // Dir specifies the location of the directory containing the files to serve.
31 Dir string
32
33 // MaxAge specifies the maximum age for the cache control and expiration
34 // headers.
35 MaxAge time.Duration
36
37 // Error specifies the function used to generate error responses. If Error
38 // is nil, then http.Error is used to generate error responses.
39 Error Error
40
41 // MIMETypes is a map from file extensions to MIME types.
42 MIMETypes map[string]string
43
44 mu sync.Mutex
45 etags map[string]string
46}
47
48func (ss *StaticServer) resolve(fname string) string {
49 if path.IsAbs(fname) {
50 panic("Absolute path not allowed when creating a StaticServer handler")
51 }
52 dir := ss.Dir
53 if dir == "" {
54 dir = "."
55 }
56 fname = filepath.FromSlash(fname)
57 return filepath.Join(dir, fname)
58}
59
60func (ss *StaticServer) mimeType(fname string) string {
61 ext := path.Ext(fname)
62 var mimeType string
63 if ss.MIMETypes != nil {
64 mimeType = ss.MIMETypes[ext]
65 }
66 if mimeType == "" {
67 mimeType = mime.TypeByExtension(ext)
68 }
69 if mimeType == "" {
70 mimeType = "application/octet-stream"
71 }
72 return mimeType
73}
74
75func (ss *StaticServer) openFile(fname string) (io.ReadCloser, int64, string, error) {
76 f, err := os.Open(fname)
77 if err != nil {
78 return nil, 0, "", err
79 }
80 fi, err := f.Stat()
81 if err != nil {
82 f.Close()
83 return nil, 0, "", err
84 }
85 const modeType = os.ModeDir | os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice
86 if fi.Mode()&modeType != 0 {
87 f.Close()
88 return nil, 0, "", errors.New("not a regular file")
89 }
90 return f, fi.Size(), ss.mimeType(fname), nil
91}
92
93// FileHandler returns a handler that serves a single file. The file is
94// specified by a slash separated path relative to the static server's Dir
95// field.
96func (ss *StaticServer) FileHandler(fileName string) http.Handler {
97 id := fileName
98 fileName = ss.resolve(fileName)
99 return &staticHandler{
100 ss: ss,
101 id: func(_ string) string { return id },
102 open: func(_ string) (io.ReadCloser, int64, string, error) { return ss.openFile(fileName) },
103 }
104}
105
106// DirectoryHandler returns a handler that serves files from a directory tree.
107// The directory is specified by a slash separated path relative to the static
108// server's Dir field.
109func (ss *StaticServer) DirectoryHandler(prefix, dirName string) http.Handler {
110 if !strings.HasSuffix(prefix, "/") {
111 prefix += "/"
112 }
113 idBase := dirName
114 dirName = ss.resolve(dirName)
115 return &staticHandler{
116 ss: ss,
117 id: func(p string) string {
118 if !strings.HasPrefix(p, prefix) {
119 return "."
120 }
121 return path.Join(idBase, p[len(prefix):])
122 },
123 open: func(p string) (io.ReadCloser, int64, string, error) {
124 if !strings.HasPrefix(p, prefix) {
125 return nil, 0, "", errors.New("request url does not match directory prefix")
126 }
127 p = p[len(prefix):]
128 return ss.openFile(filepath.Join(dirName, filepath.FromSlash(p)))
129 },
130 }
131}
132
133// FilesHandler returns a handler that serves the concatentation of the
134// specified files. The files are specified by slash separated paths relative
135// to the static server's Dir field.
136func (ss *StaticServer) FilesHandler(fileNames ...string) http.Handler {
137
138 // todo: cache concatenated files on disk and serve from there.
139
140 mimeType := ss.mimeType(fileNames[0])
141 var buf []byte
142 var openErr error
143
144 for _, fileName := range fileNames {
145 p, err := ioutil.ReadFile(ss.resolve(fileName))
146 if err != nil {
147 openErr = err
148 buf = nil
149 break
150 }
151 buf = append(buf, p...)
152 }
153
154 id := strings.Join(fileNames, " ")
155
156 return &staticHandler{
157 ss: ss,
158 id: func(_ string) string { return id },
159 open: func(p string) (io.ReadCloser, int64, string, error) {
160 return ioutil.NopCloser(bytes.NewReader(buf)), int64(len(buf)), mimeType, openErr
161 },
162 }
163}
164
165type staticHandler struct {
166 id func(fname string) string
167 open func(p string) (io.ReadCloser, int64, string, error)
168 ss *StaticServer
169}
170
171func (h *staticHandler) error(w http.ResponseWriter, r *http.Request, status int, err error) {
172 http.Error(w, http.StatusText(status), status)
173}
174
175func (h *staticHandler) etag(p string) (string, error) {
176 id := h.id(p)
177
178 h.ss.mu.Lock()
179 if h.ss.etags == nil {
180 h.ss.etags = make(map[string]string)
181 }
182 etag := h.ss.etags[id]
183 h.ss.mu.Unlock()
184
185 if etag != "" {
186 return etag, nil
187 }
188
189 // todo: if a concurrent goroutine is calculating the hash, then wait for
190 // it instead of computing it again here.
191
192 rc, _, _, err := h.open(p)
193 if err != nil {
194 return "", err
195 }
196
197 defer rc.Close()
198
199 w := sha1.New()
200 _, err = io.Copy(w, rc)
201 if err != nil {
202 return "", err
203 }
204
205 etag = fmt.Sprintf(`"%x"`, w.Sum(nil))
206
207 h.ss.mu.Lock()
208 h.ss.etags[id] = etag
209 h.ss.mu.Unlock()
210
211 return etag, nil
212}
213
214func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
215 p := path.Clean(r.URL.Path)
216 if p != r.URL.Path {
217 http.Redirect(w, r, p, 301)
218 return
219 }
220
221 etag, err := h.etag(p)
222 if err != nil {
223 h.error(w, r, http.StatusNotFound, err)
224 return
225 }
226
227 maxAge := h.ss.MaxAge
228 if maxAge == 0 {
229 maxAge = 24 * time.Hour
230 }
231 if r.FormValue("v") != "" {
232 maxAge = 365 * 24 * time.Hour
233 }
234
235 cacheControl := fmt.Sprintf("public, max-age=%d", maxAge/time.Second)
236
237 for _, e := range header.ParseList(r.Header, "If-None-Match") {
238 if e == etag {
239 w.Header().Set("Cache-Control", cacheControl)
240 w.Header().Set("Etag", etag)
241 w.WriteHeader(http.StatusNotModified)
242 return
243 }
244 }
245
246 rc, cl, ct, err := h.open(p)
247 if err != nil {
248 h.error(w, r, http.StatusNotFound, err)
249 return
250 }
251 defer rc.Close()
252
253 w.Header().Set("Cache-Control", cacheControl)
254 w.Header().Set("Etag", etag)
255 if ct != "" {
256 w.Header().Set("Content-Type", ct)
257 }
258 if cl != 0 {
259 w.Header().Set("Content-Length", strconv.FormatInt(cl, 10))
260 }
261 w.WriteHeader(http.StatusOK)
262 if r.Method != "HEAD" {
263 io.Copy(w, rc)
264 }
265}
diff --git a/httputil/static_test.go b/httputil/static_test.go
new file mode 100644
index 0000000..a258d52
--- /dev/null
+++ b/httputil/static_test.go
@@ -0,0 +1,175 @@
1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil_test
8
9import (
10 "crypto/sha1"
11 "encoding/hex"
12 "io/ioutil"
13 "net/http"
14 "net/http/httptest"
15 "net/url"
16 "os"
17 "strconv"
18 "testing"
19 "time"
20
21 "github.com/golang/gddo/httputil"
22 "github.com/google/go-cmp/cmp"
23)
24
25var (
26 testHash = computeTestHash()
27 testEtag = `"` + testHash + `"`
28 testContentLength = computeTestContentLength()
29)
30
31func mustParseURL(urlStr string) *url.URL {
32 u, err := url.Parse(urlStr)
33 if err != nil {
34 panic(err)
35 }
36 return u
37}
38
39func computeTestHash() string {
40 p, err := ioutil.ReadFile("static_test.go")
41 if err != nil {
42 panic(err)
43 }
44 w := sha1.New()
45 w.Write(p)
46 return hex.EncodeToString(w.Sum(nil))
47}
48
49func computeTestContentLength() string {
50 info, err := os.Stat("static_test.go")
51 if err != nil {
52 panic(err)
53 }
54 return strconv.FormatInt(info.Size(), 10)
55}
56
57var fileServerTests = []*struct {
58 name string // test name for log
59 ss *httputil.StaticServer
60 r *http.Request
61 header http.Header // expected response headers
62 status int // expected response status
63 empty bool // true if response body not expected.
64}{
65 {
66 name: "get",
67 ss: &httputil.StaticServer{MaxAge: 3 * time.Second},
68 r: &http.Request{
69 URL: mustParseURL("/dir/static_test.go"),
70 Method: "GET",
71 },
72 status: http.StatusOK,
73 header: http.Header{
74 "Etag": {testEtag},
75 "Cache-Control": {"public, max-age=3"},
76 "Content-Length": {testContentLength},
77 "Content-Type": {"application/octet-stream"},
78 },
79 },
80 {
81 name: "get .",
82 ss: &httputil.StaticServer{Dir: ".", MaxAge: 3 * time.Second},
83 r: &http.Request{
84 URL: mustParseURL("/dir/static_test.go"),
85 Method: "GET",
86 },
87 status: http.StatusOK,
88 header: http.Header{
89 "Etag": {testEtag},
90 "Cache-Control": {"public, max-age=3"},
91 "Content-Length": {testContentLength},
92 "Content-Type": {"application/octet-stream"},
93 },
94 },
95 {
96 name: "get with ?v=",
97 ss: &httputil.StaticServer{MaxAge: 3 * time.Second},
98 r: &http.Request{
99 URL: mustParseURL("/dir/static_test.go?v=xxxxx"),
100 Method: "GET",
101 },
102 status: http.StatusOK,
103 header: http.Header{
104 "Etag": {testEtag},
105 "Cache-Control": {"public, max-age=31536000"},
106 "Content-Length": {testContentLength},
107 "Content-Type": {"application/octet-stream"},
108 },
109 },
110 {
111 name: "head",
112 ss: &httputil.StaticServer{MaxAge: 3 * time.Second},
113 r: &http.Request{
114 URL: mustParseURL("/dir/static_test.go"),
115 Method: "HEAD",
116 },
117 status: http.StatusOK,
118 header: http.Header{
119 "Etag": {testEtag},
120 "Cache-Control": {"public, max-age=3"},
121 "Content-Length": {testContentLength},
122 "Content-Type": {"application/octet-stream"},
123 },
124 empty: true,
125 },
126 {
127 name: "if-none-match",
128 ss: &httputil.StaticServer{MaxAge: 3 * time.Second},
129 r: &http.Request{
130 URL: mustParseURL("/dir/static_test.go"),
131 Method: "GET",
132 Header: http.Header{"If-None-Match": {testEtag}},
133 },
134 status: http.StatusNotModified,
135 header: http.Header{
136 "Cache-Control": {"public, max-age=3"},
137 "Etag": {testEtag},
138 },
139 empty: true,
140 },
141}
142
143func testStaticServer(t *testing.T, f func(*httputil.StaticServer) http.Handler) {
144 for _, tt := range fileServerTests {
145 w := httptest.NewRecorder()
146
147 h := f(tt.ss)
148 h.ServeHTTP(w, tt.r)
149
150 if w.Code != tt.status {
151 t.Errorf("%s, status=%d, want %d", tt.name, w.Code, tt.status)
152 }
153
154 if !cmp.Equal(w.HeaderMap, tt.header) {
155 t.Errorf("%s\n\theader=%v,\n\twant %v", tt.name, w.HeaderMap, tt.header)
156 }
157
158 empty := w.Body.Len() == 0
159 if empty != tt.empty {
160 t.Errorf("%s empty=%v, want %v", tt.name, empty, tt.empty)
161 }
162 }
163}
164
165func TestFileHandler(t *testing.T) {
166 testStaticServer(t, func(ss *httputil.StaticServer) http.Handler { return ss.FileHandler("static_test.go") })
167}
168
169func TestDirectoryHandler(t *testing.T) {
170 testStaticServer(t, func(ss *httputil.StaticServer) http.Handler { return ss.DirectoryHandler("/dir", ".") })
171}
172
173func TestFilesHandler(t *testing.T) {
174 testStaticServer(t, func(ss *httputil.StaticServer) http.Handler { return ss.FilesHandler("static_test.go") })
175}
diff --git a/httputil/transport.go b/httputil/transport.go
new file mode 100644
index 0000000..fdad3b4
--- /dev/null
+++ b/httputil/transport.go
@@ -0,0 +1,87 @@
1// Copyright 2015 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7// This file implements a http.RoundTripper that authenticates
8// requests issued against api.github.com endpoint.
9
10package httputil
11
12import (
13 "net/http"
14 "net/url"
15)
16
17// AuthTransport is an implementation of http.RoundTripper that authenticates
18// with the GitHub API.
19//
20// When both a token and client credentials are set, the latter is preferred.
21type AuthTransport struct {
22 UserAgent string
23 GithubToken string
24 GithubClientID string
25 GithubClientSecret string
26 Base http.RoundTripper
27}
28
29// RoundTrip implements the http.RoundTripper interface.
30func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
31 var reqCopy *http.Request
32 if t.UserAgent != "" {
33 reqCopy = copyRequest(req)
34 reqCopy.Header.Set("User-Agent", t.UserAgent)
35 }
36 if req.URL.Host == "api.github.com" && req.URL.Scheme == "https" {
37 switch {
38 case t.GithubClientID != "" && t.GithubClientSecret != "":
39 if reqCopy == nil {
40 reqCopy = copyRequest(req)
41 }
42 if reqCopy.URL.RawQuery == "" {
43 reqCopy.URL.RawQuery = "client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
44 } else {
45 reqCopy.URL.RawQuery += "&client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
46 }
47 case t.GithubToken != "":
48 if reqCopy == nil {
49 reqCopy = copyRequest(req)
50 }
51 reqCopy.Header.Set("Authorization", "token "+t.GithubToken)
52 }
53 }
54 if reqCopy != nil {
55 return t.base().RoundTrip(reqCopy)
56 }
57 return t.base().RoundTrip(req)
58}
59
60// CancelRequest cancels an in-flight request by closing its connection.
61func (t *AuthTransport) CancelRequest(req *http.Request) {
62 type canceler interface {
63 CancelRequest(req *http.Request)
64 }
65 if cr, ok := t.base().(canceler); ok {
66 cr.CancelRequest(req)
67 }
68}
69
70func (t *AuthTransport) base() http.RoundTripper {
71 if t.Base != nil {
72 return t.Base
73 }
74 return http.DefaultTransport
75}
76
77func copyRequest(req *http.Request) *http.Request {
78 req2 := new(http.Request)
79 *req2 = *req
80 req2.URL = new(url.URL)
81 *req2.URL = *req.URL
82 req2.Header = make(http.Header, len(req.Header))
83 for k, s := range req.Header {
84 req2.Header[k] = append([]string(nil), s...)
85 }
86 return req2
87}
diff --git a/httputil/transport_test.go b/httputil/transport_test.go
new file mode 100644
index 0000000..4b36118
--- /dev/null
+++ b/httputil/transport_test.go
@@ -0,0 +1,126 @@
1// Copyright 2017 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7package httputil
8
9import (
10 "bytes"
11 "io/ioutil"
12 "net/http"
13 "net/url"
14 "testing"
15)
16
17func TestTransportGithubAuth(t *testing.T) {
18 tests := []struct {
19 name string
20
21 url string
22 token string
23 clientID string
24 clientSecret string
25
26 queryClientID string
27 queryClientSecret string
28 authorization string
29 }{
30 {
31 name: "Github token",
32 url: "https://api.github.com/",
33 token: "xyzzy",
34 authorization: "token xyzzy",
35 },
36 {
37 name: "Github client ID/secret",
38 url: "https://api.github.com/",
39 clientID: "12345",
40 clientSecret: "xyzzy",
41 queryClientID: "12345",
42 queryClientSecret: "xyzzy",
43 },
44 {
45 name: "non-Github site does not have token headers",
46 url: "http://www.example.com/",
47 token: "xyzzy",
48 },
49 {
50 name: "non-Github site does not have client ID/secret headers",
51 url: "http://www.example.com/",
52 clientID: "12345",
53 clientSecret: "xyzzy",
54 },
55 {
56 name: "Github token not sent over HTTP",
57 url: "http://api.github.com/",
58 token: "xyzzy",
59 },
60 {
61 name: "Github client ID/secret not sent over HTTP",
62 url: "http://api.github.com/",
63 clientID: "12345",
64 clientSecret: "xyzzy",
65 },
66 {
67 name: "Github token not sent over schemeless",
68 url: "//api.github.com/",
69 token: "xyzzy",
70 },
71 {
72 name: "Github client ID/secret not sent over schemeless",
73 url: "//api.github.com/",
74 clientID: "12345",
75 clientSecret: "xyzzy",
76 },
77 }
78 for _, test := range tests {
79 t.Run(test.name, func(t *testing.T) {
80 var (
81 query url.Values
82 authHeader string
83 )
84 client := &http.Client{
85 Transport: &AuthTransport{
86 Base: roundTripFunc(func(r *http.Request) {
87 query = r.URL.Query()
88 authHeader = r.Header.Get("Authorization")
89 }),
90 GithubToken: test.token,
91 GithubClientID: test.clientID,
92 GithubClientSecret: test.clientSecret,
93 },
94 }
95 _, err := client.Get(test.url)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if got := query.Get("client_id"); got != test.queryClientID {
100 t.Errorf("url query client_id = %q; want %q", got, test.queryClientID)
101 }
102 if got := query.Get("client_secret"); got != test.queryClientSecret {
103 t.Errorf("url query client_secret = %q; want %q", got, test.queryClientSecret)
104 }
105 if authHeader != test.authorization {
106 t.Errorf("header Authorization = %q; want %q", authHeader, test.authorization)
107 }
108 })
109 }
110}
111
112type roundTripFunc func(r *http.Request)
113
114func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
115 f(r)
116 return &http.Response{
117 Status: "200 OK",
118 StatusCode: http.StatusOK,
119 Proto: "HTTP/1.1",
120 ProtoMajor: 1,
121 ProtoMinor: 1,
122 Body: ioutil.NopCloser(bytes.NewReader(nil)),
123 ContentLength: 0,
124 Request: r,
125 }, nil
126}