зеркало из https://github.com/golang/net.git
http2, lex/httplex: make Transport reject bogus headers before sending
The http2.Transport was able to send bogus header keys & values. This changes rejects them earlier, before they hit the wire. In the process, mirror the lexical rules from the http package to x/net. Maintaining two copies has gotten increasingly annoying. Updates golang/go#14048 Change-Id: I20abcdeea92e7dc8706a1bbd60688ee8843a2b12 Reviewed-on: https://go-review.googlesource.com/23229 Reviewed-by: Andrew Gerrand <adg@golang.org>
This commit is contained in:
Родитель
3c5cb154b0
Коммит
5916dcb167
|
@ -15,6 +15,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/net/http2/hpack"
|
"golang.org/x/net/http2/hpack"
|
||||||
|
"golang.org/x/net/lex/httplex"
|
||||||
)
|
)
|
||||||
|
|
||||||
const frameHeaderLen = 9
|
const frameHeaderLen = 9
|
||||||
|
@ -1395,7 +1396,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
|
||||||
hdec.SetEmitEnabled(true)
|
hdec.SetEmitEnabled(true)
|
||||||
hdec.SetMaxStringLength(fr.maxHeaderStringLen())
|
hdec.SetMaxStringLength(fr.maxHeaderStringLen())
|
||||||
hdec.SetEmitFunc(func(hf hpack.HeaderField) {
|
hdec.SetEmitFunc(func(hf hpack.HeaderField) {
|
||||||
if !validHeaderFieldValue(hf.Value) {
|
if !httplex.ValidHeaderFieldValue(hf.Value) {
|
||||||
invalid = headerFieldValueError(hf.Value)
|
invalid = headerFieldValueError(hf.Value)
|
||||||
}
|
}
|
||||||
isPseudo := strings.HasPrefix(hf.Name, ":")
|
isPseudo := strings.HasPrefix(hf.Name, ":")
|
||||||
|
@ -1405,7 +1406,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sawRegular = true
|
sawRegular = true
|
||||||
if !validHeaderFieldName(hf.Name) {
|
if !validWireHeaderFieldName(hf.Name) {
|
||||||
invalid = headerFieldNameError(hf.Name)
|
invalid = headerFieldNameError(hf.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
129
http2/http2.go
129
http2/http2.go
|
@ -27,6 +27,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/net/lex/httplex"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -166,58 +168,23 @@ var (
|
||||||
errInvalidHeaderFieldValue = errors.New("http2: invalid header field value")
|
errInvalidHeaderFieldValue = errors.New("http2: invalid header field value")
|
||||||
)
|
)
|
||||||
|
|
||||||
// validHeaderFieldName reports whether v is a valid header field name (key).
|
// validWireHeaderFieldName reports whether v is a valid header field
|
||||||
// RFC 7230 says:
|
// name (key). See httplex.ValidHeaderName for the base rules.
|
||||||
// header-field = field-name ":" OWS field-value OWS
|
//
|
||||||
// field-name = token
|
|
||||||
// token = 1*tchar
|
|
||||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
|
|
||||||
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
|
||||||
// Further, http2 says:
|
// Further, http2 says:
|
||||||
// "Just as in HTTP/1.x, header field names are strings of ASCII
|
// "Just as in HTTP/1.x, header field names are strings of ASCII
|
||||||
// characters that are compared in a case-insensitive
|
// characters that are compared in a case-insensitive
|
||||||
// fashion. However, header field names MUST be converted to
|
// fashion. However, header field names MUST be converted to
|
||||||
// lowercase prior to their encoding in HTTP/2. "
|
// lowercase prior to their encoding in HTTP/2. "
|
||||||
func validHeaderFieldName(v string) bool {
|
func validWireHeaderFieldName(v string) bool {
|
||||||
if len(v) == 0 {
|
if len(v) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, r := range v {
|
for _, r := range v {
|
||||||
if int(r) >= len(isTokenTable) || ('A' <= r && r <= 'Z') {
|
if !httplex.IsTokenRune(r) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !isTokenTable[byte(r)] {
|
if 'A' <= r && r <= 'Z' {
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// validHeaderFieldValue reports whether v is a valid header field value.
|
|
||||||
//
|
|
||||||
// RFC 7230 says:
|
|
||||||
// field-value = *( field-content / obs-fold )
|
|
||||||
// obj-fold = N/A to http2, and deprecated
|
|
||||||
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
|
||||||
// field-vchar = VCHAR / obs-text
|
|
||||||
// obs-text = %x80-FF
|
|
||||||
// VCHAR = "any visible [USASCII] character"
|
|
||||||
//
|
|
||||||
// http2 further says: "Similarly, HTTP/2 allows header field values
|
|
||||||
// that are not valid. While most of the values that can be encoded
|
|
||||||
// will not alter header field parsing, carriage return (CR, ASCII
|
|
||||||
// 0xd), line feed (LF, ASCII 0xa), and the zero character (NUL, ASCII
|
|
||||||
// 0x0) might be exploited by an attacker if they are translated
|
|
||||||
// verbatim. Any request or response that contains a character not
|
|
||||||
// permitted in a header field value MUST be treated as malformed
|
|
||||||
// (Section 8.1.2.6). Valid characters are defined by the
|
|
||||||
// field-content ABNF rule in Section 3.2 of [RFC7230]."
|
|
||||||
//
|
|
||||||
// This function does not (yet?) properly handle the rejection of
|
|
||||||
// strings that begin or end with SP or HTAB.
|
|
||||||
func validHeaderFieldValue(v string) bool {
|
|
||||||
for i := 0; i < len(v); i++ {
|
|
||||||
if b := v[i]; b < ' ' && b != '\t' || b == 0x7f {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,86 +313,6 @@ func (e *httpError) Temporary() bool { return true }
|
||||||
|
|
||||||
var errTimeout error = &httpError{msg: "http2: timeout awaiting response headers", timeout: true}
|
var errTimeout error = &httpError{msg: "http2: timeout awaiting response headers", timeout: true}
|
||||||
|
|
||||||
var isTokenTable = [127]bool{
|
|
||||||
'!': true,
|
|
||||||
'#': true,
|
|
||||||
'$': true,
|
|
||||||
'%': true,
|
|
||||||
'&': true,
|
|
||||||
'\'': true,
|
|
||||||
'*': true,
|
|
||||||
'+': true,
|
|
||||||
'-': true,
|
|
||||||
'.': true,
|
|
||||||
'0': true,
|
|
||||||
'1': true,
|
|
||||||
'2': true,
|
|
||||||
'3': true,
|
|
||||||
'4': true,
|
|
||||||
'5': true,
|
|
||||||
'6': true,
|
|
||||||
'7': true,
|
|
||||||
'8': true,
|
|
||||||
'9': true,
|
|
||||||
'A': true,
|
|
||||||
'B': true,
|
|
||||||
'C': true,
|
|
||||||
'D': true,
|
|
||||||
'E': true,
|
|
||||||
'F': true,
|
|
||||||
'G': true,
|
|
||||||
'H': true,
|
|
||||||
'I': true,
|
|
||||||
'J': true,
|
|
||||||
'K': true,
|
|
||||||
'L': true,
|
|
||||||
'M': true,
|
|
||||||
'N': true,
|
|
||||||
'O': true,
|
|
||||||
'P': true,
|
|
||||||
'Q': true,
|
|
||||||
'R': true,
|
|
||||||
'S': true,
|
|
||||||
'T': true,
|
|
||||||
'U': true,
|
|
||||||
'W': true,
|
|
||||||
'V': true,
|
|
||||||
'X': true,
|
|
||||||
'Y': true,
|
|
||||||
'Z': true,
|
|
||||||
'^': true,
|
|
||||||
'_': true,
|
|
||||||
'`': true,
|
|
||||||
'a': true,
|
|
||||||
'b': true,
|
|
||||||
'c': true,
|
|
||||||
'd': true,
|
|
||||||
'e': true,
|
|
||||||
'f': true,
|
|
||||||
'g': true,
|
|
||||||
'h': true,
|
|
||||||
'i': true,
|
|
||||||
'j': true,
|
|
||||||
'k': true,
|
|
||||||
'l': true,
|
|
||||||
'm': true,
|
|
||||||
'n': true,
|
|
||||||
'o': true,
|
|
||||||
'p': true,
|
|
||||||
'q': true,
|
|
||||||
'r': true,
|
|
||||||
's': true,
|
|
||||||
't': true,
|
|
||||||
'u': true,
|
|
||||||
'v': true,
|
|
||||||
'w': true,
|
|
||||||
'x': true,
|
|
||||||
'y': true,
|
|
||||||
'z': true,
|
|
||||||
'|': true,
|
|
||||||
'~': true,
|
|
||||||
}
|
|
||||||
|
|
||||||
type connectionStater interface {
|
type connectionStater interface {
|
||||||
ConnectionState() tls.ConnectionState
|
ConnectionState() tls.ConnectionState
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,7 +209,9 @@ func (st *serverTester) Close() {
|
||||||
// unwindowing), force close the connection, so the
|
// unwindowing), force close the connection, so the
|
||||||
// httptest.Server doesn't wait forever for the conn
|
// httptest.Server doesn't wait forever for the conn
|
||||||
// to close.
|
// to close.
|
||||||
st.cc.Close()
|
if st.cc != nil {
|
||||||
|
st.cc.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
st.ts.Close()
|
st.ts.Close()
|
||||||
if st.cc != nil {
|
if st.cc != nil {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/http2/hpack"
|
"golang.org/x/net/http2/hpack"
|
||||||
|
"golang.org/x/net/lex/httplex"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -632,12 +633,8 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
return nil, errClientConnUnusable
|
return nil, errClientConnUnusable
|
||||||
}
|
}
|
||||||
|
|
||||||
cs := cc.newStream()
|
|
||||||
cs.req = req
|
|
||||||
cs.trace = requestTrace(req)
|
|
||||||
hasBody := body != nil
|
|
||||||
|
|
||||||
// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
|
// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
|
||||||
|
var requestedGzip bool
|
||||||
if !cc.t.disableCompression() &&
|
if !cc.t.disableCompression() &&
|
||||||
req.Header.Get("Accept-Encoding") == "" &&
|
req.Header.Get("Accept-Encoding") == "" &&
|
||||||
req.Header.Get("Range") == "" &&
|
req.Header.Get("Range") == "" &&
|
||||||
|
@ -654,13 +651,24 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
// We don't request gzip if the request is for a range, since
|
// We don't request gzip if the request is for a range, since
|
||||||
// auto-decoding a portion of a gzipped document will just fail
|
// auto-decoding a portion of a gzipped document will just fail
|
||||||
// anyway. See https://golang.org/issue/8923
|
// anyway. See https://golang.org/issue/8923
|
||||||
cs.requestedGzip = true
|
requestedGzip = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,} (DATA is
|
// we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,} (DATA is
|
||||||
// sent by writeRequestBody below, along with any Trailers,
|
// sent by writeRequestBody below, along with any Trailers,
|
||||||
// again in form HEADERS{1}, CONTINUATION{0,})
|
// again in form HEADERS{1}, CONTINUATION{0,})
|
||||||
hdrs := cc.encodeHeaders(req, cs.requestedGzip, trailers, contentLen)
|
hdrs, err := cc.encodeHeaders(req, requestedGzip, trailers, contentLen)
|
||||||
|
if err != nil {
|
||||||
|
cc.mu.Unlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := cc.newStream()
|
||||||
|
cs.req = req
|
||||||
|
cs.trace = requestTrace(req)
|
||||||
|
hasBody := body != nil
|
||||||
|
cs.requestedGzip = requestedGzip
|
||||||
|
|
||||||
cc.wmu.Lock()
|
cc.wmu.Lock()
|
||||||
endStream := !hasBody && !hasTrailers
|
endStream := !hasBody && !hasTrailers
|
||||||
werr := cc.writeHeaders(cs.ID, endStream, hdrs)
|
werr := cc.writeHeaders(cs.ID, endStream, hdrs)
|
||||||
|
@ -936,7 +944,7 @@ type badStringError struct {
|
||||||
func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) }
|
func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) }
|
||||||
|
|
||||||
// requires cc.mu be held.
|
// requires cc.mu be held.
|
||||||
func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) []byte {
|
func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) {
|
||||||
cc.hbuf.Reset()
|
cc.hbuf.Reset()
|
||||||
|
|
||||||
host := req.Host
|
host := req.Host
|
||||||
|
@ -944,6 +952,20 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
|
||||||
host = req.URL.Host
|
host = req.URL.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for any invalid headers and return an error before we
|
||||||
|
// potentially pollute our hpack state. (We want to be able to
|
||||||
|
// continue to reuse the hpack encoder for future requests)
|
||||||
|
for k, vv := range req.Header {
|
||||||
|
if !httplex.ValidHeaderFieldName(k) {
|
||||||
|
return nil, fmt.Errorf("invalid HTTP header name %q", k)
|
||||||
|
}
|
||||||
|
for _, v := range vv {
|
||||||
|
if !httplex.ValidHeaderFieldValue(v) {
|
||||||
|
return nil, fmt.Errorf("invalid HTTP header value %q for header %q", v, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 8.1.2.3 Request Pseudo-Header Fields
|
// 8.1.2.3 Request Pseudo-Header Fields
|
||||||
// The :path pseudo-header field includes the path and query parts of the
|
// The :path pseudo-header field includes the path and query parts of the
|
||||||
// target URI (the path-absolute production and optionally a '?' character
|
// target URI (the path-absolute production and optionally a '?' character
|
||||||
|
@ -1000,7 +1022,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
|
||||||
if !didUA {
|
if !didUA {
|
||||||
cc.writeHeader("user-agent", defaultUserAgent)
|
cc.writeHeader("user-agent", defaultUserAgent)
|
||||||
}
|
}
|
||||||
return cc.hbuf.Bytes()
|
return cc.hbuf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldSendReqContentLength reports whether the http2.Transport should send
|
// shouldSendReqContentLength reports whether the http2.Transport should send
|
||||||
|
|
|
@ -1666,6 +1666,68 @@ func TestTransportRejectsConnHeaders(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// golang.org/issue/14048
|
||||||
|
func TestTransportFailsOnInvalidHeaders(t *testing.T) {
|
||||||
|
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var got []string
|
||||||
|
for k := range r.Header {
|
||||||
|
got = append(got, k)
|
||||||
|
}
|
||||||
|
sort.Strings(got)
|
||||||
|
w.Header().Set("Got-Header", strings.Join(got, ","))
|
||||||
|
}, optOnlyServer)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
tests := [...]struct {
|
||||||
|
h http.Header
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
0: {
|
||||||
|
h: http.Header{"with space": {"foo"}},
|
||||||
|
wantErr: `invalid HTTP header name "with space"`,
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
h: http.Header{"name": {"Брэд"}},
|
||||||
|
wantErr: "", // okay
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
h: http.Header{"имя": {"Brad"}},
|
||||||
|
wantErr: `invalid HTTP header name "имя"`,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
h: http.Header{"foo": {"foo\x01bar"}},
|
||||||
|
wantErr: `invalid HTTP header value "foo\x01bar" for header "foo"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := &Transport{TLSClientConfig: tlsConfigInsecure}
|
||||||
|
defer tr.CloseIdleConnections()
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
req, _ := http.NewRequest("GET", st.ts.URL, nil)
|
||||||
|
req.Header = tt.h
|
||||||
|
res, err := tr.RoundTrip(req)
|
||||||
|
var bad bool
|
||||||
|
if tt.wantErr == "" {
|
||||||
|
if err != nil {
|
||||||
|
bad = true
|
||||||
|
t.Errorf("case %d: error = %v; want no error", i, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(fmt.Sprint(err), tt.wantErr) {
|
||||||
|
bad = true
|
||||||
|
t.Errorf("case %d: error = %v; want error %q", i, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if bad {
|
||||||
|
t.Logf("case %d: server got headers %q", i, res.Header.Get("Got-Header"))
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tests that gzipReader doesn't crash on a second Read call following
|
// Tests that gzipReader doesn't crash on a second Read call following
|
||||||
// the first Read call's gzip.NewReader returning an error.
|
// the first Read call's gzip.NewReader returning an error.
|
||||||
func TestGzipReader_DoubleReadCrash(t *testing.T) {
|
func TestGzipReader_DoubleReadCrash(t *testing.T) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/http2/hpack"
|
"golang.org/x/net/http2/hpack"
|
||||||
|
"golang.org/x/net/lex/httplex"
|
||||||
)
|
)
|
||||||
|
|
||||||
// writeFramer is implemented by any type that is used to write frames.
|
// writeFramer is implemented by any type that is used to write frames.
|
||||||
|
@ -240,14 +241,15 @@ func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) {
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
vv := h[k]
|
vv := h[k]
|
||||||
k = lowerHeader(k)
|
k = lowerHeader(k)
|
||||||
if !validHeaderFieldName(k) {
|
if !validWireHeaderFieldName(k) {
|
||||||
// TODO: return an error? golang.org/issue/14048
|
// Skip it as backup paranoia. Per
|
||||||
// For now just omit it.
|
// golang.org/issue/14048, these should
|
||||||
|
// already be rejected at a higher level.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
isTE := k == "transfer-encoding"
|
isTE := k == "transfer-encoding"
|
||||||
for _, v := range vv {
|
for _, v := range vv {
|
||||||
if !validHeaderFieldValue(v) {
|
if !httplex.ValidHeaderFieldValue(v) {
|
||||||
// TODO: return an error? golang.org/issue/14048
|
// TODO: return an error? golang.org/issue/14048
|
||||||
// For now just omit it.
|
// For now just omit it.
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -0,0 +1,312 @@
|
||||||
|
// Copyright 2016 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.
|
||||||
|
|
||||||
|
// Package httplex contains rules around lexical matters of various
|
||||||
|
// HTTP-related specifications.
|
||||||
|
//
|
||||||
|
// This package is shared by the standard library (which vendors it)
|
||||||
|
// and x/net/http2. It comes with no API stability promise.
|
||||||
|
package httplex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var isTokenTable = [127]bool{
|
||||||
|
'!': true,
|
||||||
|
'#': true,
|
||||||
|
'$': true,
|
||||||
|
'%': true,
|
||||||
|
'&': true,
|
||||||
|
'\'': true,
|
||||||
|
'*': true,
|
||||||
|
'+': true,
|
||||||
|
'-': true,
|
||||||
|
'.': true,
|
||||||
|
'0': true,
|
||||||
|
'1': true,
|
||||||
|
'2': true,
|
||||||
|
'3': true,
|
||||||
|
'4': true,
|
||||||
|
'5': true,
|
||||||
|
'6': true,
|
||||||
|
'7': true,
|
||||||
|
'8': true,
|
||||||
|
'9': true,
|
||||||
|
'A': true,
|
||||||
|
'B': true,
|
||||||
|
'C': true,
|
||||||
|
'D': true,
|
||||||
|
'E': true,
|
||||||
|
'F': true,
|
||||||
|
'G': true,
|
||||||
|
'H': true,
|
||||||
|
'I': true,
|
||||||
|
'J': true,
|
||||||
|
'K': true,
|
||||||
|
'L': true,
|
||||||
|
'M': true,
|
||||||
|
'N': true,
|
||||||
|
'O': true,
|
||||||
|
'P': true,
|
||||||
|
'Q': true,
|
||||||
|
'R': true,
|
||||||
|
'S': true,
|
||||||
|
'T': true,
|
||||||
|
'U': true,
|
||||||
|
'W': true,
|
||||||
|
'V': true,
|
||||||
|
'X': true,
|
||||||
|
'Y': true,
|
||||||
|
'Z': true,
|
||||||
|
'^': true,
|
||||||
|
'_': true,
|
||||||
|
'`': true,
|
||||||
|
'a': true,
|
||||||
|
'b': true,
|
||||||
|
'c': true,
|
||||||
|
'd': true,
|
||||||
|
'e': true,
|
||||||
|
'f': true,
|
||||||
|
'g': true,
|
||||||
|
'h': true,
|
||||||
|
'i': true,
|
||||||
|
'j': true,
|
||||||
|
'k': true,
|
||||||
|
'l': true,
|
||||||
|
'm': true,
|
||||||
|
'n': true,
|
||||||
|
'o': true,
|
||||||
|
'p': true,
|
||||||
|
'q': true,
|
||||||
|
'r': true,
|
||||||
|
's': true,
|
||||||
|
't': true,
|
||||||
|
'u': true,
|
||||||
|
'v': true,
|
||||||
|
'w': true,
|
||||||
|
'x': true,
|
||||||
|
'y': true,
|
||||||
|
'z': true,
|
||||||
|
'|': true,
|
||||||
|
'~': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsTokenRune(r rune) bool {
|
||||||
|
i := int(r)
|
||||||
|
return i < len(isTokenTable) && isTokenTable[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNotToken(r rune) bool {
|
||||||
|
return !IsTokenRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeaderValuesContainsToken reports whether any string in values
|
||||||
|
// contains the provided token, ASCII case-insensitively.
|
||||||
|
func HeaderValuesContainsToken(values []string, token string) bool {
|
||||||
|
for _, v := range values {
|
||||||
|
if headerValueContainsToken(v, token) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isOWS reports whether b is an optional whitespace byte, as defined
|
||||||
|
// by RFC 7230 section 3.2.3.
|
||||||
|
func isOWS(b byte) bool { return b == ' ' || b == '\t' }
|
||||||
|
|
||||||
|
// trimOWS returns x with all optional whitespace removes from the
|
||||||
|
// beginning and end.
|
||||||
|
func trimOWS(x string) string {
|
||||||
|
// TODO: consider using strings.Trim(x, " \t") instead,
|
||||||
|
// if and when it's fast enough. See issue 10292.
|
||||||
|
// But this ASCII-only code will probably always beat UTF-8
|
||||||
|
// aware code.
|
||||||
|
for len(x) > 0 && isOWS(x[0]) {
|
||||||
|
x = x[1:]
|
||||||
|
}
|
||||||
|
for len(x) > 0 && isOWS(x[len(x)-1]) {
|
||||||
|
x = x[:len(x)-1]
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// headerValueContainsToken reports whether v (assumed to be a
|
||||||
|
// 0#element, in the ABNF extension described in RFC 7230 section 7)
|
||||||
|
// contains token amongst its comma-separated tokens, ASCII
|
||||||
|
// case-insensitively.
|
||||||
|
func headerValueContainsToken(v string, token string) bool {
|
||||||
|
v = trimOWS(v)
|
||||||
|
if comma := strings.IndexByte(v, ','); comma != -1 {
|
||||||
|
return tokenEqual(trimOWS(v[:comma]), token) || headerValueContainsToken(v[comma+1:], token)
|
||||||
|
}
|
||||||
|
return tokenEqual(v, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lowerASCII returns the ASCII lowercase version of b.
|
||||||
|
func lowerASCII(b byte) byte {
|
||||||
|
if 'A' <= b && b <= 'Z' {
|
||||||
|
return b + ('a' - 'A')
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenEqual reports whether t1 and t2 are equal, ASCII case-insensitively.
|
||||||
|
func tokenEqual(t1, t2 string) bool {
|
||||||
|
if len(t1) != len(t2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, b := range t1 {
|
||||||
|
if b >= utf8.RuneSelf {
|
||||||
|
// No UTF-8 or non-ASCII allowed in tokens.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lowerASCII(byte(b)) != lowerASCII(t2[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLWS reports whether b is linear white space, according
|
||||||
|
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
|
||||||
|
// LWS = [CRLF] 1*( SP | HT )
|
||||||
|
func isLWS(b byte) bool { return b == ' ' || b == '\t' }
|
||||||
|
|
||||||
|
// isCTL reports whether b is a control byte, according
|
||||||
|
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
|
||||||
|
// CTL = <any US-ASCII control character
|
||||||
|
// (octets 0 - 31) and DEL (127)>
|
||||||
|
func isCTL(b byte) bool {
|
||||||
|
const del = 0x7f // a CTL
|
||||||
|
return b < ' ' || b == del
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidHeaderFieldName reports whether v is a valid HTTP/1.x header name.
|
||||||
|
// HTTP/2 imposes the additional restriction that uppercase ASCII
|
||||||
|
// letters are not allowed.
|
||||||
|
//
|
||||||
|
// RFC 7230 says:
|
||||||
|
// header-field = field-name ":" OWS field-value OWS
|
||||||
|
// field-name = token
|
||||||
|
// token = 1*tchar
|
||||||
|
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
|
||||||
|
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
||||||
|
func ValidHeaderFieldName(v string) bool {
|
||||||
|
if len(v) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range v {
|
||||||
|
if !IsTokenRune(r) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidHostHeader reports whether h is a valid host header.
|
||||||
|
func ValidHostHeader(h string) bool {
|
||||||
|
// The latest spec is actually this:
|
||||||
|
//
|
||||||
|
// http://tools.ietf.org/html/rfc7230#section-5.4
|
||||||
|
// Host = uri-host [ ":" port ]
|
||||||
|
//
|
||||||
|
// Where uri-host is:
|
||||||
|
// http://tools.ietf.org/html/rfc3986#section-3.2.2
|
||||||
|
//
|
||||||
|
// But we're going to be much more lenient for now and just
|
||||||
|
// search for any byte that's not a valid byte in any of those
|
||||||
|
// expressions.
|
||||||
|
for i := 0; i < len(h); i++ {
|
||||||
|
if !validHostByte[h[i]] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// See the validHostHeader comment.
|
||||||
|
var validHostByte = [256]bool{
|
||||||
|
'0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, '7': true,
|
||||||
|
'8': true, '9': true,
|
||||||
|
|
||||||
|
'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true, 'h': true,
|
||||||
|
'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true, 'p': true,
|
||||||
|
'q': true, 'r': true, 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
|
||||||
|
'y': true, 'z': true,
|
||||||
|
|
||||||
|
'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true, 'H': true,
|
||||||
|
'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true, 'O': true, 'P': true,
|
||||||
|
'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
|
||||||
|
'Y': true, 'Z': true,
|
||||||
|
|
||||||
|
'!': true, // sub-delims
|
||||||
|
'$': true, // sub-delims
|
||||||
|
'%': true, // pct-encoded (and used in IPv6 zones)
|
||||||
|
'&': true, // sub-delims
|
||||||
|
'(': true, // sub-delims
|
||||||
|
')': true, // sub-delims
|
||||||
|
'*': true, // sub-delims
|
||||||
|
'+': true, // sub-delims
|
||||||
|
',': true, // sub-delims
|
||||||
|
'-': true, // unreserved
|
||||||
|
'.': true, // unreserved
|
||||||
|
':': true, // IPv6address + Host expression's optional port
|
||||||
|
';': true, // sub-delims
|
||||||
|
'=': true, // sub-delims
|
||||||
|
'[': true,
|
||||||
|
'\'': true, // sub-delims
|
||||||
|
']': true,
|
||||||
|
'_': true, // unreserved
|
||||||
|
'~': true, // unreserved
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidHeaderFieldValue reports whether v is a valid "field-value" according to
|
||||||
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 :
|
||||||
|
//
|
||||||
|
// message-header = field-name ":" [ field-value ]
|
||||||
|
// field-value = *( field-content | LWS )
|
||||||
|
// field-content = <the OCTETs making up the field-value
|
||||||
|
// and consisting of either *TEXT or combinations
|
||||||
|
// of token, separators, and quoted-string>
|
||||||
|
//
|
||||||
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 :
|
||||||
|
//
|
||||||
|
// TEXT = <any OCTET except CTLs,
|
||||||
|
// but including LWS>
|
||||||
|
// LWS = [CRLF] 1*( SP | HT )
|
||||||
|
// CTL = <any US-ASCII control character
|
||||||
|
// (octets 0 - 31) and DEL (127)>
|
||||||
|
//
|
||||||
|
// RFC 7230 says:
|
||||||
|
// field-value = *( field-content / obs-fold )
|
||||||
|
// obj-fold = N/A to http2, and deprecated
|
||||||
|
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||||
|
// field-vchar = VCHAR / obs-text
|
||||||
|
// obs-text = %x80-FF
|
||||||
|
// VCHAR = "any visible [USASCII] character"
|
||||||
|
//
|
||||||
|
// http2 further says: "Similarly, HTTP/2 allows header field values
|
||||||
|
// that are not valid. While most of the values that can be encoded
|
||||||
|
// will not alter header field parsing, carriage return (CR, ASCII
|
||||||
|
// 0xd), line feed (LF, ASCII 0xa), and the zero character (NUL, ASCII
|
||||||
|
// 0x0) might be exploited by an attacker if they are translated
|
||||||
|
// verbatim. Any request or response that contains a character not
|
||||||
|
// permitted in a header field value MUST be treated as malformed
|
||||||
|
// (Section 8.1.2.6). Valid characters are defined by the
|
||||||
|
// field-content ABNF rule in Section 3.2 of [RFC7230]."
|
||||||
|
//
|
||||||
|
// This function does not (yet?) properly handle the rejection of
|
||||||
|
// strings that begin or end with SP or HTAB.
|
||||||
|
func ValidHeaderFieldValue(v string) bool {
|
||||||
|
for i := 0; i < len(v); i++ {
|
||||||
|
b := v[i]
|
||||||
|
if isCTL(b) && !isLWS(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright 2009 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.
|
||||||
|
|
||||||
|
package httplex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isChar(c rune) bool { return c <= 127 }
|
||||||
|
|
||||||
|
func isCtl(c rune) bool { return c <= 31 || c == 127 }
|
||||||
|
|
||||||
|
func isSeparator(c rune) bool {
|
||||||
|
switch c {
|
||||||
|
case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsToken(t *testing.T) {
|
||||||
|
for i := 0; i <= 130; i++ {
|
||||||
|
r := rune(i)
|
||||||
|
expected := isChar(r) && !isCtl(r) && !isSeparator(r)
|
||||||
|
if IsTokenRune(r) != expected {
|
||||||
|
t.Errorf("isToken(0x%x) = %v", r, !expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderValuesContainsToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
vals []string
|
||||||
|
token string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
vals: []string{"foo"},
|
||||||
|
token: "foo",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"bar", "foo"},
|
||||||
|
token: "foo",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"foo"},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"foo"},
|
||||||
|
token: "bar",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{" foo "},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"foo,bar"},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"bar,foo,bar"},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"bar , foo"},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"foo ,bar "},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"bar, foo ,bar"},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vals: []string{"bar , foo"},
|
||||||
|
token: "FOO",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := HeaderValuesContainsToken(tt.vals, tt.token)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("headerValuesContainsToken(%q, %q) = %v; want %v", tt.vals, tt.token, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче