Browse code

Content encoding negotiation added to archive request.

Signed-off-by: Emil Davtyan <emil2k@gmail.com>

Emil Davtyan authored on 2018/01/31 21:39:48
Showing 11 changed files
... ...
@@ -1,6 +1,8 @@
1 1
 package container // import "github.com/docker/docker/api/server/router/container"
2 2
 
3 3
 import (
4
+	"compress/flate"
5
+	"compress/gzip"
4 6
 	"encoding/base64"
5 7
 	"encoding/json"
6 8
 	"io"
... ...
@@ -9,6 +11,7 @@ import (
9 9
 	"github.com/docker/docker/api/server/httputils"
10 10
 	"github.com/docker/docker/api/types"
11 11
 	"github.com/docker/docker/api/types/versions"
12
+	gddohttputil "github.com/golang/gddo/httputil"
12 13
 	"golang.org/x/net/context"
13 14
 )
14 15
 
... ...
@@ -81,6 +84,29 @@ func (s *containerRouter) headContainersArchive(ctx context.Context, w http.Resp
81 81
 	return setContainerPathStatHeader(stat, w.Header())
82 82
 }
83 83
 
84
+func writeCompressedResponse(w http.ResponseWriter, r *http.Request, body io.Reader) error {
85
+	var cw io.Writer
86
+	switch gddohttputil.NegotiateContentEncoding(r, []string{"gzip", "deflate"}) {
87
+	case "gzip":
88
+		gw := gzip.NewWriter(w)
89
+		defer gw.Close()
90
+		cw = gw
91
+		w.Header().Set("Content-Encoding", "gzip")
92
+	case "deflate":
93
+		fw, err := flate.NewWriter(w, flate.DefaultCompression)
94
+		if err != nil {
95
+			return err
96
+		}
97
+		defer fw.Close()
98
+		cw = fw
99
+		w.Header().Set("Content-Encoding", "deflate")
100
+	default:
101
+		cw = w
102
+	}
103
+	_, err := io.Copy(cw, body)
104
+	return err
105
+}
106
+
84 107
 func (s *containerRouter) getContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
85 108
 	v, err := httputils.ArchiveFormValues(r, vars)
86 109
 	if err != nil {
... ...
@@ -98,9 +124,7 @@ func (s *containerRouter) getContainersArchive(ctx context.Context, w http.Respo
98 98
 	}
99 99
 
100 100
 	w.Header().Set("Content-Type", "application/x-tar")
101
-	_, err = io.Copy(w, tarArchive)
102
-
103
-	return err
101
+	return writeCompressedResponse(w, r, tarArchive)
104 102
 }
105 103
 
106 104
 func (s *containerRouter) putContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
... ...
@@ -5,6 +5,7 @@ github.com/Microsoft/go-winio v0.4.6
5 5
 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
6 6
 github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a
7 7
 github.com/go-check/check 4ed411733c5785b40214c70bce814c3a3a689609 https://github.com/cpuguy83/check.git
8
+github.com/golang/gddo 9b12a26f3fbd7397dee4e20939ddca719d840d2a
8 9
 github.com/gorilla/context v1.1
9 10
 github.com/gorilla/mux v1.1
10 11
 github.com/Microsoft/opengcs v0.3.6
11 12
new file mode 100644
... ...
@@ -0,0 +1,27 @@
0
+Copyright (c) 2013 The Go Authors. All rights reserved.
1
+
2
+Redistribution and use in source and binary forms, with or without
3
+modification, are permitted provided that the following conditions are
4
+met:
5
+
6
+   * Redistributions of source code must retain the above copyright
7
+notice, this list of conditions and the following disclaimer.
8
+   * Redistributions in binary form must reproduce the above
9
+copyright notice, this list of conditions and the following disclaimer
10
+in the documentation and/or other materials provided with the
11
+distribution.
12
+   * Neither the name of Google Inc. nor the names of its
13
+contributors may be used to endorse or promote products derived from
14
+this software without specific prior written permission.
15
+
16
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 27
new file mode 100644
... ...
@@ -0,0 +1,44 @@
0
+This project is the source for http://godoc.org/
1
+
2
+[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](http://godoc.org/github.com/golang/gddo)
3
+[![Build
4
+Status](https://travis-ci.org/golang/gddo.svg?branch=master)](https://travis-ci.org/golang/gddo)
5
+
6
+The code in this project is designed to be used by godoc.org. Send mail to
7
+golang-dev@googlegroups.com if you want to discuss other uses of the code.
8
+
9
+## Feedback
10
+
11
+Send ideas and questions to golang-dev@googlegroups.com. Request features and
12
+report bugs using the [GitHub Issue
13
+Tracker](https://github.com/golang/gddo/issues/new).
14
+
15
+## Contributions
16
+
17
+Contributions to this project are welcome, though please [file an
18
+issue](https://github.com/golang/gddo/issues/new). before starting work on
19
+anything major.
20
+
21
+**We do not accept GitHub pull requests**
22
+
23
+Please refer to the [Contribution
24
+Guidelines](https://golang.org/doc/contribute.html) on how to submit changes.
25
+
26
+We use https://go-review.googlesource.com to review change submissions.
27
+
28
+## Getting the Source
29
+
30
+To get started contributing to this project, clone the repository from its
31
+canonical location
32
+
33
+```
34
+git clone https://go.googlesource.com/gddo $GOPATH/src/github.com/golang/gddo
35
+```
36
+
37
+Information on how to set up a local environment is available at
38
+https://github.com/golang/gddo/wiki/Development-Environment-Setup.
39
+
40
+## More Documentation
41
+
42
+More documentation about this project is available on the
43
+[wiki](https://github.com/golang/gddo/wiki).
0 44
new file mode 100644
... ...
@@ -0,0 +1,95 @@
0
+// Copyright 2013 The Go Authors. All rights reserved.
1
+//
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file or at
4
+// https://developers.google.com/open-source/licenses/bsd.
5
+
6
+package httputil
7
+
8
+import (
9
+	"io"
10
+	"io/ioutil"
11
+	"net/http"
12
+	"net/url"
13
+	"strings"
14
+	"sync"
15
+)
16
+
17
+type busterWriter struct {
18
+	headerMap http.Header
19
+	status    int
20
+	io.Writer
21
+}
22
+
23
+func (bw *busterWriter) Header() http.Header {
24
+	return bw.headerMap
25
+}
26
+
27
+func (bw *busterWriter) WriteHeader(status int) {
28
+	bw.status = status
29
+}
30
+
31
+// CacheBusters maintains a cache of cache busting tokens for static resources served by Handler.
32
+type CacheBusters struct {
33
+	Handler http.Handler
34
+
35
+	mu     sync.Mutex
36
+	tokens map[string]string
37
+}
38
+
39
+func sanitizeTokenRune(r rune) rune {
40
+	if r <= ' ' || r >= 127 {
41
+		return -1
42
+	}
43
+	// Convert percent encoding reserved characters to '-'.
44
+	if strings.ContainsRune("!#$&'()*+,/:;=?@[]", r) {
45
+		return '-'
46
+	}
47
+	return r
48
+}
49
+
50
+// Get returns the cache busting token for path. If the token is not already
51
+// cached, Get issues a HEAD request on handler and uses the response ETag and
52
+// Last-Modified headers to compute a token.
53
+func (cb *CacheBusters) Get(path string) string {
54
+	cb.mu.Lock()
55
+	if cb.tokens == nil {
56
+		cb.tokens = make(map[string]string)
57
+	}
58
+	token, ok := cb.tokens[path]
59
+	cb.mu.Unlock()
60
+	if ok {
61
+		return token
62
+	}
63
+
64
+	w := busterWriter{
65
+		Writer:    ioutil.Discard,
66
+		headerMap: make(http.Header),
67
+	}
68
+	r := &http.Request{URL: &url.URL{Path: path}, Method: "HEAD"}
69
+	cb.Handler.ServeHTTP(&w, r)
70
+
71
+	if w.status == 200 {
72
+		token = w.headerMap.Get("Etag")
73
+		if token == "" {
74
+			token = w.headerMap.Get("Last-Modified")
75
+		}
76
+		token = strings.Trim(token, `" `)
77
+		token = strings.Map(sanitizeTokenRune, token)
78
+	}
79
+
80
+	cb.mu.Lock()
81
+	cb.tokens[path] = token
82
+	cb.mu.Unlock()
83
+
84
+	return token
85
+}
86
+
87
+// AppendQueryParam appends the token as a query parameter to path.
88
+func (cb *CacheBusters) AppendQueryParam(path string, name string) string {
89
+	token := cb.Get(path)
90
+	if token == "" {
91
+		return path
92
+	}
93
+	return path + "?" + name + "=" + token
94
+}
0 95
new file mode 100644
... ...
@@ -0,0 +1,298 @@
0
+// Copyright 2013 The Go Authors. All rights reserved.
1
+//
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file or at
4
+// https://developers.google.com/open-source/licenses/bsd.
5
+
6
+// Package header provides functions for parsing HTTP headers.
7
+package header
8
+
9
+import (
10
+	"net/http"
11
+	"strings"
12
+	"time"
13
+)
14
+
15
+// Octet types from RFC 2616.
16
+var octetTypes [256]octetType
17
+
18
+type octetType byte
19
+
20
+const (
21
+	isToken octetType = 1 << iota
22
+	isSpace
23
+)
24
+
25
+func init() {
26
+	// OCTET      = <any 8-bit sequence of data>
27
+	// CHAR       = <any US-ASCII character (octets 0 - 127)>
28
+	// CTL        = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
29
+	// CR         = <US-ASCII CR, carriage return (13)>
30
+	// LF         = <US-ASCII LF, linefeed (10)>
31
+	// SP         = <US-ASCII SP, space (32)>
32
+	// HT         = <US-ASCII HT, horizontal-tab (9)>
33
+	// <">        = <US-ASCII double-quote mark (34)>
34
+	// CRLF       = CR LF
35
+	// LWS        = [CRLF] 1*( SP | HT )
36
+	// TEXT       = <any OCTET except CTLs, but including LWS>
37
+	// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
38
+	//              | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
39
+	// token      = 1*<any CHAR except CTLs or separators>
40
+	// qdtext     = <any TEXT except <">>
41
+
42
+	for c := 0; c < 256; c++ {
43
+		var t octetType
44
+		isCtl := c <= 31 || c == 127
45
+		isChar := 0 <= c && c <= 127
46
+		isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
47
+		if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
48
+			t |= isSpace
49
+		}
50
+		if isChar && !isCtl && !isSeparator {
51
+			t |= isToken
52
+		}
53
+		octetTypes[c] = t
54
+	}
55
+}
56
+
57
+// Copy returns a shallow copy of the header.
58
+func Copy(header http.Header) http.Header {
59
+	h := make(http.Header)
60
+	for k, vs := range header {
61
+		h[k] = vs
62
+	}
63
+	return h
64
+}
65
+
66
+var timeLayouts = []string{"Mon, 02 Jan 2006 15:04:05 GMT", time.RFC850, time.ANSIC}
67
+
68
+// ParseTime parses the header as time. The zero value is returned if the
69
+// header is not present or there is an error parsing the
70
+// header.
71
+func ParseTime(header http.Header, key string) time.Time {
72
+	if s := header.Get(key); s != "" {
73
+		for _, layout := range timeLayouts {
74
+			if t, err := time.Parse(layout, s); err == nil {
75
+				return t.UTC()
76
+			}
77
+		}
78
+	}
79
+	return time.Time{}
80
+}
81
+
82
+// ParseList parses a comma separated list of values. Commas are ignored in
83
+// quoted strings. Quoted values are not unescaped or unquoted. Whitespace is
84
+// trimmed.
85
+func ParseList(header http.Header, key string) []string {
86
+	var result []string
87
+	for _, s := range header[http.CanonicalHeaderKey(key)] {
88
+		begin := 0
89
+		end := 0
90
+		escape := false
91
+		quote := false
92
+		for i := 0; i < len(s); i++ {
93
+			b := s[i]
94
+			switch {
95
+			case escape:
96
+				escape = false
97
+				end = i + 1
98
+			case quote:
99
+				switch b {
100
+				case '\\':
101
+					escape = true
102
+				case '"':
103
+					quote = false
104
+				}
105
+				end = i + 1
106
+			case b == '"':
107
+				quote = true
108
+				end = i + 1
109
+			case octetTypes[b]&isSpace != 0:
110
+				if begin == end {
111
+					begin = i + 1
112
+					end = begin
113
+				}
114
+			case b == ',':
115
+				if begin < end {
116
+					result = append(result, s[begin:end])
117
+				}
118
+				begin = i + 1
119
+				end = begin
120
+			default:
121
+				end = i + 1
122
+			}
123
+		}
124
+		if begin < end {
125
+			result = append(result, s[begin:end])
126
+		}
127
+	}
128
+	return result
129
+}
130
+
131
+// ParseValueAndParams parses a comma separated list of values with optional
132
+// semicolon separated name-value pairs. Content-Type and Content-Disposition
133
+// headers are in this format.
134
+func ParseValueAndParams(header http.Header, key string) (value string, params map[string]string) {
135
+	params = make(map[string]string)
136
+	s := header.Get(key)
137
+	value, s = expectTokenSlash(s)
138
+	if value == "" {
139
+		return
140
+	}
141
+	value = strings.ToLower(value)
142
+	s = skipSpace(s)
143
+	for strings.HasPrefix(s, ";") {
144
+		var pkey string
145
+		pkey, s = expectToken(skipSpace(s[1:]))
146
+		if pkey == "" {
147
+			return
148
+		}
149
+		if !strings.HasPrefix(s, "=") {
150
+			return
151
+		}
152
+		var pvalue string
153
+		pvalue, s = expectTokenOrQuoted(s[1:])
154
+		if pvalue == "" {
155
+			return
156
+		}
157
+		pkey = strings.ToLower(pkey)
158
+		params[pkey] = pvalue
159
+		s = skipSpace(s)
160
+	}
161
+	return
162
+}
163
+
164
+// AcceptSpec describes an Accept* header.
165
+type AcceptSpec struct {
166
+	Value string
167
+	Q     float64
168
+}
169
+
170
+// ParseAccept parses Accept* headers.
171
+func ParseAccept(header http.Header, key string) (specs []AcceptSpec) {
172
+loop:
173
+	for _, s := range header[key] {
174
+		for {
175
+			var spec AcceptSpec
176
+			spec.Value, s = expectTokenSlash(s)
177
+			if spec.Value == "" {
178
+				continue loop
179
+			}
180
+			spec.Q = 1.0
181
+			s = skipSpace(s)
182
+			if strings.HasPrefix(s, ";") {
183
+				s = skipSpace(s[1:])
184
+				if !strings.HasPrefix(s, "q=") {
185
+					continue loop
186
+				}
187
+				spec.Q, s = expectQuality(s[2:])
188
+				if spec.Q < 0.0 {
189
+					continue loop
190
+				}
191
+			}
192
+			specs = append(specs, spec)
193
+			s = skipSpace(s)
194
+			if !strings.HasPrefix(s, ",") {
195
+				continue loop
196
+			}
197
+			s = skipSpace(s[1:])
198
+		}
199
+	}
200
+	return
201
+}
202
+
203
+func skipSpace(s string) (rest string) {
204
+	i := 0
205
+	for ; i < len(s); i++ {
206
+		if octetTypes[s[i]]&isSpace == 0 {
207
+			break
208
+		}
209
+	}
210
+	return s[i:]
211
+}
212
+
213
+func expectToken(s string) (token, rest string) {
214
+	i := 0
215
+	for ; i < len(s); i++ {
216
+		if octetTypes[s[i]]&isToken == 0 {
217
+			break
218
+		}
219
+	}
220
+	return s[:i], s[i:]
221
+}
222
+
223
+func expectTokenSlash(s string) (token, rest string) {
224
+	i := 0
225
+	for ; i < len(s); i++ {
226
+		b := s[i]
227
+		if (octetTypes[b]&isToken == 0) && b != '/' {
228
+			break
229
+		}
230
+	}
231
+	return s[:i], s[i:]
232
+}
233
+
234
+func expectQuality(s string) (q float64, rest string) {
235
+	switch {
236
+	case len(s) == 0:
237
+		return -1, ""
238
+	case s[0] == '0':
239
+		q = 0
240
+	case s[0] == '1':
241
+		q = 1
242
+	default:
243
+		return -1, ""
244
+	}
245
+	s = s[1:]
246
+	if !strings.HasPrefix(s, ".") {
247
+		return q, s
248
+	}
249
+	s = s[1:]
250
+	i := 0
251
+	n := 0
252
+	d := 1
253
+	for ; i < len(s); i++ {
254
+		b := s[i]
255
+		if b < '0' || b > '9' {
256
+			break
257
+		}
258
+		n = n*10 + int(b) - '0'
259
+		d *= 10
260
+	}
261
+	return q + float64(n)/float64(d), s[i:]
262
+}
263
+
264
+func expectTokenOrQuoted(s string) (value string, rest string) {
265
+	if !strings.HasPrefix(s, "\"") {
266
+		return expectToken(s)
267
+	}
268
+	s = s[1:]
269
+	for i := 0; i < len(s); i++ {
270
+		switch s[i] {
271
+		case '"':
272
+			return s[:i], s[i+1:]
273
+		case '\\':
274
+			p := make([]byte, len(s)-1)
275
+			j := copy(p, s[:i])
276
+			escape := true
277
+			for i = i + 1; i < len(s); i++ {
278
+				b := s[i]
279
+				switch {
280
+				case escape:
281
+					escape = false
282
+					p[j] = b
283
+					j++
284
+				case b == '\\':
285
+					escape = true
286
+				case b == '"':
287
+					return string(p[:j]), s[i+1:]
288
+				default:
289
+					p[j] = b
290
+					j++
291
+				}
292
+			}
293
+			return "", ""
294
+		}
295
+	}
296
+	return "", ""
297
+}
0 298
new file mode 100644
... ...
@@ -0,0 +1,25 @@
0
+// Copyright 2013 The Go Authors. All rights reserved.
1
+//
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file or at
4
+// https://developers.google.com/open-source/licenses/bsd.
5
+
6
+// Package httputil is a toolkit for the Go net/http package.
7
+package httputil
8
+
9
+import (
10
+	"net"
11
+	"net/http"
12
+)
13
+
14
+// StripPort removes the port specification from an address.
15
+func StripPort(s string) string {
16
+	if h, _, err := net.SplitHostPort(s); err == nil {
17
+		s = h
18
+	}
19
+	return s
20
+}
21
+
22
+// Error defines a type for a function that accepts a ResponseWriter for
23
+// a Request with the HTTP status code and error.
24
+type Error func(w http.ResponseWriter, r *http.Request, status int, err error)
0 25
new file mode 100644
... ...
@@ -0,0 +1,79 @@
0
+// Copyright 2013 The Go Authors. All rights reserved.
1
+//
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file or at
4
+// https://developers.google.com/open-source/licenses/bsd.
5
+
6
+package httputil
7
+
8
+import (
9
+	"github.com/golang/gddo/httputil/header"
10
+	"net/http"
11
+	"strings"
12
+)
13
+
14
+// NegotiateContentEncoding returns the best offered content encoding for the
15
+// request's Accept-Encoding header. If two offers match with equal weight and
16
+// then the offer earlier in the list is preferred. If no offers are
17
+// acceptable, then "" is returned.
18
+func NegotiateContentEncoding(r *http.Request, offers []string) string {
19
+	bestOffer := "identity"
20
+	bestQ := -1.0
21
+	specs := header.ParseAccept(r.Header, "Accept-Encoding")
22
+	for _, offer := range offers {
23
+		for _, spec := range specs {
24
+			if spec.Q > bestQ &&
25
+				(spec.Value == "*" || spec.Value == offer) {
26
+				bestQ = spec.Q
27
+				bestOffer = offer
28
+			}
29
+		}
30
+	}
31
+	if bestQ == 0 {
32
+		bestOffer = ""
33
+	}
34
+	return bestOffer
35
+}
36
+
37
+// NegotiateContentType returns the best offered content type for the request's
38
+// Accept header. If two offers match with equal weight, then the more specific
39
+// offer is preferred.  For example, text/* trumps */*. If two offers match
40
+// with equal weight and specificity, then the offer earlier in the list is
41
+// preferred. If no offers match, then defaultOffer is returned.
42
+func NegotiateContentType(r *http.Request, offers []string, defaultOffer string) string {
43
+	bestOffer := defaultOffer
44
+	bestQ := -1.0
45
+	bestWild := 3
46
+	specs := header.ParseAccept(r.Header, "Accept")
47
+	for _, offer := range offers {
48
+		for _, spec := range specs {
49
+			switch {
50
+			case spec.Q == 0.0:
51
+				// ignore
52
+			case spec.Q < bestQ:
53
+				// better match found
54
+			case spec.Value == "*/*":
55
+				if spec.Q > bestQ || bestWild > 2 {
56
+					bestQ = spec.Q
57
+					bestWild = 2
58
+					bestOffer = offer
59
+				}
60
+			case strings.HasSuffix(spec.Value, "/*"):
61
+				if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) &&
62
+					(spec.Q > bestQ || bestWild > 1) {
63
+					bestQ = spec.Q
64
+					bestWild = 1
65
+					bestOffer = offer
66
+				}
67
+			default:
68
+				if spec.Value == offer &&
69
+					(spec.Q > bestQ || bestWild > 0) {
70
+					bestQ = spec.Q
71
+					bestWild = 0
72
+					bestOffer = offer
73
+				}
74
+			}
75
+		}
76
+	}
77
+	return bestOffer
78
+}
0 79
new file mode 100644
... ...
@@ -0,0 +1,58 @@
0
+// Copyright 2013 The Go Authors. All rights reserved.
1
+//
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file or at
4
+// https://developers.google.com/open-source/licenses/bsd.
5
+
6
+package httputil
7
+
8
+import (
9
+	"bytes"
10
+	"net/http"
11
+	"strconv"
12
+)
13
+
14
+// ResponseBuffer is the current response being composed by its owner.
15
+// It implements http.ResponseWriter and io.WriterTo.
16
+type ResponseBuffer struct {
17
+	buf    bytes.Buffer
18
+	status int
19
+	header http.Header
20
+}
21
+
22
+// Write implements the http.ResponseWriter interface.
23
+func (rb *ResponseBuffer) Write(p []byte) (int, error) {
24
+	return rb.buf.Write(p)
25
+}
26
+
27
+// WriteHeader implements the http.ResponseWriter interface.
28
+func (rb *ResponseBuffer) WriteHeader(status int) {
29
+	rb.status = status
30
+}
31
+
32
+// Header implements the http.ResponseWriter interface.
33
+func (rb *ResponseBuffer) Header() http.Header {
34
+	if rb.header == nil {
35
+		rb.header = make(http.Header)
36
+	}
37
+	return rb.header
38
+}
39
+
40
+// WriteTo implements the io.WriterTo interface.
41
+func (rb *ResponseBuffer) WriteTo(w http.ResponseWriter) error {
42
+	for k, v := range rb.header {
43
+		w.Header()[k] = v
44
+	}
45
+	if rb.buf.Len() > 0 {
46
+		w.Header().Set("Content-Length", strconv.Itoa(rb.buf.Len()))
47
+	}
48
+	if rb.status != 0 {
49
+		w.WriteHeader(rb.status)
50
+	}
51
+	if rb.buf.Len() > 0 {
52
+		if _, err := w.Write(rb.buf.Bytes()); err != nil {
53
+			return err
54
+		}
55
+	}
56
+	return nil
57
+}
0 58
new file mode 100644
... ...
@@ -0,0 +1,265 @@
0
+// Copyright 2013 The Go Authors. All rights reserved.
1
+//
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file or at
4
+// https://developers.google.com/open-source/licenses/bsd.
5
+
6
+package httputil
7
+
8
+import (
9
+	"bytes"
10
+	"crypto/sha1"
11
+	"errors"
12
+	"fmt"
13
+	"github.com/golang/gddo/httputil/header"
14
+	"io"
15
+	"io/ioutil"
16
+	"mime"
17
+	"net/http"
18
+	"os"
19
+	"path"
20
+	"path/filepath"
21
+	"strconv"
22
+	"strings"
23
+	"sync"
24
+	"time"
25
+)
26
+
27
+// StaticServer serves static files.
28
+type StaticServer struct {
29
+	// Dir specifies the location of the directory containing the files to serve.
30
+	Dir string
31
+
32
+	// MaxAge specifies the maximum age for the cache control and expiration
33
+	// headers.
34
+	MaxAge time.Duration
35
+
36
+	// Error specifies the function used to generate error responses. If Error
37
+	// is nil, then http.Error is used to generate error responses.
38
+	Error Error
39
+
40
+	// MIMETypes is a map from file extensions to MIME types.
41
+	MIMETypes map[string]string
42
+
43
+	mu    sync.Mutex
44
+	etags map[string]string
45
+}
46
+
47
+func (ss *StaticServer) resolve(fname string) string {
48
+	if path.IsAbs(fname) {
49
+		panic("Absolute path not allowed when creating a StaticServer handler")
50
+	}
51
+	dir := ss.Dir
52
+	if dir == "" {
53
+		dir = "."
54
+	}
55
+	fname = filepath.FromSlash(fname)
56
+	return filepath.Join(dir, fname)
57
+}
58
+
59
+func (ss *StaticServer) mimeType(fname string) string {
60
+	ext := path.Ext(fname)
61
+	var mimeType string
62
+	if ss.MIMETypes != nil {
63
+		mimeType = ss.MIMETypes[ext]
64
+	}
65
+	if mimeType == "" {
66
+		mimeType = mime.TypeByExtension(ext)
67
+	}
68
+	if mimeType == "" {
69
+		mimeType = "application/octet-stream"
70
+	}
71
+	return mimeType
72
+}
73
+
74
+func (ss *StaticServer) openFile(fname string) (io.ReadCloser, int64, string, error) {
75
+	f, err := os.Open(fname)
76
+	if err != nil {
77
+		return nil, 0, "", err
78
+	}
79
+	fi, err := f.Stat()
80
+	if err != nil {
81
+		f.Close()
82
+		return nil, 0, "", err
83
+	}
84
+	const modeType = os.ModeDir | os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice
85
+	if fi.Mode()&modeType != 0 {
86
+		f.Close()
87
+		return nil, 0, "", errors.New("not a regular file")
88
+	}
89
+	return f, fi.Size(), ss.mimeType(fname), nil
90
+}
91
+
92
+// FileHandler returns a handler that serves a single file. The file is
93
+// specified by a slash separated path relative to the static server's Dir
94
+// field.
95
+func (ss *StaticServer) FileHandler(fileName string) http.Handler {
96
+	id := fileName
97
+	fileName = ss.resolve(fileName)
98
+	return &staticHandler{
99
+		ss:   ss,
100
+		id:   func(_ string) string { return id },
101
+		open: func(_ string) (io.ReadCloser, int64, string, error) { return ss.openFile(fileName) },
102
+	}
103
+}
104
+
105
+// DirectoryHandler returns a handler that serves files from a directory tree.
106
+// The directory is specified by a slash separated path relative to the static
107
+// server's Dir field.
108
+func (ss *StaticServer) DirectoryHandler(prefix, dirName string) http.Handler {
109
+	if !strings.HasSuffix(prefix, "/") {
110
+		prefix += "/"
111
+	}
112
+	idBase := dirName
113
+	dirName = ss.resolve(dirName)
114
+	return &staticHandler{
115
+		ss: ss,
116
+		id: func(p string) string {
117
+			if !strings.HasPrefix(p, prefix) {
118
+				return "."
119
+			}
120
+			return path.Join(idBase, p[len(prefix):])
121
+		},
122
+		open: func(p string) (io.ReadCloser, int64, string, error) {
123
+			if !strings.HasPrefix(p, prefix) {
124
+				return nil, 0, "", errors.New("request url does not match directory prefix")
125
+			}
126
+			p = p[len(prefix):]
127
+			return ss.openFile(filepath.Join(dirName, filepath.FromSlash(p)))
128
+		},
129
+	}
130
+}
131
+
132
+// FilesHandler returns a handler that serves the concatentation of the
133
+// specified files. The files are specified by slash separated paths relative
134
+// to the static server's Dir field.
135
+func (ss *StaticServer) FilesHandler(fileNames ...string) http.Handler {
136
+
137
+	// todo: cache concatenated files on disk and serve from there.
138
+
139
+	mimeType := ss.mimeType(fileNames[0])
140
+	var buf []byte
141
+	var openErr error
142
+
143
+	for _, fileName := range fileNames {
144
+		p, err := ioutil.ReadFile(ss.resolve(fileName))
145
+		if err != nil {
146
+			openErr = err
147
+			buf = nil
148
+			break
149
+		}
150
+		buf = append(buf, p...)
151
+	}
152
+
153
+	id := strings.Join(fileNames, " ")
154
+
155
+	return &staticHandler{
156
+		ss: ss,
157
+		id: func(_ string) string { return id },
158
+		open: func(p string) (io.ReadCloser, int64, string, error) {
159
+			return ioutil.NopCloser(bytes.NewReader(buf)), int64(len(buf)), mimeType, openErr
160
+		},
161
+	}
162
+}
163
+
164
+type staticHandler struct {
165
+	id   func(fname string) string
166
+	open func(p string) (io.ReadCloser, int64, string, error)
167
+	ss   *StaticServer
168
+}
169
+
170
+func (h *staticHandler) error(w http.ResponseWriter, r *http.Request, status int, err error) {
171
+	http.Error(w, http.StatusText(status), status)
172
+}
173
+
174
+func (h *staticHandler) etag(p string) (string, error) {
175
+	id := h.id(p)
176
+
177
+	h.ss.mu.Lock()
178
+	if h.ss.etags == nil {
179
+		h.ss.etags = make(map[string]string)
180
+	}
181
+	etag := h.ss.etags[id]
182
+	h.ss.mu.Unlock()
183
+
184
+	if etag != "" {
185
+		return etag, nil
186
+	}
187
+
188
+	// todo: if a concurrent goroutine is calculating the hash, then wait for
189
+	// it instead of computing it again here.
190
+
191
+	rc, _, _, err := h.open(p)
192
+	if err != nil {
193
+		return "", err
194
+	}
195
+
196
+	defer rc.Close()
197
+
198
+	w := sha1.New()
199
+	_, err = io.Copy(w, rc)
200
+	if err != nil {
201
+		return "", err
202
+	}
203
+
204
+	etag = fmt.Sprintf(`"%x"`, w.Sum(nil))
205
+
206
+	h.ss.mu.Lock()
207
+	h.ss.etags[id] = etag
208
+	h.ss.mu.Unlock()
209
+
210
+	return etag, nil
211
+}
212
+
213
+func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
214
+	p := path.Clean(r.URL.Path)
215
+	if p != r.URL.Path {
216
+		http.Redirect(w, r, p, 301)
217
+		return
218
+	}
219
+
220
+	etag, err := h.etag(p)
221
+	if err != nil {
222
+		h.error(w, r, http.StatusNotFound, err)
223
+		return
224
+	}
225
+
226
+	maxAge := h.ss.MaxAge
227
+	if maxAge == 0 {
228
+		maxAge = 24 * time.Hour
229
+	}
230
+	if r.FormValue("v") != "" {
231
+		maxAge = 365 * 24 * time.Hour
232
+	}
233
+
234
+	cacheControl := fmt.Sprintf("public, max-age=%d", maxAge/time.Second)
235
+
236
+	for _, e := range header.ParseList(r.Header, "If-None-Match") {
237
+		if e == etag {
238
+			w.Header().Set("Cache-Control", cacheControl)
239
+			w.Header().Set("Etag", etag)
240
+			w.WriteHeader(http.StatusNotModified)
241
+			return
242
+		}
243
+	}
244
+
245
+	rc, cl, ct, err := h.open(p)
246
+	if err != nil {
247
+		h.error(w, r, http.StatusNotFound, err)
248
+		return
249
+	}
250
+	defer rc.Close()
251
+
252
+	w.Header().Set("Cache-Control", cacheControl)
253
+	w.Header().Set("Etag", etag)
254
+	if ct != "" {
255
+		w.Header().Set("Content-Type", ct)
256
+	}
257
+	if cl != 0 {
258
+		w.Header().Set("Content-Length", strconv.FormatInt(cl, 10))
259
+	}
260
+	w.WriteHeader(http.StatusOK)
261
+	if r.Method != "HEAD" {
262
+		io.Copy(w, rc)
263
+	}
264
+}
0 265
new file mode 100644
... ...
@@ -0,0 +1,87 @@
0
+// Copyright 2015 The Go Authors. All rights reserved.
1
+//
2
+// Use of this source code is governed by a BSD-style
3
+// license that can be found in the LICENSE file or at
4
+// https://developers.google.com/open-source/licenses/bsd.
5
+
6
+// This file implements a http.RoundTripper that authenticates
7
+// requests issued against api.github.com endpoint.
8
+
9
+package httputil
10
+
11
+import (
12
+	"net/http"
13
+	"net/url"
14
+)
15
+
16
+// AuthTransport is an implementation of http.RoundTripper that authenticates
17
+// with the GitHub API.
18
+//
19
+// When both a token and client credentials are set, the latter is preferred.
20
+type AuthTransport struct {
21
+	UserAgent          string
22
+	GithubToken        string
23
+	GithubClientID     string
24
+	GithubClientSecret string
25
+	Base               http.RoundTripper
26
+}
27
+
28
+// RoundTrip implements the http.RoundTripper interface.
29
+func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
30
+	var reqCopy *http.Request
31
+	if t.UserAgent != "" {
32
+		reqCopy = copyRequest(req)
33
+		reqCopy.Header.Set("User-Agent", t.UserAgent)
34
+	}
35
+	if req.URL.Host == "api.github.com" && req.URL.Scheme == "https" {
36
+		switch {
37
+		case t.GithubClientID != "" && t.GithubClientSecret != "":
38
+			if reqCopy == nil {
39
+				reqCopy = copyRequest(req)
40
+			}
41
+			if reqCopy.URL.RawQuery == "" {
42
+				reqCopy.URL.RawQuery = "client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
43
+			} else {
44
+				reqCopy.URL.RawQuery += "&client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
45
+			}
46
+		case t.GithubToken != "":
47
+			if reqCopy == nil {
48
+				reqCopy = copyRequest(req)
49
+			}
50
+			reqCopy.Header.Set("Authorization", "token "+t.GithubToken)
51
+		}
52
+	}
53
+	if reqCopy != nil {
54
+		return t.base().RoundTrip(reqCopy)
55
+	}
56
+	return t.base().RoundTrip(req)
57
+}
58
+
59
+// CancelRequest cancels an in-flight request by closing its connection.
60
+func (t *AuthTransport) CancelRequest(req *http.Request) {
61
+	type canceler interface {
62
+		CancelRequest(req *http.Request)
63
+	}
64
+	if cr, ok := t.base().(canceler); ok {
65
+		cr.CancelRequest(req)
66
+	}
67
+}
68
+
69
+func (t *AuthTransport) base() http.RoundTripper {
70
+	if t.Base != nil {
71
+		return t.Base
72
+	}
73
+	return http.DefaultTransport
74
+}
75
+
76
+func copyRequest(req *http.Request) *http.Request {
77
+	req2 := new(http.Request)
78
+	*req2 = *req
79
+	req2.URL = new(url.URL)
80
+	*req2.URL = *req.URL
81
+	req2.Header = make(http.Header, len(req.Header))
82
+	for k, s := range req.Header {
83
+		req2.Header[k] = append([]string(nil), s...)
84
+	}
85
+	return req2
86
+}