Skip to content

Commit

Permalink
optimize: filter invalid char in header (cloudwego#1039)
Browse files Browse the repository at this point in the history
  • Loading branch information
welkeyever authored Jan 10, 2024
1 parent 1f93aaa commit 119a744
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 32 deletions.
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ require (
github.com/cloudwego/netpoll v0.5.0
github.com/fsnotify/fsnotify v1.5.4
github.com/tidwall/gjson v1.14.4
golang.org/x/net v0.0.0-20190311183353-d8887717615a
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
google.golang.org/protobuf v1.27.1
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,13 @@ golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5P
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
Expand Down
2 changes: 2 additions & 0 deletions internal/bytesconv/bytesconv_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ const (
QuotedPathShouldEscapeTable = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x00\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"
ValidCookieValueTable = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
ValidHeaderFieldValueTable = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"
NewlineToSpaceTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t \v\f \x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
ValidHeaderFieldNameTable = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x01\x01\x01\x00\x00\x01\x01\x00\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
)
61 changes: 61 additions & 0 deletions internal/bytesconv/bytesconv_table_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,65 @@ func main() {
return a
}()

newlineToSpaceTable := func() [256]byte {
var a [256]byte
for i := 0; i < 256; i++ {
c := byte(i)
if c == '\r' || c == '\n' {
c = ' '
}
a[i] = c
}
return a
}()

validHeaderFieldNameTable := func() [256]byte {
// The implementation here is equal to httpguts ValidHeaderFieldName(string)
// see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
//
// RFC 7230 says:
// header-field = field-name ":" OWS field-value OWS
// field-name = token
// token = 1*tchar
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
var a [256]byte
for i := 0; i < 256; i++ {
a[i] = 0
}

a['!'] = 1
a['#'] = 1
a['$'] = 1
a['%'] = 1
a['&'] = 1
a['\''] = 1
a['*'] = 1
a['+'] = 1
a['-'] = 1
a['.'] = 1
a['^'] = 1
a['_'] = 1
a['`'] = 1
a['|'] = 1
a['~'] = 1

// ALPHA
for i := int('a'); i <= int('z'); i++ {
a[i] = 1
}
for i := int('A'); i <= int('Z'); i++ {
a[i] = 1
}

// DIGIT
for i := int('0'); i <= int('9'); i++ {
a[i] = 1
}

return a
}()

w := new(bytes.Buffer)
w.WriteString(pre)
fmt.Fprintf(w, "const (\n")
Expand All @@ -188,6 +247,8 @@ func main() {
fmt.Fprintf(w, "\tQuotedPathShouldEscapeTable = %q\n", quotedPathShouldEscapeTable)
fmt.Fprintf(w, "\tValidCookieValueTable = %q\n", validCookieValueTable)
fmt.Fprintf(w, "\tValidHeaderFieldValueTable = %q\n", validHeaderFieldValueTable)
fmt.Fprintf(w, "\tNewlineToSpaceTable = %q\n", newlineToSpaceTable)
fmt.Fprintf(w, "\tValidHeaderFieldNameTable = %q\n", validHeaderFieldNameTable)
fmt.Fprintf(w, ")\n")

if err := ioutil.WriteFile("bytesconv_table.go", w.Bytes(), 0o660); err != nil {
Expand Down
64 changes: 54 additions & 10 deletions internal/bytesconv/bytesconv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ package bytesconv

import (
"net/url"
"strings"
"testing"
"time"

"github.com/cloudwego/hertz/pkg/common/bytebufferpool"
"github.com/cloudwego/hertz/pkg/common/test/assert"
"github.com/cloudwego/hertz/pkg/common/test/mock"
"github.com/cloudwego/hertz/pkg/network"
"golang.org/x/net/http/httpguts"
)

func TestAppendDate(t *testing.T) {
Expand Down Expand Up @@ -185,21 +185,65 @@ func TestParseHTTPDate(t *testing.T) {
}
}

func TestValidHeaderFieldValueTable(t *testing.T) {
// For test only, but it will import golang.org/x/net/http.
// So comment out all this code. Keep this for the full context.
//func TestValidHeaderFieldValueTable(t *testing.T) {
// t.Parallel()
//
// // Test all characters
// allBytes := make([]byte, 0)
// for i := 0; i < 256; i++ {
// allBytes = append(allBytes, byte(i))
// }
// for _, s := range allBytes {
// ss := []byte{s}
// expectedS := httpguts.ValidHeaderFieldValue(string(ss))
// res := func() bool {
// return ValidHeaderFieldValueTable[s] != 0
// }()
//
// assert.DeepEqual(t, expectedS, res)
// }
//}

func TestNewlineToSpaceTable(t *testing.T) {
t.Parallel()

// Test all characters
allBytes := make([]byte, 0)
for i := 0; i < 256; i++ {
allBytes = append(allBytes, byte(i))
}
for _, s := range allBytes {
ss := []byte{s}
expectedS := httpguts.ValidHeaderFieldValue(string(ss))
res := func() bool {
return ValidHeaderFieldValueTable[s] != 0
}()

assert.DeepEqual(t, expectedS, res)
headerNewlineToSpace := strings.NewReplacer("\n", " ", "\r", " ")

expectedS := headerNewlineToSpace.Replace(string(allBytes))

res := make([]byte, len(allBytes))
copy(res, allBytes)
for i := 0; i < len(res); i++ {
res[i] = NewlineToSpaceTable[res[i]]
}

assert.DeepEqual(t, expectedS, string(res))
}

// For test only, but it will import golang.org/x/net/http.
// So comment out all this code. Keep this for the full context.
//func TestValidHeaderFieldNameTable(t *testing.T) {
// t.Parallel()
//
// // Test all characters
// allBytes := make([]byte, 0)
// for i := 0; i < 256; i++ {
// allBytes = append(allBytes, byte(i))
// }
// for _, s := range allBytes {
// ss := []byte{s}
// expectedS := httpguts.ValidHeaderFieldName(string(ss))
// res := func() bool {
// return ValidHeaderFieldNameTable[s] != 0
// }()
//
// assert.DeepEqual(t, expectedS, res)
// }
//}
68 changes: 57 additions & 11 deletions internal/bytesconv/bytesconv_timing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,83 @@
package bytesconv

import (
"strings"
"testing"

"golang.org/x/net/http/httpguts"
)

func BenchmarkValidHeaderFiledValueTable(b *testing.B) {
// For test only, but it will import golang.org/x/net/http.
// So comment out all this code. Keep this for the full context.
//func BenchmarkValidHeaderFiledValueTable(b *testing.B) {
// // Test all characters
// allBytes := make([]string, 0)
// for i := 0; i < 256; i++ {
// allBytes = append(allBytes, string([]byte{byte(i)}))
// }
//
// for i := 0; i < b.N; i++ {
// for _, s := range allBytes {
// _ = httpguts.ValidHeaderFieldValue(s)
// }
// }
//}

func BenchmarkValidHeaderFiledValueTableHertz(b *testing.B) {
// Test all characters
allBytes := make([]string, 0)
allBytes := make([]byte, 0)
for i := 0; i < 256; i++ {
allBytes = append(allBytes, string([]byte{byte(i)}))
allBytes = append(allBytes, byte(i))
}

for i := 0; i < b.N; i++ {
for _, s := range allBytes {
_ = httpguts.ValidHeaderFieldValue(s)
_ = func() bool {
return ValidHeaderFieldValueTable[s] != 0
}()
}
}
}

func BenchmarkValidHeaderFiledValueTableHertz(b *testing.B) {
func BenchmarkNewlineToSpace(b *testing.B) {
// Test all characters
allBytes := make([]byte, 0)
for i := 0; i < 256; i++ {
allBytes = append(allBytes, byte(i))
}
headerNewlineToSpace := strings.NewReplacer("\n", " ", "\r", " ")

for i := 0; i < b.N; i++ {
for _, s := range allBytes {
_ = func() bool {
return ValidHeaderFieldValueTable[s] != 0
}()
_ = headerNewlineToSpace.Replace(string(allBytes))
}
}

func BenchmarkNewlineToSpaceHertz01(b *testing.B) {
// Test all characters
allBytes := make([]byte, 0)
for i := 0; i < 256; i++ {
allBytes = append(allBytes, byte(i))
}

for i := 0; i < b.N; i++ {
filteredVal := make([]byte, 0, len(allBytes))
for i := 0; i < len(allBytes); i++ {
filteredVal = append(filteredVal, NewlineToSpaceTable[allBytes[i]])
}
_ = filteredVal
}
}

func BenchmarkNewlineToSpaceHertz02(b *testing.B) {
// Test all characters
allBytes := make([]byte, 0)
for i := 0; i < 256; i++ {
allBytes = append(allBytes, byte(i))
}

for i := 0; i < b.N; i++ {
filteredVal := make([]byte, len(allBytes))
copy(filteredVal, allBytes)
for ii := 0; ii < len(allBytes); ii++ {
filteredVal[ii] = NewlineToSpaceTable[filteredVal[ii]]
}
}
}
18 changes: 17 additions & 1 deletion pkg/protocol/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -1657,12 +1657,28 @@ func (h *ResponseHeader) GetAll(key string) []string {
}

func appendHeaderLine(dst, key, value []byte) []byte {
for _, k := range key {
// if header field contains invalid key, just skip it.
if bytesconv.ValidHeaderFieldNameTable[k] == 0 {
return dst
}
}
dst = append(dst, key...)
dst = append(dst, bytestr.StrColonSpace...)
dst = append(dst, value...)
dst = append(dst, newlineToSpace(value)...)
return append(dst, bytestr.StrCRLF...)
}

// newlineToSpace will return a copy of the original byte slice.
func newlineToSpace(val []byte) []byte {
filteredVal := make([]byte, len(val))
copy(filteredVal, val)
for i := 0; i < len(filteredVal); i++ {
filteredVal[i] = bytesconv.NewlineToSpaceTable[filteredVal[i]]
}
return filteredVal
}

func UpdateServerDate() {
refreshServerDate()
go func() {
Expand Down
2 changes: 1 addition & 1 deletion pkg/protocol/http1/resp/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func ReadHeader(h *protocol.ResponseHeader, r network.Reader) error {
}
}

// Write writes response header to w.
// WriteHeader writes response header to w.
func WriteHeader(h *protocol.ResponseHeader, w network.Writer) error {
header := h.Header()
h.SetHeaderLength(len(header))
Expand Down
6 changes: 0 additions & 6 deletions pkg/protocol/http1/resp/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,9 +820,3 @@ func TestResponseReadBodyStreamBadTrailer(t *testing.T) {
testResponseReadBodyStreamBadTrailer(t, resp, "HTTP/1.1 300 OK\r\nTransfer-Encoding: chunked\r\nContent-Type: bar\r\n\r\n5\r\n56789\r\n0\r\ncontent-type: bar\r\n\r\n")
testResponseReadBodyStreamBadTrailer(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\nproxy-connection: bar2\r\n\r\n")
}

func TestResponseString(t *testing.T) {
resp := protocol.Response{}
resp.Header.Set("Location", "foo\r\nSet-Cookie: SESSIONID=MaliciousValue\r\n")
assert.True(t, strings.Contains(GetHTTP1Response(&resp).String(), "Location: foo\r\nSet-Cookie: SESSIONID=MaliciousValue\r\n"))
}

0 comments on commit 119a744

Please sign in to comment.