Merge pull request #36164 from emil2k/gzip-archive
Content encoding negotiation added to archive request.
This commit is contained in:
commit
20c4efee7b
11 changed files with 1006 additions and 3 deletions
|
@ -1,6 +1,8 @@
|
|||
package container // import "github.com/docker/docker/api/server/router/container"
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
@ -9,6 +11,7 @@ import (
|
|||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
gddohttputil "github.com/golang/gddo/httputil"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
|
@ -81,6 +84,29 @@ func (s *containerRouter) headContainersArchive(ctx context.Context, w http.Resp
|
|||
return setContainerPathStatHeader(stat, w.Header())
|
||||
}
|
||||
|
||||
func writeCompressedResponse(w http.ResponseWriter, r *http.Request, body io.Reader) error {
|
||||
var cw io.Writer
|
||||
switch gddohttputil.NegotiateContentEncoding(r, []string{"gzip", "deflate"}) {
|
||||
case "gzip":
|
||||
gw := gzip.NewWriter(w)
|
||||
defer gw.Close()
|
||||
cw = gw
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
case "deflate":
|
||||
fw, err := flate.NewWriter(w, flate.DefaultCompression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fw.Close()
|
||||
cw = fw
|
||||
w.Header().Set("Content-Encoding", "deflate")
|
||||
default:
|
||||
cw = w
|
||||
}
|
||||
_, err := io.Copy(cw, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *containerRouter) getContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
v, err := httputils.ArchiveFormValues(r, vars)
|
||||
if err != nil {
|
||||
|
@ -98,9 +124,7 @@ func (s *containerRouter) getContainersArchive(ctx context.Context, w http.Respo
|
|||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-tar")
|
||||
_, err = io.Copy(w, tarArchive)
|
||||
|
||||
return err
|
||||
return writeCompressedResponse(w, r, tarArchive)
|
||||
}
|
||||
|
||||
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
|
|||
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
|
||||
github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
github.com/go-check/check 4ed411733c5785b40214c70bce814c3a3a689609 https://github.com/cpuguy83/check.git
|
||||
github.com/golang/gddo 9b12a26f3fbd7397dee4e20939ddca719d840d2a
|
||||
github.com/gorilla/context v1.1
|
||||
github.com/gorilla/mux v1.1
|
||||
github.com/Microsoft/opengcs v0.3.6
|
||||
|
|
27
vendor/github.com/golang/gddo/LICENSE
generated
vendored
Normal file
27
vendor/github.com/golang/gddo/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2013 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
44
vendor/github.com/golang/gddo/README.markdown
generated
vendored
Normal file
44
vendor/github.com/golang/gddo/README.markdown
generated
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
This project is the source for http://godoc.org/
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](http://godoc.org/github.com/golang/gddo)
|
||||
[![Build
|
||||
Status](https://travis-ci.org/golang/gddo.svg?branch=master)](https://travis-ci.org/golang/gddo)
|
||||
|
||||
The code in this project is designed to be used by godoc.org. Send mail to
|
||||
golang-dev@googlegroups.com if you want to discuss other uses of the code.
|
||||
|
||||
## Feedback
|
||||
|
||||
Send ideas and questions to golang-dev@googlegroups.com. Request features and
|
||||
report bugs using the [GitHub Issue
|
||||
Tracker](https://github.com/golang/gddo/issues/new).
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions to this project are welcome, though please [file an
|
||||
issue](https://github.com/golang/gddo/issues/new). before starting work on
|
||||
anything major.
|
||||
|
||||
**We do not accept GitHub pull requests**
|
||||
|
||||
Please refer to the [Contribution
|
||||
Guidelines](https://golang.org/doc/contribute.html) on how to submit changes.
|
||||
|
||||
We use https://go-review.googlesource.com to review change submissions.
|
||||
|
||||
## Getting the Source
|
||||
|
||||
To get started contributing to this project, clone the repository from its
|
||||
canonical location
|
||||
|
||||
```
|
||||
git clone https://go.googlesource.com/gddo $GOPATH/src/github.com/golang/gddo
|
||||
```
|
||||
|
||||
Information on how to set up a local environment is available at
|
||||
https://github.com/golang/gddo/wiki/Development-Environment-Setup.
|
||||
|
||||
## More Documentation
|
||||
|
||||
More documentation about this project is available on the
|
||||
[wiki](https://github.com/golang/gddo/wiki).
|
95
vendor/github.com/golang/gddo/httputil/buster.go
generated
vendored
Normal file
95
vendor/github.com/golang/gddo/httputil/buster.go
generated
vendored
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type busterWriter struct {
|
||||
headerMap http.Header
|
||||
status int
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (bw *busterWriter) Header() http.Header {
|
||||
return bw.headerMap
|
||||
}
|
||||
|
||||
func (bw *busterWriter) WriteHeader(status int) {
|
||||
bw.status = status
|
||||
}
|
||||
|
||||
// CacheBusters maintains a cache of cache busting tokens for static resources served by Handler.
|
||||
type CacheBusters struct {
|
||||
Handler http.Handler
|
||||
|
||||
mu sync.Mutex
|
||||
tokens map[string]string
|
||||
}
|
||||
|
||||
func sanitizeTokenRune(r rune) rune {
|
||||
if r <= ' ' || r >= 127 {
|
||||
return -1
|
||||
}
|
||||
// Convert percent encoding reserved characters to '-'.
|
||||
if strings.ContainsRune("!#$&'()*+,/:;=?@[]", r) {
|
||||
return '-'
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Get returns the cache busting token for path. If the token is not already
|
||||
// cached, Get issues a HEAD request on handler and uses the response ETag and
|
||||
// Last-Modified headers to compute a token.
|
||||
func (cb *CacheBusters) Get(path string) string {
|
||||
cb.mu.Lock()
|
||||
if cb.tokens == nil {
|
||||
cb.tokens = make(map[string]string)
|
||||
}
|
||||
token, ok := cb.tokens[path]
|
||||
cb.mu.Unlock()
|
||||
if ok {
|
||||
return token
|
||||
}
|
||||
|
||||
w := busterWriter{
|
||||
Writer: ioutil.Discard,
|
||||
headerMap: make(http.Header),
|
||||
}
|
||||
r := &http.Request{URL: &url.URL{Path: path}, Method: "HEAD"}
|
||||
cb.Handler.ServeHTTP(&w, r)
|
||||
|
||||
if w.status == 200 {
|
||||
token = w.headerMap.Get("Etag")
|
||||
if token == "" {
|
||||
token = w.headerMap.Get("Last-Modified")
|
||||
}
|
||||
token = strings.Trim(token, `" `)
|
||||
token = strings.Map(sanitizeTokenRune, token)
|
||||
}
|
||||
|
||||
cb.mu.Lock()
|
||||
cb.tokens[path] = token
|
||||
cb.mu.Unlock()
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// AppendQueryParam appends the token as a query parameter to path.
|
||||
func (cb *CacheBusters) AppendQueryParam(path string, name string) string {
|
||||
token := cb.Get(path)
|
||||
if token == "" {
|
||||
return path
|
||||
}
|
||||
return path + "?" + name + "=" + token
|
||||
}
|
298
vendor/github.com/golang/gddo/httputil/header/header.go
generated
vendored
Normal file
298
vendor/github.com/golang/gddo/httputil/header/header.go
generated
vendored
Normal file
|
@ -0,0 +1,298 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
// Package header provides functions for parsing HTTP headers.
|
||||
package header
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Octet types from RFC 2616.
|
||||
var octetTypes [256]octetType
|
||||
|
||||
type octetType byte
|
||||
|
||||
const (
|
||||
isToken octetType = 1 << iota
|
||||
isSpace
|
||||
)
|
||||
|
||||
func init() {
|
||||
// OCTET = <any 8-bit sequence of data>
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
// CR = <US-ASCII CR, carriage return (13)>
|
||||
// LF = <US-ASCII LF, linefeed (10)>
|
||||
// SP = <US-ASCII SP, space (32)>
|
||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
// <"> = <US-ASCII double-quote mark (34)>
|
||||
// CRLF = CR LF
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// qdtext = <any TEXT except <">>
|
||||
|
||||
for c := 0; c < 256; c++ {
|
||||
var t octetType
|
||||
isCtl := c <= 31 || c == 127
|
||||
isChar := 0 <= c && c <= 127
|
||||
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
||||
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
||||
t |= isSpace
|
||||
}
|
||||
if isChar && !isCtl && !isSeparator {
|
||||
t |= isToken
|
||||
}
|
||||
octetTypes[c] = t
|
||||
}
|
||||
}
|
||||
|
||||
// Copy returns a shallow copy of the header.
|
||||
func Copy(header http.Header) http.Header {
|
||||
h := make(http.Header)
|
||||
for k, vs := range header {
|
||||
h[k] = vs
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
var timeLayouts = []string{"Mon, 02 Jan 2006 15:04:05 GMT", time.RFC850, time.ANSIC}
|
||||
|
||||
// ParseTime parses the header as time. The zero value is returned if the
|
||||
// header is not present or there is an error parsing the
|
||||
// header.
|
||||
func ParseTime(header http.Header, key string) time.Time {
|
||||
if s := header.Get(key); s != "" {
|
||||
for _, layout := range timeLayouts {
|
||||
if t, err := time.Parse(layout, s); err == nil {
|
||||
return t.UTC()
|
||||
}
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// ParseList parses a comma separated list of values. Commas are ignored in
|
||||
// quoted strings. Quoted values are not unescaped or unquoted. Whitespace is
|
||||
// trimmed.
|
||||
func ParseList(header http.Header, key string) []string {
|
||||
var result []string
|
||||
for _, s := range header[http.CanonicalHeaderKey(key)] {
|
||||
begin := 0
|
||||
end := 0
|
||||
escape := false
|
||||
quote := false
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
end = i + 1
|
||||
case quote:
|
||||
switch b {
|
||||
case '\\':
|
||||
escape = true
|
||||
case '"':
|
||||
quote = false
|
||||
}
|
||||
end = i + 1
|
||||
case b == '"':
|
||||
quote = true
|
||||
end = i + 1
|
||||
case octetTypes[b]&isSpace != 0:
|
||||
if begin == end {
|
||||
begin = i + 1
|
||||
end = begin
|
||||
}
|
||||
case b == ',':
|
||||
if begin < end {
|
||||
result = append(result, s[begin:end])
|
||||
}
|
||||
begin = i + 1
|
||||
end = begin
|
||||
default:
|
||||
end = i + 1
|
||||
}
|
||||
}
|
||||
if begin < end {
|
||||
result = append(result, s[begin:end])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseValueAndParams parses a comma separated list of values with optional
|
||||
// semicolon separated name-value pairs. Content-Type and Content-Disposition
|
||||
// headers are in this format.
|
||||
func ParseValueAndParams(header http.Header, key string) (value string, params map[string]string) {
|
||||
params = make(map[string]string)
|
||||
s := header.Get(key)
|
||||
value, s = expectTokenSlash(s)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
value = strings.ToLower(value)
|
||||
s = skipSpace(s)
|
||||
for strings.HasPrefix(s, ";") {
|
||||
var pkey string
|
||||
pkey, s = expectToken(skipSpace(s[1:]))
|
||||
if pkey == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(s, "=") {
|
||||
return
|
||||
}
|
||||
var pvalue string
|
||||
pvalue, s = expectTokenOrQuoted(s[1:])
|
||||
if pvalue == "" {
|
||||
return
|
||||
}
|
||||
pkey = strings.ToLower(pkey)
|
||||
params[pkey] = pvalue
|
||||
s = skipSpace(s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AcceptSpec describes an Accept* header.
|
||||
type AcceptSpec struct {
|
||||
Value string
|
||||
Q float64
|
||||
}
|
||||
|
||||
// ParseAccept parses Accept* headers.
|
||||
func ParseAccept(header http.Header, key string) (specs []AcceptSpec) {
|
||||
loop:
|
||||
for _, s := range header[key] {
|
||||
for {
|
||||
var spec AcceptSpec
|
||||
spec.Value, s = expectTokenSlash(s)
|
||||
if spec.Value == "" {
|
||||
continue loop
|
||||
}
|
||||
spec.Q = 1.0
|
||||
s = skipSpace(s)
|
||||
if strings.HasPrefix(s, ";") {
|
||||
s = skipSpace(s[1:])
|
||||
if !strings.HasPrefix(s, "q=") {
|
||||
continue loop
|
||||
}
|
||||
spec.Q, s = expectQuality(s[2:])
|
||||
if spec.Q < 0.0 {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
specs = append(specs, spec)
|
||||
s = skipSpace(s)
|
||||
if !strings.HasPrefix(s, ",") {
|
||||
continue loop
|
||||
}
|
||||
s = skipSpace(s[1:])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func skipSpace(s string) (rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isSpace == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
func expectToken(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isToken == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func expectTokenSlash(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if (octetTypes[b]&isToken == 0) && b != '/' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func expectQuality(s string) (q float64, rest string) {
|
||||
switch {
|
||||
case len(s) == 0:
|
||||
return -1, ""
|
||||
case s[0] == '0':
|
||||
q = 0
|
||||
case s[0] == '1':
|
||||
q = 1
|
||||
default:
|
||||
return -1, ""
|
||||
}
|
||||
s = s[1:]
|
||||
if !strings.HasPrefix(s, ".") {
|
||||
return q, s
|
||||
}
|
||||
s = s[1:]
|
||||
i := 0
|
||||
n := 0
|
||||
d := 1
|
||||
for ; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if b < '0' || b > '9' {
|
||||
break
|
||||
}
|
||||
n = n*10 + int(b) - '0'
|
||||
d *= 10
|
||||
}
|
||||
return q + float64(n)/float64(d), s[i:]
|
||||
}
|
||||
|
||||
func expectTokenOrQuoted(s string) (value string, rest string) {
|
||||
if !strings.HasPrefix(s, "\"") {
|
||||
return expectToken(s)
|
||||
}
|
||||
s = s[1:]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '"':
|
||||
return s[:i], s[i+1:]
|
||||
case '\\':
|
||||
p := make([]byte, len(s)-1)
|
||||
j := copy(p, s[:i])
|
||||
escape := true
|
||||
for i = i + 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j++
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j++
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
25
vendor/github.com/golang/gddo/httputil/httputil.go
generated
vendored
Normal file
25
vendor/github.com/golang/gddo/httputil/httputil.go
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
// Package httputil is a toolkit for the Go net/http package.
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// StripPort removes the port specification from an address.
|
||||
func StripPort(s string) string {
|
||||
if h, _, err := net.SplitHostPort(s); err == nil {
|
||||
s = h
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Error defines a type for a function that accepts a ResponseWriter for
|
||||
// a Request with the HTTP status code and error.
|
||||
type Error func(w http.ResponseWriter, r *http.Request, status int, err error)
|
79
vendor/github.com/golang/gddo/httputil/negotiate.go
generated
vendored
Normal file
79
vendor/github.com/golang/gddo/httputil/negotiate.go
generated
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"github.com/golang/gddo/httputil/header"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NegotiateContentEncoding returns the best offered content encoding for the
|
||||
// request's Accept-Encoding header. If two offers match with equal weight and
|
||||
// then the offer earlier in the list is preferred. If no offers are
|
||||
// acceptable, then "" is returned.
|
||||
func NegotiateContentEncoding(r *http.Request, offers []string) string {
|
||||
bestOffer := "identity"
|
||||
bestQ := -1.0
|
||||
specs := header.ParseAccept(r.Header, "Accept-Encoding")
|
||||
for _, offer := range offers {
|
||||
for _, spec := range specs {
|
||||
if spec.Q > bestQ &&
|
||||
(spec.Value == "*" || spec.Value == offer) {
|
||||
bestQ = spec.Q
|
||||
bestOffer = offer
|
||||
}
|
||||
}
|
||||
}
|
||||
if bestQ == 0 {
|
||||
bestOffer = ""
|
||||
}
|
||||
return bestOffer
|
||||
}
|
||||
|
||||
// NegotiateContentType returns the best offered content type for the request's
|
||||
// Accept header. If two offers match with equal weight, then the more specific
|
||||
// offer is preferred. For example, text/* trumps */*. If two offers match
|
||||
// with equal weight and specificity, then the offer earlier in the list is
|
||||
// preferred. If no offers match, then defaultOffer is returned.
|
||||
func NegotiateContentType(r *http.Request, offers []string, defaultOffer string) string {
|
||||
bestOffer := defaultOffer
|
||||
bestQ := -1.0
|
||||
bestWild := 3
|
||||
specs := header.ParseAccept(r.Header, "Accept")
|
||||
for _, offer := range offers {
|
||||
for _, spec := range specs {
|
||||
switch {
|
||||
case spec.Q == 0.0:
|
||||
// ignore
|
||||
case spec.Q < bestQ:
|
||||
// better match found
|
||||
case spec.Value == "*/*":
|
||||
if spec.Q > bestQ || bestWild > 2 {
|
||||
bestQ = spec.Q
|
||||
bestWild = 2
|
||||
bestOffer = offer
|
||||
}
|
||||
case strings.HasSuffix(spec.Value, "/*"):
|
||||
if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) &&
|
||||
(spec.Q > bestQ || bestWild > 1) {
|
||||
bestQ = spec.Q
|
||||
bestWild = 1
|
||||
bestOffer = offer
|
||||
}
|
||||
default:
|
||||
if spec.Value == offer &&
|
||||
(spec.Q > bestQ || bestWild > 0) {
|
||||
bestQ = spec.Q
|
||||
bestWild = 0
|
||||
bestOffer = offer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestOffer
|
||||
}
|
58
vendor/github.com/golang/gddo/httputil/respbuf.go
generated
vendored
Normal file
58
vendor/github.com/golang/gddo/httputil/respbuf.go
generated
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ResponseBuffer is the current response being composed by its owner.
|
||||
// It implements http.ResponseWriter and io.WriterTo.
|
||||
type ResponseBuffer struct {
|
||||
buf bytes.Buffer
|
||||
status int
|
||||
header http.Header
|
||||
}
|
||||
|
||||
// Write implements the http.ResponseWriter interface.
|
||||
func (rb *ResponseBuffer) Write(p []byte) (int, error) {
|
||||
return rb.buf.Write(p)
|
||||
}
|
||||
|
||||
// WriteHeader implements the http.ResponseWriter interface.
|
||||
func (rb *ResponseBuffer) WriteHeader(status int) {
|
||||
rb.status = status
|
||||
}
|
||||
|
||||
// Header implements the http.ResponseWriter interface.
|
||||
func (rb *ResponseBuffer) Header() http.Header {
|
||||
if rb.header == nil {
|
||||
rb.header = make(http.Header)
|
||||
}
|
||||
return rb.header
|
||||
}
|
||||
|
||||
// WriteTo implements the io.WriterTo interface.
|
||||
func (rb *ResponseBuffer) WriteTo(w http.ResponseWriter) error {
|
||||
for k, v := range rb.header {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
if rb.buf.Len() > 0 {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(rb.buf.Len()))
|
||||
}
|
||||
if rb.status != 0 {
|
||||
w.WriteHeader(rb.status)
|
||||
}
|
||||
if rb.buf.Len() > 0 {
|
||||
if _, err := w.Write(rb.buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
265
vendor/github.com/golang/gddo/httputil/static.go
generated
vendored
Normal file
265
vendor/github.com/golang/gddo/httputil/static.go
generated
vendored
Normal file
|
@ -0,0 +1,265 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang/gddo/httputil/header"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StaticServer serves static files.
|
||||
type StaticServer struct {
|
||||
// Dir specifies the location of the directory containing the files to serve.
|
||||
Dir string
|
||||
|
||||
// MaxAge specifies the maximum age for the cache control and expiration
|
||||
// headers.
|
||||
MaxAge time.Duration
|
||||
|
||||
// Error specifies the function used to generate error responses. If Error
|
||||
// is nil, then http.Error is used to generate error responses.
|
||||
Error Error
|
||||
|
||||
// MIMETypes is a map from file extensions to MIME types.
|
||||
MIMETypes map[string]string
|
||||
|
||||
mu sync.Mutex
|
||||
etags map[string]string
|
||||
}
|
||||
|
||||
func (ss *StaticServer) resolve(fname string) string {
|
||||
if path.IsAbs(fname) {
|
||||
panic("Absolute path not allowed when creating a StaticServer handler")
|
||||
}
|
||||
dir := ss.Dir
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
fname = filepath.FromSlash(fname)
|
||||
return filepath.Join(dir, fname)
|
||||
}
|
||||
|
||||
func (ss *StaticServer) mimeType(fname string) string {
|
||||
ext := path.Ext(fname)
|
||||
var mimeType string
|
||||
if ss.MIMETypes != nil {
|
||||
mimeType = ss.MIMETypes[ext]
|
||||
}
|
||||
if mimeType == "" {
|
||||
mimeType = mime.TypeByExtension(ext)
|
||||
}
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
return mimeType
|
||||
}
|
||||
|
||||
func (ss *StaticServer) openFile(fname string) (io.ReadCloser, int64, string, error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, 0, "", err
|
||||
}
|
||||
const modeType = os.ModeDir | os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice
|
||||
if fi.Mode()&modeType != 0 {
|
||||
f.Close()
|
||||
return nil, 0, "", errors.New("not a regular file")
|
||||
}
|
||||
return f, fi.Size(), ss.mimeType(fname), nil
|
||||
}
|
||||
|
||||
// FileHandler returns a handler that serves a single file. The file is
|
||||
// specified by a slash separated path relative to the static server's Dir
|
||||
// field.
|
||||
func (ss *StaticServer) FileHandler(fileName string) http.Handler {
|
||||
id := fileName
|
||||
fileName = ss.resolve(fileName)
|
||||
return &staticHandler{
|
||||
ss: ss,
|
||||
id: func(_ string) string { return id },
|
||||
open: func(_ string) (io.ReadCloser, int64, string, error) { return ss.openFile(fileName) },
|
||||
}
|
||||
}
|
||||
|
||||
// DirectoryHandler returns a handler that serves files from a directory tree.
|
||||
// The directory is specified by a slash separated path relative to the static
|
||||
// server's Dir field.
|
||||
func (ss *StaticServer) DirectoryHandler(prefix, dirName string) http.Handler {
|
||||
if !strings.HasSuffix(prefix, "/") {
|
||||
prefix += "/"
|
||||
}
|
||||
idBase := dirName
|
||||
dirName = ss.resolve(dirName)
|
||||
return &staticHandler{
|
||||
ss: ss,
|
||||
id: func(p string) string {
|
||||
if !strings.HasPrefix(p, prefix) {
|
||||
return "."
|
||||
}
|
||||
return path.Join(idBase, p[len(prefix):])
|
||||
},
|
||||
open: func(p string) (io.ReadCloser, int64, string, error) {
|
||||
if !strings.HasPrefix(p, prefix) {
|
||||
return nil, 0, "", errors.New("request url does not match directory prefix")
|
||||
}
|
||||
p = p[len(prefix):]
|
||||
return ss.openFile(filepath.Join(dirName, filepath.FromSlash(p)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// FilesHandler returns a handler that serves the concatentation of the
|
||||
// specified files. The files are specified by slash separated paths relative
|
||||
// to the static server's Dir field.
|
||||
func (ss *StaticServer) FilesHandler(fileNames ...string) http.Handler {
|
||||
|
||||
// todo: cache concatenated files on disk and serve from there.
|
||||
|
||||
mimeType := ss.mimeType(fileNames[0])
|
||||
var buf []byte
|
||||
var openErr error
|
||||
|
||||
for _, fileName := range fileNames {
|
||||
p, err := ioutil.ReadFile(ss.resolve(fileName))
|
||||
if err != nil {
|
||||
openErr = err
|
||||
buf = nil
|
||||
break
|
||||
}
|
||||
buf = append(buf, p...)
|
||||
}
|
||||
|
||||
id := strings.Join(fileNames, " ")
|
||||
|
||||
return &staticHandler{
|
||||
ss: ss,
|
||||
id: func(_ string) string { return id },
|
||||
open: func(p string) (io.ReadCloser, int64, string, error) {
|
||||
return ioutil.NopCloser(bytes.NewReader(buf)), int64(len(buf)), mimeType, openErr
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type staticHandler struct {
|
||||
id func(fname string) string
|
||||
open func(p string) (io.ReadCloser, int64, string, error)
|
||||
ss *StaticServer
|
||||
}
|
||||
|
||||
func (h *staticHandler) error(w http.ResponseWriter, r *http.Request, status int, err error) {
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
func (h *staticHandler) etag(p string) (string, error) {
|
||||
id := h.id(p)
|
||||
|
||||
h.ss.mu.Lock()
|
||||
if h.ss.etags == nil {
|
||||
h.ss.etags = make(map[string]string)
|
||||
}
|
||||
etag := h.ss.etags[id]
|
||||
h.ss.mu.Unlock()
|
||||
|
||||
if etag != "" {
|
||||
return etag, nil
|
||||
}
|
||||
|
||||
// todo: if a concurrent goroutine is calculating the hash, then wait for
|
||||
// it instead of computing it again here.
|
||||
|
||||
rc, _, _, err := h.open(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer rc.Close()
|
||||
|
||||
w := sha1.New()
|
||||
_, err = io.Copy(w, rc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
etag = fmt.Sprintf(`"%x"`, w.Sum(nil))
|
||||
|
||||
h.ss.mu.Lock()
|
||||
h.ss.etags[id] = etag
|
||||
h.ss.mu.Unlock()
|
||||
|
||||
return etag, nil
|
||||
}
|
||||
|
||||
func (h *staticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p := path.Clean(r.URL.Path)
|
||||
if p != r.URL.Path {
|
||||
http.Redirect(w, r, p, 301)
|
||||
return
|
||||
}
|
||||
|
||||
etag, err := h.etag(p)
|
||||
if err != nil {
|
||||
h.error(w, r, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
maxAge := h.ss.MaxAge
|
||||
if maxAge == 0 {
|
||||
maxAge = 24 * time.Hour
|
||||
}
|
||||
if r.FormValue("v") != "" {
|
||||
maxAge = 365 * 24 * time.Hour
|
||||
}
|
||||
|
||||
cacheControl := fmt.Sprintf("public, max-age=%d", maxAge/time.Second)
|
||||
|
||||
for _, e := range header.ParseList(r.Header, "If-None-Match") {
|
||||
if e == etag {
|
||||
w.Header().Set("Cache-Control", cacheControl)
|
||||
w.Header().Set("Etag", etag)
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rc, cl, ct, err := h.open(p)
|
||||
if err != nil {
|
||||
h.error(w, r, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
w.Header().Set("Cache-Control", cacheControl)
|
||||
w.Header().Set("Etag", etag)
|
||||
if ct != "" {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
if cl != 0 {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(cl, 10))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if r.Method != "HEAD" {
|
||||
io.Copy(w, rc)
|
||||
}
|
||||
}
|
87
vendor/github.com/golang/gddo/httputil/transport.go
generated
vendored
Normal file
87
vendor/github.com/golang/gddo/httputil/transport.go
generated
vendored
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd.
|
||||
|
||||
// This file implements a http.RoundTripper that authenticates
|
||||
// requests issued against api.github.com endpoint.
|
||||
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// AuthTransport is an implementation of http.RoundTripper that authenticates
|
||||
// with the GitHub API.
|
||||
//
|
||||
// When both a token and client credentials are set, the latter is preferred.
|
||||
type AuthTransport struct {
|
||||
UserAgent string
|
||||
GithubToken string
|
||||
GithubClientID string
|
||||
GithubClientSecret string
|
||||
Base http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip implements the http.RoundTripper interface.
|
||||
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var reqCopy *http.Request
|
||||
if t.UserAgent != "" {
|
||||
reqCopy = copyRequest(req)
|
||||
reqCopy.Header.Set("User-Agent", t.UserAgent)
|
||||
}
|
||||
if req.URL.Host == "api.github.com" && req.URL.Scheme == "https" {
|
||||
switch {
|
||||
case t.GithubClientID != "" && t.GithubClientSecret != "":
|
||||
if reqCopy == nil {
|
||||
reqCopy = copyRequest(req)
|
||||
}
|
||||
if reqCopy.URL.RawQuery == "" {
|
||||
reqCopy.URL.RawQuery = "client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
|
||||
} else {
|
||||
reqCopy.URL.RawQuery += "&client_id=" + t.GithubClientID + "&client_secret=" + t.GithubClientSecret
|
||||
}
|
||||
case t.GithubToken != "":
|
||||
if reqCopy == nil {
|
||||
reqCopy = copyRequest(req)
|
||||
}
|
||||
reqCopy.Header.Set("Authorization", "token "+t.GithubToken)
|
||||
}
|
||||
}
|
||||
if reqCopy != nil {
|
||||
return t.base().RoundTrip(reqCopy)
|
||||
}
|
||||
return t.base().RoundTrip(req)
|
||||
}
|
||||
|
||||
// CancelRequest cancels an in-flight request by closing its connection.
|
||||
func (t *AuthTransport) CancelRequest(req *http.Request) {
|
||||
type canceler interface {
|
||||
CancelRequest(req *http.Request)
|
||||
}
|
||||
if cr, ok := t.base().(canceler); ok {
|
||||
cr.CancelRequest(req)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AuthTransport) base() http.RoundTripper {
|
||||
if t.Base != nil {
|
||||
return t.Base
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
func copyRequest(req *http.Request) *http.Request {
|
||||
req2 := new(http.Request)
|
||||
*req2 = *req
|
||||
req2.URL = new(url.URL)
|
||||
*req2.URL = *req.URL
|
||||
req2.Header = make(http.Header, len(req.Header))
|
||||
for k, s := range req.Header {
|
||||
req2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
return req2
|
||||
}
|
Loading…
Reference in a new issue