Signed-off-by: Emil Davtyan <emil2k@gmail.com>
| ... | ... |
@@ -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 |
+[](http://godoc.org/github.com/golang/gddo) |
|
| 3 |
+[](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 |
+} |