mirror of
https://github.com/valyala/fasthttp.git
synced 2026-06-14 15:56:44 +03:00
c4569c5fbb
* feat: enhance performance * fix: improve request URI parsing condition * feat: validate HTTP date parsing and optimize status code length calculation * Address parsing and lint issues * chore: update Go version to 1.24.x in CI configuration * feat: enhance HTTP date parsing and request URI handling * refactor: optimize month and day name parsing using bitwise operations * refactor: replace cookie token comparison with case insensitive function and streamline request URI parsing * refactor: streamline request body handling and simplify request URI assignment * chore: update Go version to 1.25.x in CI configuration * feat: add fuzz testing for HTTP date parsing to improve robustness * refactor: avoid unused return values in HTTP date parsing benchmarks * refactor: update HTTP date parsing to use http.TimeFormat for consistency
475 lines
13 KiB
Go
475 lines
13 KiB
Go
package fasthttp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"html"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/valyala/bytebufferpool"
|
|
)
|
|
|
|
func TestAppendQuotedArg(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Sync with url.QueryEscape
|
|
allcases := make([]byte, 256)
|
|
for i := range 256 {
|
|
allcases[i] = byte(i)
|
|
}
|
|
res := string(AppendQuotedArg(nil, allcases))
|
|
expect := url.QueryEscape(string(allcases))
|
|
if res != expect {
|
|
t.Fatalf("unexpected string %q. Expecting %q.", res, expect)
|
|
}
|
|
}
|
|
|
|
func TestAppendHTMLEscape(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Sync with html.EscapeString
|
|
allcases := make([]byte, 256)
|
|
for i := range 256 {
|
|
allcases[i] = byte(i)
|
|
}
|
|
res := string(AppendHTMLEscape(nil, string(allcases)))
|
|
expect := html.EscapeString(string(allcases))
|
|
if res != expect {
|
|
t.Fatalf("unexpected string %q. Expecting %q.", res, expect)
|
|
}
|
|
|
|
testAppendHTMLEscape(t, "", "")
|
|
testAppendHTMLEscape(t, "<", "<")
|
|
testAppendHTMLEscape(t, "a", "a")
|
|
testAppendHTMLEscape(t, `><"''`, "><"''")
|
|
testAppendHTMLEscape(t, "fo<b x='ss'>a</b>xxx", "fo<b x='ss'>a</b>xxx")
|
|
}
|
|
|
|
func testAppendHTMLEscape(t *testing.T, s, expectedS string) {
|
|
buf := AppendHTMLEscapeBytes(nil, []byte(s))
|
|
if string(buf) != expectedS {
|
|
t.Fatalf("unexpected html-escaped string %q. Expecting %q. Original string %q", buf, expectedS, s)
|
|
}
|
|
}
|
|
|
|
func TestParseIPv4(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testParseIPv4(t, net.IP{0}, "0.0.0.0", true)
|
|
testParseIPv4(t, nil, "0.0.0.0", true)
|
|
testParseIPv4(t, net.IP{0, 0, 0, 0, 0}, "0.0.0.0", true)
|
|
testParseIPv4(t, nil, "255.255.255.255", true)
|
|
testParseIPv4(t, nil, "123.45.67.89", true)
|
|
|
|
// ipv6 shouldn't work
|
|
testParseIPv4(t, nil, "2001:4860:0:2001::68", false)
|
|
|
|
// invalid ip
|
|
testParseIPv4(t, nil, "", false)
|
|
testParseIPv4(t, nil, "foobar", false)
|
|
testParseIPv4(t, nil, "1.2.3", false)
|
|
testParseIPv4(t, nil, "123.456.789.11", false)
|
|
testParseIPv4(t, nil, "b.1.2.3", false)
|
|
testParseIPv4(t, nil, "1.2.3.b", false)
|
|
testParseIPv4(t, nil, "1.2.3.456", false)
|
|
}
|
|
|
|
func testParseIPv4(t *testing.T, dst net.IP, ipStr string, isValid bool) {
|
|
ip, err := ParseIPv4(dst, []byte(ipStr))
|
|
if isValid {
|
|
if err != nil {
|
|
t.Fatalf("unexpected error when parsing ip %q: %v", ipStr, err)
|
|
}
|
|
s := string(AppendIPv4(nil, ip))
|
|
if s != ipStr {
|
|
t.Fatalf("unexpected ip parsed %q. Expecting %q", s, ipStr)
|
|
}
|
|
} else if err == nil {
|
|
t.Fatalf("expecting error when parsing ip %q", ipStr)
|
|
}
|
|
}
|
|
|
|
func TestAppendIPv4(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testAppendIPv4(t, "0.0.0.0", true)
|
|
testAppendIPv4(t, "127.0.0.1", true)
|
|
testAppendIPv4(t, "8.8.8.8", true)
|
|
testAppendIPv4(t, "123.45.67.89", true)
|
|
|
|
// ipv6 shouldn't work
|
|
testAppendIPv4(t, "2001:4860:0:2001::68", false)
|
|
}
|
|
|
|
func testAppendIPv4(t *testing.T, ipStr string, isValid bool) {
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil {
|
|
t.Fatalf("cannot parse ip %q", ipStr)
|
|
}
|
|
s := string(AppendIPv4(nil, ip))
|
|
if isValid {
|
|
if s != ipStr {
|
|
t.Fatalf("unexpected ip %q. Expecting %q", s, ipStr)
|
|
}
|
|
} else {
|
|
ipStr = "non-v4 ip passed to AppendIPv4"
|
|
if s != ipStr {
|
|
t.Fatalf("unexpected ip %q. Expecting %q", s, ipStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testAppendUint(t *testing.T, n int) {
|
|
expectedS := strconv.Itoa(n)
|
|
s := AppendUint(nil, n)
|
|
if string(s) != expectedS {
|
|
t.Fatalf("unexpected uint %q. Expecting %q. n=%d", s, expectedS, n)
|
|
}
|
|
}
|
|
|
|
func testWriteHexInt(t *testing.T, n int, expectedS string) {
|
|
var w bytebufferpool.ByteBuffer
|
|
bw := bufio.NewWriter(&w)
|
|
if err := writeHexInt(bw, n); err != nil {
|
|
t.Fatalf("unexpected error when writing hex %x: %v", n, err)
|
|
}
|
|
if err := bw.Flush(); err != nil {
|
|
t.Fatalf("unexpected error when flushing hex %x: %v", n, err)
|
|
}
|
|
s := string(w.B)
|
|
if s != expectedS {
|
|
t.Fatalf("unexpected hex after writing %q. Expected %q", s, expectedS)
|
|
}
|
|
}
|
|
|
|
func TestReadHexIntError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testReadHexIntError(t, "")
|
|
testReadHexIntError(t, "ZZZ")
|
|
testReadHexIntError(t, "-123")
|
|
testReadHexIntError(t, "+434")
|
|
}
|
|
|
|
func testReadHexIntError(t *testing.T, s string) {
|
|
r := bytes.NewBufferString(s)
|
|
br := bufio.NewReader(r)
|
|
n, err := readHexInt(br)
|
|
if err == nil {
|
|
t.Fatalf("expecting error when reading hex int %q", s)
|
|
}
|
|
if n >= 0 {
|
|
t.Fatalf("unexpected hex value read %d for hex int %q. must be negative", n, s)
|
|
}
|
|
}
|
|
|
|
func testReadHexIntSuccess(t *testing.T, s string, expectedN int) {
|
|
r := bytes.NewBufferString(s)
|
|
br := bufio.NewReader(r)
|
|
n, err := readHexInt(br)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v. s=%q", err, s)
|
|
}
|
|
if n != expectedN {
|
|
t.Fatalf("unexpected hex int %d. Expected %d. s=%q", n, expectedN, s)
|
|
}
|
|
}
|
|
|
|
func TestAppendHTTPDate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
d := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
|
s := string(AppendHTTPDate(nil, d))
|
|
expectedS := "Tue, 10 Nov 2009 23:00:00 GMT"
|
|
if s != expectedS {
|
|
t.Fatalf("unexpected date %q. Expecting %q", s, expectedS)
|
|
}
|
|
|
|
b := []byte("prefix")
|
|
s = string(AppendHTTPDate(b, d))
|
|
if s[:len(b)] != string(b) {
|
|
t.Fatalf("unexpected prefix %q. Expecting %q", s[:len(b)], b)
|
|
}
|
|
s = s[len(b):]
|
|
if s != expectedS {
|
|
t.Fatalf("unexpected date %q. Expecting %q", s, expectedS)
|
|
}
|
|
}
|
|
|
|
func TestParseHTTPDateCompatibility(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
value string
|
|
hasError bool
|
|
roundTrip bool
|
|
}{
|
|
{name: "gmt-fast-path", value: "Tue, 10 Nov 2009 23:00:00 GMT", roundTrip: true},
|
|
{name: "epoch", value: "Thu, 01 Jan 1970 00:00:00 GMT", roundTrip: true},
|
|
{name: "year-boundary", value: "Fri, 31 Dec 1999 23:59:59 GMT", roundTrip: true},
|
|
{name: "leap-year", value: "Mon, 29 Feb 2016 12:34:56 GMT", roundTrip: true},
|
|
{name: "utc-fallback", value: "Tue, 10 Nov 2009 23:00:00 UTC", hasError: true},
|
|
{name: "mixedcase-weekday-month", value: "tUe, 10 nOv 2009 23:00:00 GMT"},
|
|
{name: "day-zero", value: "Tue, 00 Nov 2009 23:00:00 GMT", hasError: true},
|
|
{name: "invalid-day", value: "Tue, 31 Feb 2009 23:00:00 GMT", hasError: true},
|
|
{name: "invalid-weekday", value: "Xxx, 10 Nov 2009 23:00:00 GMT", hasError: true},
|
|
{name: "invalid-month", value: "Tue, 10 Foo 2009 23:00:00 GMT", hasError: true},
|
|
{name: "invalid-hour", value: "Tue, 10 Nov 2009 24:00:00 GMT", hasError: true},
|
|
{name: "invalid-minute", value: "Tue, 10 Nov 2009 23:60:00 GMT", hasError: true},
|
|
{name: "invalid-second", value: "Tue, 10 Nov 2009 23:00:60 GMT", hasError: true},
|
|
{name: "invalid-separator", value: "Tue 10 Nov 2009 23:00:00 GMT", hasError: true},
|
|
{name: "invalid-time-separator", value: "Tue, 10 Nov 2009 23-00-00 GMT", hasError: true},
|
|
{name: "non-leap-year", value: "Tue, 29 Feb 2019 23:00:00 GMT", hasError: true},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, gotErr := ParseHTTPDate([]byte(tc.value))
|
|
want, wantErr := time.Parse(http.TimeFormat, tc.value)
|
|
|
|
if (gotErr != nil) != (wantErr != nil) {
|
|
t.Fatalf("error mismatch for %q: ParseHTTPDate err=%v, ParseInLocation err=%v", tc.value, gotErr, wantErr)
|
|
}
|
|
if tc.hasError != (gotErr != nil) {
|
|
t.Fatalf("unexpected error state for %q: gotErr=%v, expectedError=%v", tc.value, gotErr, tc.hasError)
|
|
}
|
|
if gotErr != nil {
|
|
return
|
|
}
|
|
if !got.Equal(want) {
|
|
t.Fatalf("parsed time mismatch for %q: got=%v want=%v", tc.value, got, want)
|
|
}
|
|
if tc.roundTrip && got.Format(time.RFC1123) != tc.value {
|
|
t.Fatalf("unexpected formatted date %q. Expecting %q", got.Format(time.RFC1123), tc.value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkParseHTTPDate(b *testing.B) {
|
|
date := []byte("Tue, 10 Nov 2009 23:00:00 GMT")
|
|
|
|
b.Run("fast-path", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
_, _ = ParseHTTPDate(date)
|
|
}
|
|
})
|
|
|
|
b.Run("stdlib-only", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
s := string(date)
|
|
for range b.N {
|
|
_, _ = time.Parse(http.TimeFormat, s)
|
|
}
|
|
})
|
|
}
|
|
|
|
func FuzzParseHTTPDate(f *testing.F) {
|
|
// Seed corpus: valid RFC1123 dates.
|
|
seeds := []string{
|
|
"Tue, 10 Nov 2009 23:00:00 GMT",
|
|
"Thu, 01 Jan 1970 00:00:00 GMT",
|
|
"Fri, 31 Dec 1999 23:59:59 GMT",
|
|
"Mon, 29 Feb 2016 12:34:56 GMT",
|
|
"Sun, 06 Nov 1994 08:49:37 GMT",
|
|
// Invalid inputs to exercise rejection paths.
|
|
"Tue, 10 Nov 2009 23:00:00 UTC",
|
|
"Tue, 31 Feb 2009 23:00:00 GMT",
|
|
"Xxx, 10 Nov 2009 23:00:00 GMT",
|
|
"Tue, 00 Nov 2009 23:00:00 GMT",
|
|
"Tue, 10 Nov 2009 24:00:00 GMT",
|
|
"not a date at all",
|
|
"",
|
|
}
|
|
for _, s := range seeds {
|
|
f.Add(s)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, s string) {
|
|
b := []byte(s)
|
|
|
|
// Reference: time.Parse with http.TimeFormat is what ParseHTTPDate falls back to.
|
|
stdTime, stdErr := time.Parse(http.TimeFormat, s)
|
|
|
|
// The public API must always agree with time.Parse.
|
|
got, gotErr := ParseHTTPDate(b)
|
|
if (gotErr != nil) != (stdErr != nil) {
|
|
t.Fatalf("ParseHTTPDate error mismatch for %q: got err=%v, std err=%v", s, gotErr, stdErr)
|
|
}
|
|
if gotErr == nil && !got.Equal(stdTime) {
|
|
t.Fatalf("ParseHTTPDate time mismatch for %q: got=%v std=%v", s, got, stdTime)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseUintError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// empty string
|
|
testParseUintError(t, "")
|
|
|
|
// negative value
|
|
testParseUintError(t, "-123")
|
|
|
|
// non-num
|
|
testParseUintError(t, "foobar234")
|
|
|
|
// non-num chars at the end
|
|
testParseUintError(t, "123w")
|
|
|
|
// floating point num
|
|
testParseUintError(t, "1234.545")
|
|
|
|
// too big num
|
|
testParseUintError(t, "12345678901234567890")
|
|
testParseUintError(t, "1234567890123456789012")
|
|
}
|
|
|
|
func TestParseUfloatSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testParseUfloatSuccess(t, "0", 0)
|
|
testParseUfloatSuccess(t, "1.", 1.)
|
|
testParseUfloatSuccess(t, ".1", 0.1)
|
|
testParseUfloatSuccess(t, "123.456", 123.456)
|
|
testParseUfloatSuccess(t, "123", 123)
|
|
testParseUfloatSuccess(t, "1234e2", 1234e2)
|
|
testParseUfloatSuccess(t, "1234E-5", 1234e-5)
|
|
testParseUfloatSuccess(t, "1.234e+3", 1.234e+3)
|
|
testParseUfloatSuccess(t, "1234e23", 1234e23)
|
|
testParseUfloatSuccess(t, "1.234e+32", 1.234e+32)
|
|
testParseUfloatSuccess(t, "123456789123456789.987654321", 123456789123456789.987654321)
|
|
testParseUfloatSuccess(t, "1.23456789123456789987654321", 1.23456789123456789987654321)
|
|
testParseUfloatSuccess(t, "340282346638528859811704183484516925440", 340282346638528859811704183484516925440)
|
|
testParseUfloatSuccess(t, "00000000000000000001", 1)
|
|
}
|
|
|
|
func TestParseUfloatError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// empty num
|
|
testParseUfloatError(t, "")
|
|
|
|
// negative num
|
|
testParseUfloatError(t, "-123.53")
|
|
|
|
// non-num chars
|
|
testParseUfloatError(t, "123sdfsd")
|
|
testParseUfloatError(t, "sdsf234")
|
|
testParseUfloatError(t, "sdfdf")
|
|
|
|
// non-num chars in exponent
|
|
testParseUfloatError(t, "123e3s")
|
|
testParseUfloatError(t, "12.3e-op")
|
|
testParseUfloatError(t, "123E+SS5")
|
|
|
|
// duplicate point
|
|
testParseUfloatError(t, "1.3.4")
|
|
|
|
// duplicate exponent
|
|
testParseUfloatError(t, "123e5e6")
|
|
|
|
// missing exponent
|
|
testParseUfloatError(t, "123534e")
|
|
|
|
// negative number
|
|
testParseUfloatError(t, "-1")
|
|
testParseUfloatError(t, "-Inf")
|
|
}
|
|
|
|
func testParseUfloatError(t *testing.T, s string) {
|
|
n, err := ParseUfloat([]byte(s))
|
|
if err == nil {
|
|
t.Fatalf("Expecting error when parsing %q. obtained %f", s, n)
|
|
}
|
|
if n >= 0 {
|
|
t.Fatalf("Expecting negative num instead of %f when parsing %q", n, s)
|
|
}
|
|
}
|
|
|
|
func testParseUfloatSuccess(t *testing.T, s string, expectedF float64) {
|
|
f, err := ParseUfloat([]byte(s))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error when parsing %q: %v", s, err)
|
|
}
|
|
delta := f - expectedF
|
|
if delta < 0 {
|
|
delta = -delta
|
|
}
|
|
if delta > expectedF*1e-10 {
|
|
t.Fatalf("Unexpected value when parsing %q: %f. Expected %f", s, f, expectedF)
|
|
}
|
|
}
|
|
|
|
func testParseUintError(t *testing.T, s string) {
|
|
n, err := ParseUint([]byte(s))
|
|
if err == nil {
|
|
t.Fatalf("Expecting error when parsing %q. obtained %d", s, n)
|
|
}
|
|
if n >= 0 {
|
|
t.Fatalf("Unexpected n=%d when parsing %q. Expected negative num", n, s)
|
|
}
|
|
}
|
|
|
|
func testParseUintSuccess(t *testing.T, s string, expectedN int) {
|
|
n, err := ParseUint([]byte(s))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error when parsing %q: %v", s, err)
|
|
}
|
|
if n != expectedN {
|
|
t.Fatalf("Unexpected value %d. Expected %d. num=%q", n, expectedN, s)
|
|
}
|
|
}
|
|
|
|
func TestAppendUnquotedArg(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testAppendUnquotedArg(t, "", "")
|
|
testAppendUnquotedArg(t, "abc", "abc")
|
|
testAppendUnquotedArg(t, "тест.abc", "тест.abc")
|
|
testAppendUnquotedArg(t, "%D1%82%D0%B5%D1%81%D1%82%20%=&;:", "тест %=&;:")
|
|
}
|
|
|
|
func testAppendUnquotedArg(t *testing.T, s, expectedS string) {
|
|
// test appending to nil
|
|
result := AppendUnquotedArg(nil, []byte(s))
|
|
if string(result) != expectedS {
|
|
t.Fatalf("Unexpected AppendUnquotedArg(%q)=%q, want %q", s, result, expectedS)
|
|
}
|
|
|
|
// test appending to prefix
|
|
prefix := "prefix"
|
|
dst := []byte(prefix)
|
|
dst = AppendUnquotedArg(dst, []byte(s))
|
|
if !bytes.HasPrefix(dst, []byte(prefix)) {
|
|
t.Fatalf("Unexpected prefix for AppendUnquotedArg(%q)=%q, want %q", s, dst, prefix)
|
|
}
|
|
result = dst[len(prefix):]
|
|
if string(result) != expectedS {
|
|
t.Fatalf("Unexpected AppendUnquotedArg(%q)=%q, want %q", s, result, expectedS)
|
|
}
|
|
|
|
// test in-place appending
|
|
result = []byte(s)
|
|
result = AppendUnquotedArg(result[:0], result)
|
|
if string(result) != expectedS {
|
|
t.Fatalf("Unexpected AppendUnquotedArg(%q)=%q, want %q", s, result, expectedS)
|
|
}
|
|
|
|
// verify AppendQuotedArg <-> AppendUnquotedArg conversion
|
|
quotedS := AppendQuotedArg(nil, []byte(s))
|
|
unquotedS := AppendUnquotedArg(nil, quotedS)
|
|
if s != string(unquotedS) {
|
|
t.Fatalf("Unexpected AppendUnquotedArg(AppendQuotedArg(%q))=%q, want %q", s, unquotedS, s)
|
|
}
|
|
}
|