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
453 lines
10 KiB
Go
453 lines
10 KiB
Go
//go:generate go run bytesconv_table_gen.go
|
|
|
|
package fasthttp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// AppendHTMLEscape appends html-escaped s to dst and returns the extended dst.
|
|
func AppendHTMLEscape(dst []byte, s string) []byte {
|
|
var (
|
|
prev int
|
|
sub string
|
|
)
|
|
|
|
for i, n := 0, len(s); i < n; i++ {
|
|
sub = ""
|
|
switch s[i] {
|
|
case '&':
|
|
sub = "&"
|
|
case '<':
|
|
sub = "<"
|
|
case '>':
|
|
sub = ">"
|
|
case '"':
|
|
sub = """ // """ is shorter than """.
|
|
case '\'':
|
|
sub = "'" // "'" is shorter than "'" and apos was not in HTML until HTML5.
|
|
}
|
|
if sub != "" {
|
|
dst = append(dst, s[prev:i]...)
|
|
dst = append(dst, sub...)
|
|
prev = i + 1
|
|
}
|
|
}
|
|
return append(dst, s[prev:]...)
|
|
}
|
|
|
|
// AppendHTMLEscapeBytes appends html-escaped s to dst and returns
|
|
// the extended dst.
|
|
func AppendHTMLEscapeBytes(dst, s []byte) []byte {
|
|
return AppendHTMLEscape(dst, b2s(s))
|
|
}
|
|
|
|
// AppendIPv4 appends string representation of the given ip v4 to dst
|
|
// and returns the extended dst.
|
|
func AppendIPv4(dst []byte, ip net.IP) []byte {
|
|
ip = ip.To4()
|
|
if ip == nil {
|
|
return append(dst, "non-v4 ip passed to AppendIPv4"...)
|
|
}
|
|
|
|
dst = AppendUint(dst, int(ip[0]))
|
|
for i := 1; i < 4; i++ {
|
|
dst = append(dst, '.')
|
|
dst = AppendUint(dst, int(ip[i]))
|
|
}
|
|
return dst
|
|
}
|
|
|
|
var errEmptyIPStr = errors.New("empty ip address string")
|
|
|
|
var httpDateGMT = time.FixedZone("GMT", 0)
|
|
|
|
// ParseIPv4 parses ip address from ipStr into dst and returns the extended dst.
|
|
func ParseIPv4(dst net.IP, ipStr []byte) (net.IP, error) {
|
|
if len(ipStr) == 0 {
|
|
return dst, errEmptyIPStr
|
|
}
|
|
if len(dst) < net.IPv4len || len(dst) > net.IPv4len {
|
|
dst = make([]byte, net.IPv4len)
|
|
}
|
|
copy(dst, net.IPv4zero)
|
|
dst = dst.To4() // dst is always non-nil here
|
|
|
|
b := ipStr
|
|
for i := range 3 {
|
|
n := bytes.IndexByte(b, '.')
|
|
if n < 0 {
|
|
return dst, fmt.Errorf("cannot find dot in ipStr %q", ipStr)
|
|
}
|
|
octet, parsed, err := parseIPv4Octet(b[:n])
|
|
if err != nil {
|
|
if errors.Is(err, errIPv4PartTooLarge) {
|
|
return dst, fmt.Errorf("cannot parse ipStr %q: ip part cannot exceed 255: parsed %d", ipStr, parsed)
|
|
}
|
|
return dst, fmt.Errorf("cannot parse ipStr %q: %w", ipStr, err)
|
|
}
|
|
dst[i] = octet
|
|
b = b[n+1:]
|
|
}
|
|
octet, parsed, err := parseIPv4Octet(b)
|
|
if err != nil {
|
|
if errors.Is(err, errIPv4PartTooLarge) {
|
|
return dst, fmt.Errorf("cannot parse ipStr %q: ip part cannot exceed 255: parsed %d", ipStr, parsed)
|
|
}
|
|
return dst, fmt.Errorf("cannot parse ipStr %q: %w", ipStr, err)
|
|
}
|
|
dst[3] = octet
|
|
|
|
return dst, nil
|
|
}
|
|
|
|
// AppendHTTPDate appends HTTP-compliant (RFC1123) representation of date
|
|
// to dst and returns the extended dst.
|
|
func AppendHTTPDate(dst []byte, date time.Time) []byte {
|
|
dst = date.In(time.UTC).AppendFormat(dst, time.RFC1123)
|
|
copy(dst[len(dst)-3:], strGMT)
|
|
return dst
|
|
}
|
|
|
|
// ParseHTTPDate parses HTTP-compliant (RFC1123) date.
|
|
func ParseHTTPDate(date []byte) (time.Time, error) {
|
|
if t, ok := parseRFC1123DateGMT(date); ok {
|
|
return t, nil
|
|
}
|
|
return time.Parse(http.TimeFormat, b2s(date))
|
|
}
|
|
|
|
func parseRFC1123DateGMT(b []byte) (time.Time, bool) {
|
|
// Expects "Mon, 02 Jan 2006 15:04:05 GMT".
|
|
if len(b) != 29 {
|
|
return time.Time{}, false
|
|
}
|
|
if !isWeekday3(b[0], b[1], b[2]) {
|
|
return time.Time{}, false
|
|
}
|
|
if b[3] != ',' || b[4] != ' ' || b[7] != ' ' || b[11] != ' ' ||
|
|
b[16] != ' ' || b[19] != ':' || b[22] != ':' || b[25] != ' ' {
|
|
return time.Time{}, false
|
|
}
|
|
if b[26] != 'G' || b[27] != 'M' || b[28] != 'T' {
|
|
return time.Time{}, false
|
|
}
|
|
|
|
day, ok := parse2Digits(b[5], b[6])
|
|
if !ok || day < 1 || day > 31 {
|
|
return time.Time{}, false
|
|
}
|
|
month, ok := parseMonth3(b[8], b[9], b[10])
|
|
if !ok {
|
|
return time.Time{}, false
|
|
}
|
|
year, ok := parse4Digits(b[12], b[13], b[14], b[15])
|
|
if !ok {
|
|
return time.Time{}, false
|
|
}
|
|
hour, ok := parse2Digits(b[17], b[18])
|
|
if !ok || hour > 23 {
|
|
return time.Time{}, false
|
|
}
|
|
minute, ok := parse2Digits(b[20], b[21])
|
|
if !ok || minute > 59 {
|
|
return time.Time{}, false
|
|
}
|
|
second, ok := parse2Digits(b[23], b[24])
|
|
if !ok || second > 59 {
|
|
return time.Time{}, false
|
|
}
|
|
|
|
t := time.Date(year, month, day, hour, minute, second, 0, httpDateGMT)
|
|
// Reject calendar-invalid dates like "31 Feb", which time.Date normalizes.
|
|
if t.Year() != year || t.Month() != month || t.Day() != day {
|
|
return time.Time{}, false
|
|
}
|
|
return t, true
|
|
}
|
|
|
|
func isWeekday3(a, b, c byte) bool {
|
|
a |= 0x20
|
|
b |= 0x20
|
|
c |= 0x20
|
|
k := uint32(a)<<16 | uint32(b)<<8 | uint32(c)
|
|
switch k {
|
|
case uint32('m')<<16 | uint32('o')<<8 | uint32('n'),
|
|
uint32('t')<<16 | uint32('u')<<8 | uint32('e'),
|
|
uint32('w')<<16 | uint32('e')<<8 | uint32('d'),
|
|
uint32('t')<<16 | uint32('h')<<8 | uint32('u'),
|
|
uint32('f')<<16 | uint32('r')<<8 | uint32('i'),
|
|
uint32('s')<<16 | uint32('a')<<8 | uint32('t'),
|
|
uint32('s')<<16 | uint32('u')<<8 | uint32('n'):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func parse2Digits(a, b byte) (int, bool) {
|
|
if a < '0' || a > '9' || b < '0' || b > '9' {
|
|
return 0, false
|
|
}
|
|
return int(a-'0')*10 + int(b-'0'), true
|
|
}
|
|
|
|
func parse4Digits(a, b, c, d byte) (int, bool) {
|
|
v1, ok := parse2Digits(a, b)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
v2, ok := parse2Digits(c, d)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return v1*100 + v2, true
|
|
}
|
|
|
|
func parseMonth3(a, b, c byte) (time.Month, bool) {
|
|
a |= 0x20
|
|
b |= 0x20
|
|
c |= 0x20
|
|
k := uint32(a)<<16 | uint32(b)<<8 | uint32(c)
|
|
switch k {
|
|
case uint32('j')<<16 | uint32('a')<<8 | uint32('n'):
|
|
return time.January, true
|
|
case uint32('f')<<16 | uint32('e')<<8 | uint32('b'):
|
|
return time.February, true
|
|
case uint32('m')<<16 | uint32('a')<<8 | uint32('r'):
|
|
return time.March, true
|
|
case uint32('a')<<16 | uint32('p')<<8 | uint32('r'):
|
|
return time.April, true
|
|
case uint32('m')<<16 | uint32('a')<<8 | uint32('y'):
|
|
return time.May, true
|
|
case uint32('j')<<16 | uint32('u')<<8 | uint32('n'):
|
|
return time.June, true
|
|
case uint32('j')<<16 | uint32('u')<<8 | uint32('l'):
|
|
return time.July, true
|
|
case uint32('a')<<16 | uint32('u')<<8 | uint32('g'):
|
|
return time.August, true
|
|
case uint32('s')<<16 | uint32('e')<<8 | uint32('p'):
|
|
return time.September, true
|
|
case uint32('o')<<16 | uint32('c')<<8 | uint32('t'):
|
|
return time.October, true
|
|
case uint32('n')<<16 | uint32('o')<<8 | uint32('v'):
|
|
return time.November, true
|
|
case uint32('d')<<16 | uint32('e')<<8 | uint32('c'):
|
|
return time.December, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// AppendUint appends n to dst and returns the extended dst.
|
|
func AppendUint(dst []byte, n int) []byte {
|
|
if n < 0 {
|
|
// developer sanity-check
|
|
panic("BUG: int must be positive")
|
|
}
|
|
|
|
return strconv.AppendUint(dst, uint64(n), 10)
|
|
}
|
|
|
|
// ParseUint parses uint from buf.
|
|
func ParseUint(buf []byte) (int, error) {
|
|
v, n, err := parseUintBuf(buf)
|
|
if n != len(buf) {
|
|
return -1, errUnexpectedTrailingChar
|
|
}
|
|
return v, err
|
|
}
|
|
|
|
var (
|
|
errEmptyInt = errors.New("empty integer")
|
|
errIPv4PartTooLarge = errors.New("ip part cannot exceed 255")
|
|
errUnexpectedFirstChar = errors.New("unexpected first char found. Expecting 0-9")
|
|
errUnexpectedTrailingChar = errors.New("unexpected trailing char found. Expecting 0-9")
|
|
errTooLongInt = errors.New("too long int")
|
|
)
|
|
|
|
func parseUintBuf(b []byte) (int, int, error) {
|
|
n := len(b)
|
|
if n == 0 {
|
|
return -1, 0, errEmptyInt
|
|
}
|
|
v := 0
|
|
for i := range n {
|
|
c := b[i]
|
|
k := c - '0'
|
|
if k > 9 {
|
|
if i == 0 {
|
|
return -1, i, errUnexpectedFirstChar
|
|
}
|
|
return v, i, nil
|
|
}
|
|
vNew := 10*v + int(k)
|
|
// Test for overflow.
|
|
if vNew < v {
|
|
return -1, i, errTooLongInt
|
|
}
|
|
v = vNew
|
|
}
|
|
return v, n, nil
|
|
}
|
|
|
|
func parseIPv4Octet(b []byte) (byte, int, error) {
|
|
if len(b) == 0 {
|
|
return 0, 0, errEmptyInt
|
|
}
|
|
|
|
var (
|
|
octet byte
|
|
parsed int
|
|
)
|
|
for i := range len(b) {
|
|
c := b[i]
|
|
k := c - '0'
|
|
if k > 9 {
|
|
if i == 0 {
|
|
return 0, parsed, errUnexpectedFirstChar
|
|
}
|
|
return 0, parsed, errUnexpectedTrailingChar
|
|
}
|
|
parsed = parsed*10 + int(k)
|
|
if octet > 25 || (octet == 25 && k > 5) {
|
|
return 0, parsed, errIPv4PartTooLarge
|
|
}
|
|
octet = octet*10 + k
|
|
}
|
|
return octet, parsed, nil
|
|
}
|
|
|
|
// ParseUfloat parses unsigned float from buf.
|
|
func ParseUfloat(buf []byte) (float64, error) {
|
|
// The implementation of parsing a float string is not easy.
|
|
// We believe that the conservative approach is to call strconv.ParseFloat.
|
|
// https://github.com/valyala/fasthttp/pull/1865
|
|
res, err := strconv.ParseFloat(b2s(buf), 64)
|
|
if res < 0 {
|
|
return -1, errors.New("negative input is invalid")
|
|
}
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
var (
|
|
errEmptyHexNum = errors.New("empty hex number")
|
|
errTooLargeHexNum = errors.New("too large hex number")
|
|
)
|
|
|
|
func readHexInt(r *bufio.Reader) (int, error) {
|
|
var k, i, n int
|
|
for {
|
|
c, err := r.ReadByte()
|
|
if err != nil {
|
|
if err == io.EOF && i > 0 {
|
|
return n, nil
|
|
}
|
|
return -1, err
|
|
}
|
|
k = int(hex2intTable[c])
|
|
if k == 16 {
|
|
if i == 0 {
|
|
return -1, errEmptyHexNum
|
|
}
|
|
if err := r.UnreadByte(); err != nil {
|
|
return -1, err
|
|
}
|
|
return n, nil
|
|
}
|
|
if i >= maxHexIntChars {
|
|
return -1, errTooLargeHexNum
|
|
}
|
|
n = (n << 4) | k
|
|
i++
|
|
}
|
|
}
|
|
|
|
var hexIntBufPool sync.Pool
|
|
|
|
func writeHexInt(w *bufio.Writer, n int) error {
|
|
if n < 0 {
|
|
// developer sanity-check
|
|
panic("BUG: int must be positive")
|
|
}
|
|
|
|
v := hexIntBufPool.Get()
|
|
if v == nil {
|
|
v = make([]byte, maxHexIntChars+1)
|
|
}
|
|
buf := v.([]byte)
|
|
i := len(buf) - 1
|
|
for {
|
|
buf[i] = lowerhex[n&0xf]
|
|
n >>= 4
|
|
if n == 0 {
|
|
break
|
|
}
|
|
i--
|
|
}
|
|
_, err := w.Write(buf[i:])
|
|
hexIntBufPool.Put(v)
|
|
return err
|
|
}
|
|
|
|
const (
|
|
upperhex = "0123456789ABCDEF"
|
|
lowerhex = "0123456789abcdef"
|
|
)
|
|
|
|
func lowercaseBytes(b []byte) {
|
|
for i := range b {
|
|
p := &b[i]
|
|
*p = toLowerTable[*p]
|
|
}
|
|
}
|
|
|
|
// AppendUnquotedArg appends url-decoded src to dst and returns appended dst.
|
|
//
|
|
// dst may point to src. In this case src will be overwritten.
|
|
func AppendUnquotedArg(dst, src []byte) []byte {
|
|
return decodeArgAppend(dst, src)
|
|
}
|
|
|
|
// AppendQuotedArg appends url-encoded src to dst and returns appended dst.
|
|
func AppendQuotedArg(dst, src []byte) []byte {
|
|
for _, c := range src {
|
|
switch {
|
|
case c == ' ':
|
|
dst = append(dst, '+')
|
|
case quotedArgShouldEscapeTable[int(c)] != 0:
|
|
dst = append(dst, '%', upperhex[c>>4], upperhex[c&0xf])
|
|
default:
|
|
dst = append(dst, c)
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func appendQuotedPath(dst, src []byte) []byte {
|
|
// Fix issue in https://github.com/golang/go/issues/11202
|
|
if len(src) == 1 && src[0] == '*' {
|
|
return append(dst, '*')
|
|
}
|
|
|
|
for _, c := range src {
|
|
if quotedPathShouldEscapeTable[int(c)] != 0 {
|
|
dst = append(dst, '%', upperhex[c>>4], upperhex[c&0xf])
|
|
} else {
|
|
dst = append(dst, c)
|
|
}
|
|
}
|
|
return dst
|
|
}
|